This post will walk through setting up an OAuth2 provider service for protecting access to REST resources. The code is available in github. You can fork the code and start writing services that will be protected by OAuth access. This is a separate module but builds on services covered in a previous series that includes:
It would be useful but not required to be familiar with some of the technologies covered such as:
* OAuth2 Protocol
* Spring Security
* Spring Integration
* Spring Data
* Jersey/JAX-RS
* Gradle / Groovy
* MongoDB
* Spring Security
* Spring Integration
* Spring Data
* Jersey/JAX-RS
* Gradle / Groovy
* MongoDB
Resource Owner Password Flow
Subsequent posts will deal with the other types of authorization flow, such as using third party providers (Facebook, Google, etc). The intent of this post is a walk through of the Resource Owner Password flow. This is a typical use case if you are the system of record for user credentials and trust client applications. It is simply the exchange of the user's username and password for an access token. This token can then be used on subsequent requests to authorize access to resources. It is also important to support token expiration and by extension token refresh.
Let's test out the code and then I'll walk through the application and explain how it is built
Let's test out the code and then I'll walk through the application and explain how it is built
Building the project
Check out the source code
> git clone git@github.com:iainporter/oauth2-provider.git
> cd oauth2-provider
> ./gradlew clean build integrationTest
> cd oauth2-provider
> ./gradlew clean build integrationTest
Running the Web Application
The application uses MongoDB as the persistence store. Before running the application ensure that mongod is running on port 27017. If you don't have mongo installed get it here
Once mongoDB is installed and running fire up the web application using the following command
> ./gradlew tomcatRun
To try it out you can open a browser window and navigate to http://localhost:8080/oauth2-provider/index.html
Testing with Curl
To create a user:
> curl -v -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Basic MzUzYjMwMmM0NDU3NGY1NjUwNDU2ODdlNTM0ZTdkNmE6Mjg2OTI0Njk3ZTYxNWE2NzJhNjQ2YTQ5MzU0NTY0NmM=" \
-d '{"user":{"emailAddress":"user@example.com"}, "password":"password"}' \
'http://localhost:8080/oauth2-provider/v1.0/users'
-H "Content-Type: application/json" \
-H "Authorization: Basic MzUzYjMwMmM0NDU3NGY1NjUwNDU2ODdlNTM0ZTdkNmE6Mjg2OTI0Njk3ZTYxNWE2NzJhNjQ2YTQ5MzU0NTY0NmM=" \
-d '{"user":{"emailAddress":"user@example.com"}, "password":"password"}' \
'http://localhost:8080/oauth2-provider/v1.0/users'
The result:
{"apiUser":
{"emailAddress":"user@example.com",
"firstName":null,
"lastName":null,
"age":null,
"id":"8a34d009-3558-4c8c-a8da-1ad2b2a393c7",
"name":"user@example.com"},
"oauth2AccessToken":
{"access_token":"7e0e4708-7837-4a7e-9f87-81c6429b02ac",
"token_type":"bearer",
"refresh_token":"d0f248ab-e30f-4a85-860c-bd1e388a39b5",
"expires_in":5183999,
"scope":"read write"
}
}
{"emailAddress":"user@example.com",
"firstName":null,
"lastName":null,
"age":null,
"id":"8a34d009-3558-4c8c-a8da-1ad2b2a393c7",
"name":"user@example.com"},
"oauth2AccessToken":
{"access_token":"7e0e4708-7837-4a7e-9f87-81c6429b02ac",
"token_type":"bearer",
"refresh_token":"d0f248ab-e30f-4a85-860c-bd1e388a39b5",
"expires_in":5183999,
"scope":"read write"
}
}
Requesting an access token:
> curl -v -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Basic MzUzYjMwMmM0NDU3NGY1NjUwNDU2ODdlNTM0ZTdkNmE6Mjg2OTI0Njk3ZTYxNWE2NzJhNjQ2YTQ5MzU0NTY0NmM=" \
'http://localhost:8080/oauth2-provider/oauth/token?grant_type=password&username=user@example.com&password=password'
-H "Content-Type: application/json" \
-H "Authorization: Basic MzUzYjMwMmM0NDU3NGY1NjUwNDU2ODdlNTM0ZTdkNmE6Mjg2OTI0Njk3ZTYxNWE2NzJhNjQ2YTQ5MzU0NTY0NmM=" \
'http://localhost:8080/oauth2-provider/oauth/token?grant_type=password&username=user@example.com&password=password'
The result:
{
"access_token":"a838780e-35ef-4bd5-92c0-07a45aa74948",
"token_type":"bearer",
"refresh_token":"ab06022f-247c-450a-a11e-2ffab116e3dc",
"expires_in":5183999
}
"access_token":"a838780e-35ef-4bd5-92c0-07a45aa74948",
"token_type":"bearer",
"refresh_token":"ab06022f-247c-450a-a11e-2ffab116e3dc",
"expires_in":5183999
}
Refreshing a token:
> curl -v -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Basic MzUzYjMwMmM0NDU3NGY1NjUwNDU2ODdlNTM0ZTdkNmE6Mjg2OTI0Njk3ZTYxNWE2NzJhNjQ2YTQ5MzU0NTY0NmM=" \
'http://localhost:8080/oauth2-provider/oauth/token?grant_type=refresh_token&refresh_token=ab06022f-247c-450a-a11e-2ffab116e3dc'
-H "Content-Type: application/json" \
-H "Authorization: Basic MzUzYjMwMmM0NDU3NGY1NjUwNDU2ODdlNTM0ZTdkNmE6Mjg2OTI0Njk3ZTYxNWE2NzJhNjQ2YTQ5MzU0NTY0NmM=" \
'http://localhost:8080/oauth2-provider/oauth/token?grant_type=refresh_token&refresh_token=ab06022f-247c-450a-a11e-2ffab116e3dc'
The result:
{
"access_token":"4835cd11-8bb7-4b76-b857-55c6e7f36fc4",
"token_type":"bearer",
"refresh_token":"ab06022f-247c-450a-a11e-2ffab116e3dc",
"expires_in":5183999
}
"access_token":"4835cd11-8bb7-4b76-b857-55c6e7f36fc4",
"token_type":"bearer",
"refresh_token":"ab06022f-247c-450a-a11e-2ffab116e3dc",
"expires_in":5183999
}
That's all that is needed to support registration of Users and managing tokens on their behalf. The source code also contains everything needed to support lost password and email verification. See previous posts for how to set it up.
In a typical deployment the authorization server and the resource server(s) would be separate, but for the purposes of this tutorial they are managed within the same application.
Let's delve into the details:
In a typical deployment the authorization server and the resource server(s) would be separate, but for the purposes of this tutorial they are managed within the same application.
Let's delve into the details:
Web Context
There are two servlets.
A Jersey servlet as the default to handle all resource calls:
<servlet-mapping> <servlet-name>jersey-servlet</servlet-name> <url-pattern>/*</url-pattern> </servlet-mapping>A Spring servlet to handle all oauth calls:
<servlet-mapping> <servlet-name>spring</servlet-name> <url-pattern>/oauth/*</url-pattern> </servlet-mapping>Wire in spring security by defining a filter:
<filter> <filter-name>springSecurityFilterChain</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> <init-param> <param-name>contextAttribute</param-name> <param-value>org.springframework.web.servlet.FrameworkServlet.CONTEXT.spring</param-value> </init-param> </filter>and then map it to the root context so all calls are filtered:
<filter-mapping> <filter-name>springSecurityFilterChain</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
Configuring OAuth flows to support
<oauth:authorization-server client-details-service-ref="client-details-service" token-services-ref="tokenServices"> <oauth:refresh-token/> <oauth:password/> </oauth:authorization-server>
In this scenario only password flow and refresh token are being supported. The default token endpoint is /oauth/token.
Protecting the token endpoint
It is a good idea to protect access to token requests to only those client applications that you know about. Using Spring security we can set up basic authentication on calls to the token endpoint:
<http pattern="/oauth/token" create-session="stateless" authentication-manager-ref="clientAuthenticationManager" xmlns="http://www.springframework.org/schema/security"> <anonymous enabled="false"/> <http-basic entry-point-ref="clientAuthenticationEntryPoint"/> <access-denied-handler ref="oauthAccessDeniedHandler"/> </http>Next we configure the authentication manager and client details service
<bean id="clientCredentialsTokenEndpointFilter" class="org.springframework.security.oauth2.provider.client.ClientCredentialsTokenEndpointFilter"> <property name="authenticationManager" ref="clientAuthenticationManager"/> </bean> <authentication-manager id="clientAuthenticationManager" xmlns="http://www.springframework.org/schema/security"> <authentication-provider user-service-ref="client-details-user-service"/> </authentication-manager> <bean id="client-details-user-service" class="org.springframework.security.oauth2.provider.client.ClientDetailsUserDetailsService"> <constructor-arg ref="client-details-service" /> </bean>
Depending on how many clients we expect to access the service will determine what type of client details service we implement. A simple file-based service might be sufficient. If you wanted to go a lot further with client sign up and managing API keys then you would have to involve a persistence tier for the client details service. Configuring with the default Spring file-based service is trivial:
<oauth:client-details-service id="client-details-service"> <oauth:client client-id="353b302c44574f565045687e534e7d6a" secret="286924697e615a672a646a493545646c" authorized-grant-types="password,refresh_token" authorities="ROLE_TEST" access-token-validity="${oauth.token.access.expiresInSeconds}" refresh-token-validity="${oauth.token.refresh.expiresInSeconds}" /> <oauth:client client-id="7b5a38705d7b3562655925406a652e32" secret="655f523128212d6e70634446224c2a48" authorized-grant-types="password,refresh_token" authorities="ROLE_WEB" access-token-validity="${oauth.token.access.expiresInSeconds}" refresh-token-validity="${oauth.token.refresh.expiresInSeconds}" /> <oauth:client client-id="5e572e694e4d61763b567059273a4d3d" secret="316457735c4055642744596b302e2151" authorized-grant-types="password,refresh_token" authorities="ROLE_IOS" access-token-validity="${oauth.token.access.expiresInSeconds}" refresh-token-validity="${oauth.token.refresh.expiresInSeconds}" /> <oauth:client client-id="302a7d556175264c7e5b326827497349" secret="4770414c283a20347c7b553650425773" authorized-grant-types="password,refresh_token" authorities="ROLE_ANDROID" access-token-validity="${oauth.token.access.expiresInSeconds}" refresh-token-validity="${oauth.token.refresh.expiresInSeconds}" /> </oauth:client-details-service>
Accessing the oauth endpoint now requires a basic authentication header with the client id and secret concatenated with a ":" separator and base64 encoded.
e.g. -H "Authorization: Basic MzUzYjMwMmM0NDU3NGY1NjUwNDU2ODdlNTM0ZTdkNmE6Mjg2OTI0Njk3ZTYxNWE2NzJhNjQ2YTQ5MzU0NTY0NmM="
e.g. -H "Authorization: Basic MzUzYjMwMmM0NDU3NGY1NjUwNDU2ODdlNTM0ZTdkNmE6Mjg2OTI0Njk3ZTYxNWE2NzJhNjQ2YTQ5MzU0NTY0NmM="
Configuring User Authentication Services
The Resource Owner Password flow requires an authentication manager for managing users.
<bean id="passwordEncoder" class="org.springframework.security.crypto.password.StandardPasswordEncoder"/> <sec:authentication-manager alias="userAuthenticationManager"> <sec:authentication-provider user-service-ref="userService"> <sec:password-encoder ref="passwordEncoder"/> </sec:authentication-provider> </sec:authentication-manager>
The password encoder is used to encrypt the password on authentication. The user service also uses the same encoder to encrypt passwords before persisting them.
The user service must implement UserDetailsService and retrieve the user by username.
The user service must implement UserDetailsService and retrieve the user by username.
@Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { notNull(username, "Mandatory argument 'username' missing."); User user = userRepository.findByEmailAddress(username.toLowerCase()); if (user == null) { throw new AuthenticationException(); } return user; }
Configuring Token Services
The final piece of configuration is to manage storage and retrieval of access tokens.
<bean id="tokenServices" class="org.springframework.security.oauth2.provider.token.DefaultTokenServices"> <property name="tokenStore" ref="tokenStore"/> <property name="supportRefreshToken" value="true"/> <property name="clientDetailsService" ref="client-details-service"/> </bean>
Spring has a handy in-memory implementation that is useful when testing:
<bean id="tokenStore" class="org.springframework.security.oauth2.provider.token.InMemoryTokenStore"/>
For this tutorial I have chosen MongoDB as the persistence technology, but it would be easy to wire in an alternative such as MySql or Redis It is a matter of implementing org.springframework.security.oauth2.provider.token.TokenStore
<bean id="tokenStore" class="com.porterhead.oauth2.mongodb.OAuth2RepositoryTokenStore"> <constructor-arg ref="OAuth2AccessTokenRepository"/> <constructor-arg ref="OAuth2RefreshTokenRepository"/> </bean>
Protecting access to resources
First we need to configure a spring resource server filter. This will check that there is a valid access token in the request header.
<oauth:resource-server id="resourceServerFilter" token-services-ref="tokenServices"/>
Spring security has some useful classes for fine-grain control of roles and permissions. I prefer to use JSR-250 annotations. Luckily it is easy to wire this in. First we need to enable JSR-250 annotations with the following configuration:
<sec:global-method-security jsr250-annotations="enabled" access-decision-manager-ref="accessDecisionManager"/>
Configure an access manager that uses the Jsr250Voter
<bean id="accessDecisionManager" class="org.springframework.security.access.vote.UnanimousBased"> <property name="decisionVoters"> <list> <bean class="org.springframework.security.access.annotation.Jsr250Voter"/> </list> </property> </bean>
Now we can protect REST resource methods with JSR-250 annotations such as @RolesAllowed
Trying it out
Typically the glue between the OAuth server and the application is a user identifier. When a client gets an access token for a user the next step is to typically load data related to that user. This will usually involve building a url with the userId as part of the path
i.e. /v1.0/users/{id}/someresource
A useful service to provide is a way to get user information based on the access token. The application can identify the user that owns the token and return information on that user to the client.
@Path("/v1.0/me") @Component @Produces({MediaType.APPLICATION_JSON}) @Consumes({MediaType.APPLICATION_JSON}) public class MeResource extends BaseResource { @RolesAllowed({"ROLE_USER"}) @GET public ApiUser getUser(final @Context SecurityContext securityContext) { User requestingUser = loadUserFromSecurityContext(securityContext); if(requestingUser == null) { throw new UserNotFoundException(); } return new ApiUser(requestingUser); } protected User loadUserFromSecurityContext(SecurityContext securityContext) { OAuth2Authentication requestingUser = (OAuth2Authentication) securityContext.getUserPrincipal(); Object principal = requestingUser.getUserAuthentication().getPrincipal(); User user = null; if(principal instanceof User) { user = (User)principal; } else { user = userRepository.findByEmailAddress((String)principal); } return user; } }
To test this out start up the application:
> ./gradlew tomcatRun
Execute the following curl statement, substituting the access token that was returned from the login curl above
> curl -v -X GET \
-H "Content-Type: application/json" \
-H "Authorization: Bearer [your token here]" \
'http://localhost:8080/oauth2-provider/v1.0/me'
-H "Content-Type: application/json" \
-H "Authorization: Bearer [your token here]" \
'http://localhost:8080/oauth2-provider/v1.0/me'
To try out some of the other services included in the code see previous posts:
- Part 1: An introduction to writing REST Services in Java
- Email Verification
- Lost Password
- Moving to Production
- JSR 303 Validation