Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

 

 

 

Wiki Markup
{span:style=font-size:2em;font-weight:bold} JAX-RS: OAuth {span}

 

 

 

Table of Contents

Introduction

...

Maven dependencies

Code Block
xml
xml

<dependency>
  <groupId>org.apache.cxf</groupId>
  <artifactId>cxf-rt-rs-security-oauth</artifactId>
  <version>2.5.0</version>
</dependency>

...

Here is an example request log:

Code Block
xml
xml

Address: http://localhost:8080/services/oauth/initiate
Encoding: ISO-8859-1
Http-Method: POST
Content-Type: */*
Headers: {
Accept=[application/x-www-form-urlencoded], 

Content-Length=[0],

Authorization=[OAuth oauth_callback="http%3A%2F%2Flocalhost%3A8080%2Fservices%2Freservations%2Freserve%2Fcomplete", 
                     oauth_nonce="e365fa02-772e-4e33-900d-00a766ccadf8", 
                     oauth_consumer_key="123456789", 
                     oauth_signature_method="HMAC-SHA1", 
                     oauth_timestamp="1320748683", 
                     oauth_version="1.0", 
                     oauth_signature="ztTQuqaJS7L6dNQwn%2Fqi1MdaqQQ%3D"] 
}

...

Finally, one more property that may be set on this bean instance: list of scopes. List of scopes represents optional permissions that the consumer may need to access the resources. These can be provided by an "x_oauth_scope" ("scope" in OAuth 2.0) request parameter, for example,

Code Block
xml
xml

Authorization=[OAuth ..., 
                     x_oauth_scope="readCalendar updateCalendar"]

...

After a new request token has been created by OAuthDataProvider, RequestTokenService returns the token key and secret pair to the consumer:

Code Block
xml
xml

Response-Code: 200
Content-Type: application/x-www-form-urlencoded
Headers: {Date=[Tue, 08 Nov 2011 10:38:03 GMT]}
Payload: 
oauth_callback_confirmed=true&oauth_token=6dfd5e52-236c-4939-8df8-a53212f7d2a2&oauth_token_secret=ca8273df-b9b0-43f9-9875-cfbb54ced550

...

Remember that a third-party consumer redirects the current user to AuthorizationRequestService, for example, here is how a redirection may happen:

Code Block
xml
xml

Response-Code: 303
Headers: {Location=[http://localhost:8080/services/social/authorize?oauth_token=f4415e16-56ea-465f-9df1-8bd769253a7d]}

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

Code Block
xml
xml

Address: http://localhost:8080/services/social/authorize?oauth_token=6dfd5e52-236c-4939-8df8-a53212f7d2a2
Http-Method: GET
Content-Type: 
Headers: {
Accept=[text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8],   
Referer=[http://localhost:8080/services/forms/reservation.jsp], 
...
}

...

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

Code Block
xml
xml

08-Nov-2011 13:32:40 org.apache.cxf.jaxrs.provider.RequestDispatcherProvider logRedirection
INFO: Setting an instance of "org.apache.cxf.rs.security.oauth.data.OAuthAuthorizationData" as HttpServletRequest attribute 
"data" and redirecting the response to "/forms/oauthAuthorize.jsp".

08-Nov-2011 13:32:40 org.apache.cxf.interceptor.LoggingOutInterceptor
---------------------------
Response-Code: 200
Content-Type: text/html

...

Next the user makes a decision and selects a button allowing or denying the consumer accessing the resources. AuthorizationRequestService does not need to know how a user has been asked to make the decision, but it expects to receive a form-based submission containing the following 3 parameters, named "session_authenticity_token" and "oauth_token" with values matching those of OAuthAuthorizationData's "authenticityToken" and "oauthToken" properties, and "oAuthDecision" with either "allow" or "deny" values:

Code Block
xml
xml

Address: http://localhost:8080/services/social/authorize/decision
Http-Method: POST
Content-Type: application/x-www-form-urlencoded
Headers: {
Authorization=[Basic YmFycnlAc29jaWFsLmNvbToxMjM0],
Cookie=[JSESSIONID=eovucah9rwqp], 
Referer=[http://localhost:8080/services/social/authorize?oauth_token=6dfd5e52-236c-4939-8df8-a53212f7d2a2], 
User-Agent=[Mozilla/5.0 (X11; Linux x86_64; rv:2.0) Gecko/20100101 Firefox/4.0]}
--------------------------------------
09-Nov-2011 16:41:58 org.apache.cxf.jaxrs.utils.FormUtils logRequestParametersIfNeeded
INFO: session_authenticity_token=e52b5033-9bf5-4b34-9d3a-39a7d5b7e686&oauthDecision=allow&oauth_token=6dfd5e52-236c-4939-8df8-a53212f7d2a2

AuthorizationRequestService will use a session_authenticity_token to validate that the session is valid and will process the user decision next.
If it is set to "allow" then it will ask OAuthDataProvider to generate an authorization key (verifier) and return this verifier alongside with the request token key and the state if any by redirecting the current user back to the callback URI provided during the request token request:

Code Block
xml
xml

Response-Code: 303
Headers: {
Location=[http://localhost:8080/services/reservations/reserve/complete?
oauth_token=6dfd5e52-236c-4939-8df8-a53212f7d2a2&oauth_verifier=00bd8fa7-4233-42a2-8957-0a0a22c684ba]
}

which leads to a browser redirecting the user:

Code Block
java
java

Address: http://localhost:8080/services/reservations/reserve/complete?
oauth_token=6dfd5e52-236c-4939-8df8-a53212f7d2a2&oauth_verifier=00bd8fa7-4233-42a2-8957-0a0a22c684ba
Http-Method: GET
Content-Type: 
Headers: {
Authorization=[Basic YmFycnlAc29jaWFsLmNvbToxMjM0], 
Cookie=[JSESSIONID=eovucah9rwqp],
Referer=[http://localhost:8080/services/social/authorize?oauth_token=6dfd5e52-236c-4939-8df8-a53212f7d2a2], 
User-Agent=[Mozilla/5.0 (X11; Linux x86_64; rv:2.0) Gecko/20100101 Firefox/4.0]}

...

The OAuth 1.0 mentions so called "oob" (out-of-band) callbacks. If the third-party client is not running as a web application or if it is known it can not receive the redirect response from AuthorizationRequestService for whatever reasons, then a callback URI can be set to "oob", when a request token is
requested:

Code Block
xml
xml

Address: http://localhost:8080/services/oauth/initiate
Encoding: ISO-8859-1
Http-Method: POST
Content-Type: */*
Headers: {
Accept=[application/x-www-form-urlencoded], 

Content-Length=[0],

Authorization=[OAuth oauth_callback="oob", 
                     oauth_nonce="e365fa02-772e-4e33-900d-00a766ccadf8", 
                     oauth_consumer_key="123456789", 
                     oauth_signature_method="HMAC-SHA1", 
                     oauth_timestamp="1320748683", 
                     oauth_version="1.0", 
                     oauth_signature="ztTQuqaJS7L6dNQwn%2Fqi1MdaqQQ%3D"] 
}

...

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

Code Block
xml
xml

Address: http://localhost:8080/services/oauth/token
Http-Method: POST
Headers: {
Accept=[application/x-www-form-urlencoded], 
Authorization=[OAuth oauth_signature_method="HMAC-SHA1", 
                     oauth_consumer_key="123456789", 
                     oauth_token="6dfd5e52-236c-4939-8df8-a53212f7d2a2", 
                     oauth_verifier="00bd8fa7-4233-42a2-8957-0a0a22c684ba", 
                     oauth_timestamp="1320760259", 
                     oauth_nonce="16237669362301", 
                     oauth_version="1.0", 
                     oauth_signature="dU%2BhXPNFfFpX2sC74IOxzTjdVrY%3D"]
}

...

Next it asks the data provider to create a new AccessToken based on this RequestToken. The resulting access token key and secret pair is returned back to a consumer:

Code Block
xml
xml

Response-Code: 200
Content-Type: application/x-www-form-urlencoded
Headers: {Date=[Tue, 08 Nov 2011 13:50:59 GMT]}
Payload: oauth_token=abc15aca-2073-4bde-b1be-1a02dc7ccafe&oauth_token_secret=859dfe9e-ca4c-4b36-9e60-044434ab636c

The consumer 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:

Code Block
xml
xml

Address: http://localhost:8080/services/user/calendar
Http-Method: GET
Headers: {
Accept=[application/XML], 
Authorization=[OAuth oauth_signature_method="HMAC-SHA1", 
                     oauth_consumer_key="123456789", 
                     oauth_token="abc15aca-2073-4bde-b1be-1a02dc7ccafe", 
                     oauth_version="1.0", 
                     oauth_signature="dU%2BhXPNFfFpX2sC74IOxzTjdVrY%3D"]
}

...

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 consumers is very specific to a particular OAuth 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:

Code Block
java
java

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

...

When creating RequestToken or AccessToken tokens as well as authorization keys, OAuthDataProvider will need to create unique identifiers.
The way it's done is application specific and custom implementations may also use a utility MD5SequenceGenerator shipped with CXF, for example:

Code Block
java
java

public String setRequestTokenVerifier(RequestToken requestToken) throws OAuthServiceException {
    requestToken.setVerifier(generateSequence());
    return requestToken.getVerifier();
}

private String generateSequence() throws OAuthServiceException {
    try {
       return tokenGenerator.generate(UUID.randomUUID().toString().getBytes("UTF-8"));
    } catch (Exception e) {
       throw new OAuthServiceException("Unable to generate the key", e.getCause());
    }
}

...

With CXF offering OAuth service implementations and a custom OAuthAuthorizationData provider in place, it is time to deploy the OAuth server.
Most likely, you'd want to deploy RequestTokenService and AccessTokenService as two root resources inside a single JAX-RS endpoint (or have one RequestTokenService and one AccessTokenService endpoint), for example:

Code Block
xml
xml

<!-- implements OAuthDataProvider -->
<bean id="oauthProvider" class="oauth.manager.OAuthManager"/>

<bean id="requestTokenService" class="org.apache.cxf.rs.security.oauth.services.RequestTokenService">
   <property name="dataProvider" ref="oauthProvider"/>
</bean>
     
<bean id="accessTokenService" class="org.apache.cxf.rs.security.oauth.services.AccessTokenService">
  <property name="dataProvider" ref="oauthProvider"/>
</bean>

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

...

AuthorizationRequestService is better to put where the main application endpoint is. It can be put alongside RequestTokenService and 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 consumer to AuthorizationRequestService. That would make it more complex for the OAuth server endpoint to manage both OAuth (third-party consumer) 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 AuthorizationRequestService under the control of the security filter enforcing the end user authentication:

Code Block
java
java

<bean id="authorizationService" class="org.apache.cxf.rs.security.oauth.services.AuthorizationRequestService">
  <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>

...

When checking a request like this:

Code Block
xml
xml

Address: http://localhost:8080/services/user/calendar
Http-Method: GET
Headers: {
Accept=[application/XML], 
Authorization=[OAuth oauth_signature_method="HMAC-SHA1", 
                     oauth_consumer_key="123456789", 
                     oauth_token="abc15aca-2073-4bde-b1be-1a02dc7ccafe", 
                     oauth_version="1.0", 
                     oauth_signature="dU%2BhXPNFfFpX2sC74IOxzTjdVrY%3D"]
}

...

For example, the following custom code can be used by the third-party application:

Code Block
java
java

public class OAuthClientManager {
	
	private WebClient accessTokenService;
        private WebClient requestTokenService;
        private String authorizationServiceURI;
    
        // inject properties...
	
	public URI getAuthorizationServiceURI(String token) {
	    return OAuthClientUtils.getAuthorizationURI(authorizationServiceURI, token);
	}
	
	public Token getRequestToken(URI callback) {
	    try {
	        return OAuthClientUtils.getRequestToken(requestTokenService, consumer, callback, null);
	    } catch (OAuthServiceException ex) {
               return null;
            }    
	}
	
	public Token getAccessToken(Token requestToken, String verifier) {
	    try {
	        return OAuthClientUtils.getAccessToken(accessTokenService, consumer, requestToken, verifier);
	    } catch (OAuthServiceException ex) {
	        return null;
	    }
	}
	
	public String createAuthorizationHeader(Token token, String method, String requestURI) {
            return OAuthClientUtils.createAuthorizationHeader(consumer, token, method, requestURI);
	}
}

...

CXF OAuth 1.0 services will report only HTTP status code in case of various OAuth-related errors to minimize the information about the actual cause of the failure and will log the details locally. If providing the extra error information can help with debugging 3rd-party applications or if such application can indeed recover from the failures based on such details, then setting a contextual "report.failure.details" property to "true" will get the error messages available in the response body. Some OAuth1.0 implementers have chosen to return a custom "oauth_problem" HTTP header instead - this option can be supported by additionally setting a contextual "report.failure.details.as.header" property to "true", for example:

Code Block
xml
xml

<jaxrs:server id="oauthServer" address="/initiate">
        <jaxrs:serviceBeans>
            <bean class="org.apache.cxf.rs.security.oauth.services.RequestTokenService"/>
        </jaxrs:serviceBeans>
        <jaxrs:properties>
           <entry key="report.failure.details" value="true"/>
           <entry key="report.failure.details.as.header" value="true"/>
        </jaxrs:properties>
</jaxrs:server>

...

The first problem which needs to be addressed is how to distinguish end users from third-party consumers and get both parties authenticated as required.
Perhaps the simplest option is to extend a CXF OAuth filter (JAX-RS or servlet one), check Authorization header, if it is OAuth then delegate to the superclass, alternatively - proceed with authenticating the end users:

Code Block
java
java

public class SecurityFilter extends org.apache.cxf.rs.security.oauth.filters.OAuthRequestFilter {
   @Context
   private HttpHeaders headers;

   public Response handleRequest(ClassResourceInfo cri, Message message) {
       String header = headers.getRequestHeaders().getFirst("Authorization");
       if (header.startsWith("OAuth ")) {
           return super.handleRequest(cri, message);
       } else {
           // authenticate the end user
       }
   }

} 

The next issue is how to enforce that the end users can only access the resources they've been authorized to access.
For example, consider the following JAX-RS resource class:

Code Block
java
java

@Path("calendar")
public class CalendarResource {

   @GET
   @Path("{id}")
   public Calendar getPublicCalendar(@PathParam("id") long id) {
       // return the calendar for a user identified by 'id'
   }

   @GET
   @Path("{id}/private")
   public Calendar getPrivateCalendar(@PathParam("id") long id) {
       // return the calendar for a user identified by 'id'
   }

   @PUT
   @Path("{id}")
   public void updateCalendar(@PathParam("id") long id, Calendar c) {
       // update the calendar for a user identified by 'id'
   }
}

...