Developing OAuth2 Servers - 6.3

Talend ESB Service Developer Guide

EnrichVersion
6.3
EnrichProdName
Talend Data Fabric
Talend Data Services Platform
Talend ESB
Talend MDM Platform
Talend Open Studio for ESB
Talend Real-Time Big Data Platform
task
Design and Development
Installation and Upgrade
EnrichPlatform
Talend ESB

The OAuth2 server is the core piece of the complete OAuth2-based solution. Typically it contains 2 services for:

  1. Authorizing request tokens by asking the end users to let clients access some of their resources and returning the grants back to the client (Authorization Service)

  2. Exchanging the token grants for access tokens (Access Token Service)

CXF offers several JAX-RS service implementations that can be used to create the OAuth2 servers fast: AuthorizationCodeGrantService and ImplicitGrantService for managing the redirection-based flows, as well as AccessTokenService for exchanging the grants for new tokens. Note that some grants that do not require the redirection-based support, such as SAML2 one, etc, may only require an Access Token Service be operational.

All of these services rely on the custom OAuthDataProvider which persists the access tokens and converts the opaque scope values to the information that can be presented to the users. Additionally, AuthorizationCodeDataProvider is an OAuthDataProvider which can keep temporary information about the authorization code grants which needs to be removed after the tokens are requested in exchange.

Writing your own OAuthDataProvider implementation is what is needed to get the OAuth2 server up and running. In many cases all you need to do is to persist or remove the Authorization Code Grant data, use one of the available utility classes to create a new access token and also persist it or remove the expired one, and finally convert the optional opaque scope values (if any are supported) to a more view-able information.

Authorization Service

The main responsibility of OAuth2 Authorization Service is to present an end user with a form asking the user to allow or deny the client accessing some of the user resources. CXF offers AuthorizationCodeGrantService and ImplicitGrantService for accepting the redirection requests, challenging the end users with the authorization forms, handling the end user decisions and returning the results back to the clients.

One of the differences between the AuthorizationCode and Implicit flows is that in the latter case the grant is the actual access token which is returned as the URI fragment value. The way the end user is asked to authorize the client request is similar between the two flows. In this section we will assume that the Authorization Code flow is being exercised.

A third-party client redirects the current user to AuthorizationCodeGrantService, for example, here is how a redirection may happen:

Response-Code: 303
Headers: {Location=[http://localhost:8080/services/social/authorize?
   client_id=123456789&scope=updateCalendar-7&response_type=code
   &redirect_uri=http%3A//localhost%3A8080/services/reservations/reserve
   /complete&state=1],Date=[Thu, 12 Apr 2012 12:26:21 GMT], 
   Content-Length=[0]}

The client application asks the current user (the browser) to go to a new address provided by the Location header and the follow-up request to AuthorizationCodeGrantService will look like this:

Address: http://localhost:8080/services/social/authorize?client_id=
   123456789&scope=updateCalendar-7&response_type=code&redirect_uri=
   http%3A//localhost%3A8080/services/reservations/reserve/complete&state=1
Http-Method: GET
Headers: {
Accept=[text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8],
Authorization=[Basic YmFycnlAc29jaWFsLmNvbToxMjM0], 
Cookie=[JSESSIONID=suj2wyl54c4g], 
Referer=[http://localhost:8080/services/forms/reservation.jsp]
...
}

Note that the end user needs to authenticate. The Request URI includes the client_id, custom scope value, response_type set to 'code', the current request state and the redirect uri. Note the scope is optional - the Authorization Service will usually allocate a default scope; however even if the client does include an additional custom scope the end user may still not approve it. The redirect uri is also optional, assuming one or more ones redirect URIs have been provided at the client registration time.

AuthorizationCodeGrantService will report a warning is no secure HTTPS transport is used:

12-Apr-2012 13:26:21 
   org.apache.cxf.rs.security.oauth2.services.AbstractOAuthService
   checkTransportSecurity
WARNING: Unsecure HTTP, Transport Layer Security is recommended

It can also be configured to reject the requests over un-secure HTTP transport.

AuthorizationCodeGrantService will retrieve the information about the client application to populate an instance of OAuthAuthorizationData bean and return it. OAuthAuthorizationData contains application name and URI properties, optional list of Permissions and other properties which can be either presented to the user or kept in the hidden form fields in order to uniquely identify the actual authorization request when the end user returns the decision.

One important OAuthAuthorizationData property is "authenticityToken". It is used for validating that the current session has not been hijacked - AuthorizationCodeGrantService generates a random key, stores it in a Servlet HTTPSession instance and expects the returned authenticityToken value to match it - this is a recommended approach and it also implies that the authenticityToken value is hidden from a user, for example, it's kept in a 'hidden' form field. The other properties which are meant to be hidden are clientId, state, redirectUri, proposedScope. The helper "replyTo" property is an absolute URI identifying the AuthorizationCodeGrantService handler processing the user decision and can be used by view handlers when building the forms or by other OAuthAuthorizationData handlers.

So the populated OAuthAuthorizationData is finally returned. Note that it's a JAXB XMLRootElement-annotated bean and can be processed by registered JAXB or JSON providers given that AuthorizationCodeGrantService supports producing "application/xml" and "application/json" (See the OAuth Without Browser section below for more). But in this case we have the end user working with a browser so an HTML form is what is really expected back.

AuthorizationCodeGrantService supports producing "text/html" and simply relies on a registered RequestDispatcherProvider to set the OAuthAuthorizationData bean as an HttpServletRequest attribute and redirect the response to a view handler (can be JSP or some other servlet) to actually build the form and return it to the user. Alternatively, registering XSLTJaxbProvider would also be a good option for creating HTML views.

Assuming RequestDispatcherProvider is used, the following example log shows the initial response from AuthorizationCodeGrantService:

12-Apr-2012 13:26:21 org.apache.cxf.jaxrs.provider.
    RequestDispatcherProvider logRedirection
INFO: Setting an instance of "org.apache.cxf.rs.security.oauth2.common.
    OAuthAuthorizationData" as HttpServletRequest attribute "data" and 
    redirecting the response to "/forms/oauthAuthorize.jsp".

Note that a "/forms/oauthAuthorize.jsp" view handler will create an HTML view - this is a custom JSP handler and whatever HTML view is required can be created there, using the OAuthAuthorizationData bean for building the view. Most likely you will want to present a form asking the user to allow or deny the client accessing some of this user's resources. If OAuthAuthorizationData has a list of Permissions set then adding the information about the permissions is needed.

Next the user makes a decision and selects a button allowing or denying the client accessing the resources. The form data are submitted to AuthorizationCodeGrantService:

Address: http://localhost:8080/services/social/authorize/decision
Encoding: ISO-8859-1
Http-Method: POST
Content-Type: application/x-www-form-urlencoded
Headers: {
Authorization=[Basic YmFycnlAc29jaWFsLmNvbToxMjM0],
Content-Type=[application/x-www-form-urlencoded],
...
}
--------------------------------------
12-Apr-2012 15:36:29 org.apache.cxf.jaxrs.utils.FormUtils 
    logRequestParametersIfNeeded
INFO: updateCalendar-7_status=allow&readCalendar_status=allow
&scope=updateCalendar-7+readCalendar
&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Fservices%2F
    reservations%2Freserve%2Fcomplete
&session_authenticity_token=4f0005d9-565f-4309-8ffb-c13c72139ebe
    &oauthDecision=allow
&state=1&client_id=123456789

AuthorizationCodeGrantService will use a session_authenticity_token to validate that the session is valid and will process the user decision next.

If the decision is "allow" then it will check the status of the individual scope values. It relies on the "scopename_status" convention, if the form has offered the user a chance to selectively enable individual scopes then name/value pairs such as "updateCalendar-7_status=allow" are submitted. If none of such pairs is coming back then it means the user has approved all the default and additional (if any) scopes. Next it will ask OAuthDataProvider to generate an authorization code grant and return it alongside with the state if any by redirecting the current user back to the redirect URI:

Response-Code: 303
Headers: {
 Location=[http://localhost:8080/services/reservations/reserve/complete
 ?state=1&code=5c993144b910bccd5977131f7d2629ab], 
 Date=[Thu, 12 Apr 2012 14:36:29 GMT], 
 Content-Length=[0]}

Which leads to a browser redirecting the user:

Address: http://localhost:8080/services/reservations/reserve/complete?
state=1&code=5c993144b910bccd5977131f7d2629ab
Http-Method: GET
Headers: {
Accept=[text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8],
Authorization=[Basic YmFycnlAcmVzdGF1cmFudC5jb206NTY3OA==], 
Cookie=[JSESSIONID=1c289vha0cxfe],
}

If a user decision was set to "deny" then the error will be returned to the client. Assuming the decision was "allow", the client has now received back the authorization code grant and is ready to exchange it for a new access token.

AccessTokenService

The role of AccessTokenService is to exchange a token grant for a new access token which will be used by the client to access the end user's resources. Here is an example request log:

Address: http://localhost:8080/services/oauth/token
Http-Method: POST

Headers: {
Accept=[application/json], 
Authorization=[Basic MTIzNDU2Nzg5Ojk4NzY1NDMyMQ==], 
Content-Type=[application/x-www-form-urlencoded]
}
Payload: 

grant_type=authorization_code&code=5c993144b910bccd5977131f7d2629ab
   &redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Fservices%2Freservations
   %2Freserve%2Fcomplete

This request contains a client_id and client_secret (Authorization header), the grant_type, the grant value (code) plus the redirect URI the authorization grant was returned to which is needed for the additional validation. Note that the alternative client authentication methods are also possible, in this case the token service will expect a mapping between the client credentials and the client_id representing the client registration available.

After validating the request, the service will find a matching AccessTokenGrantHandler and request to create a ServerAccessToken which is a server-side representation of the access token. The grant handlers, such as AuthorizationCodeGrantHandler may delegate the creation of the actual access token to data providers, which may create Bearer or MAC tokens with the help of utility classes shipped with CXF or depend on other 3rd party token libraries.

The data providers are not strictly required to persist the data such as access tokens, instead the token key may act as an encrypted bag capturing all the relevant information.

Now that the token has been created, it is mapped by the service to a client representation and is returned back as a JSON payload:

Response-Code: 200
Content-Type: application/json
Headers: {
 Cache-Control=[no-store], 
 Pragma=[no-cache], 
 Date=[Thu, 12 Apr 2012 14:36:29 GMT]
}

Payload: 

{"access_token":"5b5c8e677413277c4bb8b740d522b378", "token_type":"bearer"}

The client will use this access token to access the current user's resources in order to complete the original user's request, for example, the request to access a user's calendar may look like this:

Address: http://localhost:8080/services/thirdPartyAccess/calendar
Http-Method: GET
Headers: 
{
  Authorization=[Bearer 5b5c8e677413277c4bb8b740d522b378], 
  Accept=[application/xml]
}

Note that the access token key is passed as the Bearer scheme value. Other token types such as MAC ones, etc, can be represented differently.

Access Token Types

As mentioned above, AccessTokenService can work with whatever token is created by a given data provider. This section provides more information on how CXF may help with supporting Bearer and MAC tokens.

Bearer

The following code fragment shows how a BearerAccessToken utility class can be used to create Bearer tokens:

import org.apache.cxf.rs.security.oauth2.common.AccessTokenRegistration; 
import org.apache.cxf.rs.security.oauth2.common.ServerAccessToken; 
import org.apache.cxf.rs.security.oauth2.tokens.bearer.BearerAccessToken; 

public class CustomOAuthDataProvider implements AuthorizationCodeDataProvider { 

   public ServerAccessToken createAccessToken(AccessTokenRegistration reg) 
      throws OAuthServiceException { 

      ServerAccessToken token = new BearerAccessToken(reg.getClient(), 3600L); 

      List<String> scope = reg.getApprovedScope().isEmpty() ? 
         reg.getRequestedScope() : reg.getApprovedScope(); 
      token.setScopes(convertScopeToPermissions(reg.getClient(), scope)); 
      token.setSubject(reg.getSubject()); 
      token.setGrantType(reg.getGrantType()); 

      // persist as needed and then return 

      return token; 
   } 
   // other methods not shown
}

CustomOAuthDataProvider will also be asked by OAuthRequestFilter to validate the incoming Bearer tokens given that they typically act as database key or key alias, if no Bearer token validator is registered.

MAC

CXF 2.6.2 supports MAC tokens as specified in the latest MAC Access Authentication draft. MAC tokens offer an option for clients to demonstrate they 'hold' the token secret issued to them by AccessTokenService. It is recommended that AccessTokenService endpoint issuing MAC tokens enforces a two-way TLS for an extra protection of the MAC token data returned to clients.

The following code fragment shows how a MacAccessToken utility class can be used to create MAC tokens:

import org.apache.cxf.rs.security.oauth2.common.AccessTokenRegistration; 
import org.apache.cxf.rs.security.oauth2.common.ServerAccessToken; 
import org.apache.cxf.rs.security.oauth2.tokens.mac.HmacAlgorithm; 
import org.apache.cxf.rs.security.oauth2.tokens.mac.MacAccessToken; 

public class CustomOAuthDataProvider implements AuthorizationCodeDataProvider { 

   public ServerAccessToken createAccessToken(AccessTokenRegistration reg) 
      throws OAuthServiceException { 

      // generate 
      ServerAccessToken token = new MacAccessToken(reg.getClient(), 
         HmacAlgorithm.HmacSHA1, 3600L); 

      // set other token fields as shown in the Bearer section 

      // persist as needed and then return 

      return token; 
   } 
   // other methods not shown 
}

One can expect the following response:

Response-Code: 200 
Content-Type: application/json 
Headers: { 
Cache-Control=[no-store], 
Pragma=[no-cache], 
Date=[Thu, 12 Apr 2012 14:36:29 GMT]
} 

Payload: 

{"access_token":"5b5c8e677413277c4bb8b740d522b378", "token_type":"mac",
"secret"="1234568", algorithm="hmac-sha-1"} 

Note that 'access_token' is the MAC key identifier, 'secret' - MAC key.

MacAccessTokenValidator has to be registered with OAuthRequestFilter for validating the incoming MAC tokens. This validator can get a reference to custom NonceVerifier with CXF possibly shipping a default implementation in the future.

The client can use CXF OAuthClientUtils to create Authorization MAC headers. All is needed is to provide references to ClientAccessToken representing the MAC token issued by AccessTokenService and HttpRequestProperties capturing the information about the current request URI:

String requestURI = "http://localhost:8080/calendar"; 
WebClient wc = WebClient.create(requestURI); 

// represents client registration 
OAuthClientUtils.Consumer consumer = getConsumer(); 
// the token issued by AccessTokenService 
ClientAccessToken token = getToken(); 

HttpRequestProperties httpProps = new HttpRequestProperties(wc, "GET"); 
String authHeader = OAuthClientUtils.createAuthorizationHeader(consumer, token, 
   httpProps); 
wc.header("Authorization", authHeader); 

Calendar calendar = wc.get(Calendar.class);

This code will result in something like:

GET /calendar HTTP/1.1 
Host: localhost 
Accept: application/xml 
Authorization: MAC id="5b5c8e677413277c4bb8b740d522b378", 
nonce="273156:di3hvdf8", 
mac="W7bdMZbv9UWOTadASIQHagZyirA=" 
ext="12345678"

where the 'ext' attribute is used to pass a timestamp value.

Writing OAuthDataProviders

Using CXF OAuth service implementations will help a lot with setting up an OAuth server. As you can see from the above sections, these services rely on a custom OAuthDataProvider implementation.

The main task of OAuthDataProvider is to persist and generate access tokens. Additionally, as noted above, AuthorizationCodeDataProvider needs to persist and remove the code grant registrations. The way it's done is really application-specific. Consider starting with a basic memory based implementation and then move on to keeping the data in some DB.

Note that OAuthDataProvider supports retrieving Client instances but it has no methods for creating or removing Clients. The reason for it is that the process of registering third-party clients is very specific to a particular OAuth2 application, so CXF does not offer a registration support service and hence OAuthDataProvider has no Client create/update methods. You will likely need to do something like this:

public class CustomOAuthProvider implements OAuthDataProvider {
   public Client registerClient(String applicationName, 
      String applicationURI, ...) {}
   public void removeClient(String cliendId) {}
   // ...
   // OAuthDataProvider methods
}

CustomOAuthProvider will also remove all tokens associated with a given Client in removeClient(String cliendId).

Finally OAuthDataProvider may need to convert opaque scope values such as "readCalendar" into a list of OAuthPermissions. AuthorizationCodeGrantService and OAuth2 security filters will depend on it (assuming scopes are used in the first place). In the former case AuthorizationCodeGrantService will use this list to populate OAuthAuthorizationData - the reason this bean only sees Permissions is that some of the properties OAuthPermission keeps are of no interest to OAuthAuthorizationData handlers.

OAuth Server JAX-RS endpoints

With CXF offering OAuth service implementations and a custom OAuthDataProvider provider in place, it is time to deploy the OAuth2 server. Most likely, you'd want to deploy AccessTokenService as an independent JAX-RS endpoint, for example:

<!-- implements OAuthDataProvider -->
<bean id="oauthProvider" class="oauth.manager.OAuthManager"/>
     
<bean id="accessTokenService" 
  class="org.apache.cxf.rs.security.oauth2.services.AccessTokenService">
  <property name="dataProvider" ref="oauthProvider"/>
</bean>

<jaxrs:server id="oauthServer" address="/oauth">
   <jaxrs:serviceBeans>
      <ref bean="accessTokenService"/>
  </jaxrs:serviceBeans>
</jaxrs:server>

AccessTokenService listens on a relative "/token" path. Given that jaxrs:server/@adress is "/oauth" and assuming a context name is "/services", the absolute address of AccessTokenService would be something like http://localhost:8080/services/oauth/token.

AuthorizationCodeGrantService is better to put where the main application endpoint is. It can be put alongside AccessTokenService - but the problem is that the end user is expected to authenticate itself with the resource server after it has been redirected by a third-party client to AuthorizationCodeGrantService. That would make it more complex for the OAuth server endpoint to manage both OAuth (third-party client) and the regular user authentication - that can be done, see more on it below in the Design considerations section, but the simpler option is to simply get AuthorizationCodeGrantService under the control of the security filter enforcing the end user authentication:

<bean id="authorizationService" 
  class="org.apache.cxf.rs.security.oauth2.services.  //
         AuthorizationCodeGrantService">
  <property name="dataProvider" ref="oauthProvider"/>
</bean>

<bean id="myApp" class="org.myapp.MyApp">
  <property name="dataProvider" ref="oauthProvider"/>
</bean>

<jaxrs:server id="oauthServer" address="/myapp">
   <jaxrs:serviceBeans>
      <ref bean="myApp"/>
      <ref bean="authorizationService"/>
  </jaxrs:serviceBeans>
</jaxrs:server>

AuthorizationCodeGrantService listens on a relative "/authorize" path so in this case its absolute address will be something like http://localhost:8080/services/myapp/authorize. This address and that of AccessTokenService will be used by third-party clients.