You are viewing an old version of this page. View the current version.

Compare with Current View Page History

« Previous Version 11 Next »

Unknown macro: {span}

JAX-RS: OAuth

Introduction

CXF 2.5.0 implements OAuth 1.0.

While OAuth 2.0 (which is very close to becoming the final recommendation) is the next major version of OAuth, OAuth 1.0 is being supported by many providers and the CXF OAuth module should make it easy for developers to start writing OAuth applications, be it OAuth 1.0 or OAuth 2.0 once the latter gets implemented.

OAuth offers a complex yet elegant solution toward helping the end users (resource owners) authorize third-party providers to access their resources.

The classical OAuth flow is also called a 3-leg OAuth flow as it involves 3 parties: the end user (resource owner), the third party service (client, consumer) and the resource server which is protected by OAuth filters. Typically a consumer offers a service feature that an end user requests and which requires the former to access one or more resources of this end user which are located at the resource server. For example, the consumer may need to access the end user's photos in order to print them and post to the user or read and possibly update a user's calendar in order to make a booking.

In order to make it happen, the third-party service application/consumer needs to register itself with the OAuth server. This happens out-of-band and after the registration the consumer gets back a consumer key and secret pair. For example, see this page for one approach. The registrations of third-party application does not have to be very involved for simpler applications.

From then on, the typical flows works like this:
1. End User requests the third-party service using a browser.

2. Third-party service requests a temporarily request token from OAuth RequestToken Service; this token will represent a consumer's intention to access whatever end user resources it needs to complete the current user's request.

3. After getting a request token back, the consumer redirects the end user to OAuth Authorization Service and adds the request token to the target URI.

4. Authorization Service will get all the details about the current consumer using a request token, build an HTML form and return it to the end user. The form will ask the user if a given third-party application can be allowed to access some resources on behalf of this user.

5. If the user approves it then Authorization Service will redirect the user back to the callback uri provided by the consumer when requesting a request token, including a generated verifier (authorization key) which 'links' the user's approval with the request token.

6. Now the third-party service requests an access token from OAuth AccessToken Service by providing a request token and its verifier.

7. After getting an access token token, the service finally proceeds with accessing the current user's resources and completes the user's request.

As noted above, the consumer needs to register first with the OAuth server. It's a good practice to provide an application name and so called connect URI which is typically a public URI of this application; the former will be used by OAuth Authorization Service at step 4 above and the latter will be used at step 2 to validate the provided callback URI to make sure it starts from the URI which was actually provided during the registration.

As you can see the flow can be complex yet it is functional. A number of issues may need to be taken care along the way such as managing expired tokens, making sure that the OAuth security layer is functioning properly and is not interfering with the end user itself trying to access its own resources, etc.

CXF JAX-RS gives the best effort to making this process as simple as possible and requiring only a minimum effort on behalf of OAuth server developers.
It also offers the utility code for greatly simplifying the way the third-party application can interact with the OAuth service endpoints.

Now, as far this particular 3-leg flow is concerned, OAuth 2.0 simplifies it by effectively making the steps 3 and 6 (requests for request and access tokens) redundant. Moving to OAuth 2.0 will be straightforward after learning how to build OAuth 1.0 servers with CXF.

Please check the specification and the Wikipedia article as well as other resources available on the WEB for more information you may need to know about OAuth.

Maven dependencies

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

Developing OAuth Servers

OAuth server is the core piece of the complete OAuth-based solution. Typically it contains 3 services for:

  • Initiating the flows by issuing temporarily tokens to consumers
  • Authorizing request tokens by asking the end users to let consumers access some of their resources and returning the
    confirmation back to the consumer
  • Exchanging authorized request tokens for access tokens

CXF offers 3 JAX-RS service implementations that can be used to create functional OAuth servers fast: RequestTokenService, AuthorizationRequestService and AccessTokenService.

All of these 3 services rely on the custom OAuthDataProvider which manages request and access tokens. Writing your own OAuthDataProvider implementations is what is needed to get the OAuth server up and running.

RequestTokenService

The main responsibility of RequestTokenService is to create a temporarily request token and return it back to the consumer. It supports POST and GET requests and return a form payload containing the new request token and its secret.

Here is an example request log:

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

It is an empty POST request which includes an Authorization OAuth header. The value of the header has a consumer key (obtained during the third-party registration), callback URI pointing to where AuthorizationRequestService will return an authorized token and a signature which was calculated using a consumer key and secret pair as described in the specification.

First RequestTokenService validates the signature and then it retrieves a Client instance from OAuthDataProvider using a consumer key.

Before asking OAuthDataProvider to generate a request token, it attempts to validate a callback URI against a Client's application URI.

Finally it delegates to OAuthDataProvider to create a request token, passing to it a populated RequestTokenRegistration bean.

This bean references a Client instance, callback URI and a state. State is something that a consumer may also include during the request token request using a "state" parameter and will be returned back to the consumer alongside the verifier after the request token has been authorized. For example, it may represent a key that a consumer will use to retrieve the state of the request that it was processing when requesting a token. For OAuth 1.0
consumers, the request token itself may represent a good enough key for such purposes, but "state" may need to be used too and will become more useful for OAuth 2.0.

The bean also includes "issuedAt" and "lifetime" values which represent the time a new token is about to be created and a configurable time in milliseconds that this token will 'live' for. OAuthDataProvider will be free to reset those values if needed before actually creating a request token.

Finally, two more properties may be set on this bean instance: list of scopes and uris. List of scopes represents optional permissions that the consumer may need to access the resources and a list of URIs represents an optional list of relative URIs the consumer will want to use. These can be provided by "x_oauth_scope" (just "scope" in OAuth 2.0) and "x_oauth_uri" request parameters, for example,

Authorization=[OAuth ..., 
                     x_oauth_scope="readCalendar updateCalendar", 
                     x_oauth_uri="/calendar"]

These two parameters will be covered later.

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

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

The consumer is now ready to redirect the current end user to AuthorizationRequestService.

AuthorizationRequestService

The main responsibility of AuthorizationRequestService is to present an end user with a form asking the user to allow or deny the consumer accessing some of the user resources.

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

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:

Address: http://localhost:8080/services/social/authorize?oauth_token=f4415e16-56ea-465f-9df1-8bd769253a7d
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], 
...
}

First, AuthorizationRequestService will retrieve RequestToken (which extends the base Token class) from OAuthDataProvider using the value provided by the "oauth_token" query parameter.

Next it uses this token (which also links to Client) to populate an instance of OAuthAuthorizationData bean and returns it. OAuthAuthorizationData contains application name and URI properties, optional list of Permissions and URIs.

Note that if a consumer originally specified a list of scopes using an "x_oauth_scope" parameter then AuthorizationRequestService will ask OAuthDataProvider to translate opaque names such as "readCalendar" into Permissions for a user to see a better description of these scopes. Also a Client representing the third-party consumer may have been allocated some default scopes during the registration in which case those scopes will also be taken into account when populating OAuthAuthorizationData. The same for optional URIs - more on both scopes and URIs below.

Two other important OAuthAuthorizationData properties are "oauthToken" and "authenticityToken", both are important for processing the decision request coming from the authorization form. The former is a request token key which will be used by AuthorizationRequestService to retrieve the RequestToken again and the latter for validating that the current session has not been hijacked - AuthorizationRequestService generates a random key, stores this in Servlet HTTPSession and expects the returned authenticityToken value match it - this is a recommended approach and it also implies that the authenticityToken value is kept is hidden from a user, for example, it's kept in a 'hidden' form field.

The helper "replyTo" property is an absolute URI identifying the AuthorizationRequestService 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 AuthorizationRequestService 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.

AuthorizationRequestService supports producing "text/html" and it simply relies on a registered RequestDispatcherProvider to set the OAuthAuthorizationData bean as HttpServletRequest attribute and redirect 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 AuthorizationRequestService:

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

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.

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, for example:

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=c18c1ac0-2635-414f-aebb-d2f4a4a9ee09], 
User-Agent=[Mozilla/5.0 (X11; Linux x86_64; rv:2.0) Gecko/20100101 Firefox/4.0]}

AuthorizationRequestService will use an authenticityToken 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:

Response-Code: 303
Headers: {
Location=[http://localhost:8080/services/reservations/reserve/complete?oauth_token=c18c1ac0-2635-414f-aebb-d2f4a4a9ee09&oauth_verifier=00bd8fa7-4233-42a2-8957-0a0a22c684ba]
}

which leads to a browser redirecting the user:

Address: http://localhost:8080/services/reservations/reserve/complete?oauth_token=c18c1ac0-2635-414f-aebb-d2f4a4a9ee09&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=c18c1ac0-2635-414f-aebb-d2f4a4a9ee09], 
User-Agent=[Mozilla/5.0 (X11; Linux x86_64; rv:2.0) Gecko/20100101 Firefox/4.0]}

If a user decision was set to "deny" then no verifier will be sent back to the consumer.

Assuming the decision was "allow", the consumer has now got back a request token and the verifier and is ready to exchange it for an access token.

AccessTokenService

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:

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="c18c1ac0-2635-414f-aebb-d2f4a4a9ee09", 
                     oauth_verifier="00bd8fa7-4233-42a2-8957-0a0a22c684ba", 
                     oauth_timestamp="1320760259", 
                     oauth_nonce="16237669362301", 
                     oauth_version="1.0", 
                     oauth_signature="dU%2BhXPNFfFpX2sC74IOxzTjdVrY%3D"]
}

This request is very similar to a temporarily token request. Note that the request token key is also included and this key and the request token secret pair is also used to calculate the signature.

AccessTokenService validates the signature, asks OAuthDataProvider to remove a RequestToken identified by the "oauth_token" and create a new AccessToken based on this RequestToken. The resulting access token key and secret pair is returned back to a consumer:

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

Writing OAuthDataProvider

OAuth Server JAX-RS endpoints

Protecting resources with OAuth filters

Client-side support

OAuth Without a Browser

2-leg OAuth flow

Design considerations

Sharing the same URI path between end users and consumers

What Is Next

Fine tuning the current OAuth 1.0 will be continued and the feedback from the implementers will be welcomed.
OAuth 2.0 is going to become a very major specification in the whole RESTful space and CXF will implement the selected OAuth 2.0 profiles. Among other things, OAuth 2.0 will also rely on SAML in one of the extensions and we'll look into it too. Writing a complete OAuth application will most likely require some SSO solution so some support from CXF is likely to come too.

  • No labels