Versions Compared

Key

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

...

JAX-RS:

...

OAuth2

...

Table of Contents

Introduction

...

Maven dependencies

Code Block
xml
xml

<dependency>
  <groupId>org.apache.cxf</groupId>
  <artifactId>cxf-rt-rs-security-oauth2</artifactId>
  <version>2.7.5</version>
</dependency>

...

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

Code Block
xml
xml

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:

Code Block
xml
xml

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]
...
}

...

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

Code Block
xml
xml

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

...

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

Code Block
xml
xml

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".

...

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:

Code Block
xml
xml

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%2Freservations%2Freserve%2Fcomplete&session_authenticity_token=4f0005d9-565f-4309-8ffb-c13c72139ebe&oauthDecision=allow&state=1&client_id=123456789

...

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:

Code Block
xml
xml

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:

Code Block
java
java

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],
}

...

Having OOB responses supported is useful when a public client (typically a device which can not keep the client secrets and where no redirect URI is supported) needs to get a code grant. What will happen is that a device owner will send a request to Authorization Service which may look like this:

No Format

GET
http://localhost:8080/services/social/authorize?client_id=mobileClient&response_type=code   

...

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:

Code Block
xml
xml

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

...

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:

Code Block
xml
xml

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:

Code Block
xml
xml

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

...

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

Code Block
java
java

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 are not shown
}

...

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

Code Block
java
java

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 are not shown
}

One can expect the following response:

Code Block
xml
xml

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", "mac_key"="1234568", "mac_algorithm"="hmac-sha-1"}

...

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:

Code Block
java
java

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:

Code Block
xml
xml

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

...

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:

Code Block
java
java

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

...

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:

Code Block
xml
xml

<!-- 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>

...

If the remote token validation is supported then have AccessTokenValidationService added too:

Code Block
xml
xml

<!-- 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>
<bean id="accessTokenValidateService" class="org.apache.cxf.rs.security.oauth2.services.AccessTokenValidateService">
  <property name="dataProvider" ref="oauthProvider"/>
</bean>


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

...

AuthorizationCodeGrantService is easier to put where the application endpoints are. It can be put alongside AccessTokenService, but ideally an SSO based authentication solution will be also be deployed, for the end user to avoid signing in separately several times (see more in it below). Here is an example of AuthorizationCodeGrantService being collocated with the application endpoint:

Code Block
xml
xml

<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"/>

<jaxrs:server id="appServer" 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/thirdPartyAccess/calendar
Http-Method: GET
Headers: 
{
  Authorization=[Bearer 5b5c8e677413277c4bb8b740d522b378], 
  Accept=[application/xml]
}

...

Here is one example of how OAuthRequestFilter can be configured:

Code Block
xml
xml

<bean id="oauthProvider" class="oauth.manager.OAuthManager"/>
<bean id="oauthFiler" class="org.apache.cxf.rs.security.oauth2.filters.OAuthRequestFilter">
  <property name="dataProvider" ref="oauthProvider"/>
</bean>

<bean id="myApp" class="org.myapp.MyApp"/>

<jaxrs:server id="fromThirdPartyToMyApp" address="/thirdparty-to-myapp">
   <jaxrs:serviceBeans>
      <ref bean="myApp"/>
  </jaxrs:serviceBeans>
  <jaxrs:providers>
      <ref bean="oauthFilter"/>
  </jaxrs:providers>
  
</jaxrs:server>

...

When one has Authorization and AccessToken service not collocated with the application endpoints, the following may work better:

Code Block
xml
xml


     <bean id="tvServiceClientFactory" class="org.apache.cxf.jaxrs.client.JAXRSClientFactoryBean">
         <property name="address" value="http://localhost:${http.port}/services/oauth/validate"/>
         <property name="headers">
            <map>
               <entry key="Accept" value="application/xml"/>
            </map>
         </property>
     </bean>
     
     <bean id="tvServiceClient" factory-bean="tvServiceClientFactory" factory-method="createWebClient"/>

     <bean id="tokenValidator" class="org.apache.cxf.rs.security.oauth2.filters.AccessTokenValidatorClient">
         <property name="tokenValidatorClient" ref="tvServiceClient"/>
     </bean>

     <bean id="oauthFiler" class="org.apache.cxf.rs.security.oauth2.filters.OAuthRequestFilter">
         <property name="tokenValidator" ref="tokenValidator"/>
     </bean>

<bean id="myApp" class="org.myapp.MyApp"/>

<jaxrs:server id="fromThirdPartyToMyApp" address="/thirdparty-to-myapp">
   <jaxrs:serviceBeans>
      <ref bean="myApp"/>
  </jaxrs:serviceBeans>
  <jaxrs:providers>
      <ref bean="oauthFilter"/>
  </jaxrs:providers>
</jaxrs:server>

...

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 String authorizationServiceURI;
        private Consumer consumer;

        // inject properties, register the client application...

	public URI getAuthorizationServiceURI(ReservationRequest request,
			                              URI redirectUri,
			                              /* state */String reservationRequestKey) {
	    String scope = OAuthConstants.UPDATE_CALENDAR_SCOPE + request.getHour();
	    return OAuthClientUtils.getAuthorizationURI(authorizationServiceURI, 
	    		                                consumer.getKey(),
	    		                                redirectUri.toString(),
	    		                                reservationRequestKey,
	    		                                scope);
	}
	public ClientAccessToken getAccessToken(AuthorizationCodeGrant codeGrant) {
	    try {
	        return OAuthClientUtils.getAccessToken(accessTokenService, consumer, codeGrant);
	    } catch (OAuthServiceException ex) {
	        return null;
	    }
	}
	
	public String createAuthorizationHeader(ClientAccessToken token) {
		return OAuthClientUtils.createAuthorizationHeader(consumer, token);
	}
}

...

For example, consider a case where a client who already owns an authorized access token and accessing the end user resource gets HTTP 401 error back and the client also owns a refresh token. Here is one possible way to handle it:

Code Block
java
java


import javax.ws.rs.NotAuthorizedException;
import javax.ws.rs.core.HttpHeaders;

import org.apache.cxf.rs.security.oauth2.client.OAuthClientUtils;
import org.apache.cxf.rs.security.oauth2.client.OAuthClientUtils.Consumer;
import org.apache.cxf.rs.security.oauth2.grants.code.AuthorizationCodeGrant;
import org.apache.cxf.rs.security.oauth2.grants.refresh.RefreshTokenGrant;
import org.apache.cxf.rs.security.oauth2.common.ClientAccessToken;



// the pseudo-code for getting the access token
Consumer consumer = ...
AuthorizationCodeGrant codeGrant = ...

ClientAccessToken accessToken = OAuthClientUtils.getAccessToken(codeGrant, consumer);

WebClient endUserResourceClient = WebClient.create(endUserServerAddress);

endUserResourceClient.header(HttpHeaders.AUTHORIZATION, 
                             OAuthClientUtils.createAuthorizationHeader(accessToken));
try {
   return endUserResourceClient.get();
} catch (NotAuthorizedException ex) {
    String refreshToken = accessToken.getRefreshToken();
    if (refreshToken != null) {
        // retry once

        // refresh the token
        accessToken = OAuthClientUtils.getAccessToken(new RefreshTokenGrant(refreshToken), consumer);

        // reset Authorization header
        endUserResourceClient.replaceHeader(HttpHeaders.AUTHORIZATION, 
                             OAuthClientUtils.createAuthorizationHeader(accessToken)); 

        // try to access the end user resource again
        return endUserResourceClient.get();
        
    } else {
        throw ex;
    }

}



...

This section lists all the error properties that can be returned to the client application. CXF OAuth2 services will always report a required 'error' property but will omit the optional error properties by default (for example, in case of access token grant handlers throwing OAuthServiceException initialized with OAuthError which may have the optional properties set).
When reporting the optional error properties is actually needed then setting a 'writeCustomErrors' property to 'true' will help:

Code Block
xml
xml

<bean id="oauthProvider" class="oauth2.manager.OAuthManager"/>

<bean id="accessTokenService" class="org.apache.cxf.rs.security.oauth2.services.AccessTokenService">
    <property name="dataProvider" ref="oauthProvider"/>
    <property name="writeCustomErrors" value="true"/>
</bean>

...

The first problem which needs to be addressed is how to distinguish end users from third-party clients and get both parties authenticated as required.
Perhaps the simplest option is to extend a CXF OAuth2 filter (JAX-RS or servlet one), check Authorization header, if it is OAuth2 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.oauth2.filters.OAuthRequestFilter {
   @Context
   private HttpHeaders headers;

   public Response handleRequest(ClassResourceInfo cri, Message message) {
       String header = headers.getRequestHeaders().getFirst("Authorization");
       if (header.startsWith("Bearer ")) {
           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'
   }
}

...