Table of Contents
1. Intro to the Spring Security Tutorial: 2-Legged OAuth 1.0
This post is a Spring Security OAuth 1.0 2-Legged authentication tutorial. The post will show you how to configure and modify the Spring Security OAuth library in order to support 2-Legged OAuth. This is the second post in a three part series:
- Spring Security Tutorial: 0-Legged OAuth 1.0
- Spring Security Tutorial: 2-Legged OAuth 1.0
- Spring Security Tutorial: 3-Legged OAuth 1.0
Although the Spring documentation has a page regarding 2-Legged OAuth 1.0, this is in fact not a 2-Legged implementation. If you look at our post on 0-Legged OAuth 1.0 you will notice that this is what is actually implemented by the Spring OAuth library. As mentioned on the 0-Legged OAuth post, there are three steps (or legs) in the OAuth 1.0 mechanism and the Spring OAuth library implementation has none of those steps.
Personally I would recommend using 0-Legged OAuth rather than using the 2-Legged version which simply introduces extra trips back and forth between the consumer and the provider. However at times we don’t have the freedom of choice, so for completeness I’ve decided to demonstrate how an actual 2-Legged OAuth process can be configured using the Spring OAuth library. Luckily it involves only a few minor modifications to the existing filters!
Once again I’ve borrowed the image on the right from The OAuth Bible to illustrate the 2-Legged OAuth process. As in the previous post I highly recommend you visit the site as it’s a great source of information regarding OAuth.
Before we get started on the code I wanted to list out the steps involved in the full OAuth process and map each step to its relevant Spring Security OAuth Filter. In Eclipse you can use Ctrl+Shift+T and copy in the name of the filter in order to take a closer look at the code. I’ve added an additional step which is the actual request for the protected resource, although this is not an official step it completes the list of filters involved:
- Get the Request Token: UnauthenticatedRequestTokenProcessingFilter
- Authorize the Request Token: UserAuthorizationProcessingFilter
- Get the Access Token: AccessTokenProcessingFilter
- Access Protected Resource: ProtectedResourceProcessingFilter
In order to implement 2-Legged OAuth we’re going to modify the UnauthenticatedRequestTokenProcessingFilter so that it pre-authenticates ConsumerDetails which implement the ExtraTrustConsumerDetails interface. This will be the only modification on the Provider-side. On the Consumer-side we’re going to modify the OAuthConsumerContextFilter so that it skips the authorization step and instead requests the Access Token directly.
Similarly to the 0-Legged post you can refer to the OAuth 1 Developers Guide on the Spring site for an indepth reference to all the components being used in the article.
The source code for this post is available for download at the bottom of the page. The archive contains a Maven project with two modules: Provider and Consumer. The two modules can be run using “mvn clean install tomcat7:run-war-only”. The README contained in the archive has more information about this. As usual, the source code contains some extra goodies, like the test page which can be used to try different consumerKeys and consumerSecrets!
2. Configuring the 2-Legged OAuth 1.0 Provider
The Provider is the web application which contains the REST resource protected by OAuth. There are a number of similarities between this post and the 0-Legged OAuth post. To make the post self contained I’ll be adding all the necessary code, however I might occasionally refer to the previous post when it comes to the explanation of the code.
This section is broken down into 5 parts:
- Configuring the Web.xml
- Implementing the Protected REST Resource
- Configuring the Spring Security Application Context
- Implementing the OAuth ConsumerDetailsService
- Implementing the Pre-Authenticated Request Token Filter
2.1 Provider: Configuring the Web.xml
Below you will find the web.xml needed in order to configure the Spring Application Context and the Jersey REST resource. This is the same as the 0-Legged OAuth post.
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" version="3.0" metadata-complete="true"> <!-- Spring Config File --> <context-param> <param-name>contextConfigLocation</param-name> <param-value> classpath:security-applicationContext.xml </param-value> </context-param> <!-- Spring Listeners --> <listener> <listener-class>org.springframework.web.context.request.RequestContextListener</listener-class> </listener> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <!-- Security 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.context.WebApplicationContext.ROOT</param-value> </init-param> </filter> <filter-mapping> <filter-name>springSecurityFilterChain</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <!--Jersey Servlet --> <servlet> <servlet-name>RestService</servlet-name> <servlet-class>com.sun.jersey.spi.spring.container.servlet.SpringServlet</servlet-class> <init-param> <param-name>com.sun.jersey.config.property.packages</param-name> <param-value>org.codehustler</param-value> </init-param> <init-param> <param-name>com.sun.jersey.config.feature.DisableWADL</param-name> <param-value>true</param-value> </init-param> <init-param> <param-name>com.sun.jersey.api.json.POJOMappingFeature</param-name> <param-value>true</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>RestService</servlet-name> <url-pattern>/api/*</url-pattern> </servlet-mapping> </web-app>
Let’s move onto the REST resource.
2.2 Provider: Implementing the Protected REST Resource
This is identical to the 0-Legged OAuth post.
package org.codehustler.resource; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; import org.springframework.security.access.prepost.PreAuthorize; import com.sun.jersey.spi.resource.Singleton; @Singleton @Path( "/protected") @PreAuthorize("hasRole('ROLE_OAUTH')") @Produces( MediaType.APPLICATION_JSON ) public class ProtectedResource { public ProtectedResource() {} @GET public String getOAuthProtectedData() { // For demo purposes only, don't build your JSON responses like this! =) return "{\"protected\":\"This is some OAuth protected data coming from the Provider!\"}"; } }
Now let’s configure the Spring application context.
2.3 Provider: Configuring the Spring Security OAuth Application Context
This part contains the core of the 2-Legged OAuth 1.0 Provider configuration. A good portion of the application context below is the same as the 0-Legged OAuth application context. Please refer to that post for the details as well as more information about the different Nonce services offered with the OAuth library. The list below explains the configuration elements which are not present in the previous post:
- provider the provider is used to configure the OAuth 1.0 provider mechanism. Below I’ve broken down the attributes relevant to this article.
- consumer-details-service-ref the reference to the bean that defines the consumer details service. This is where we find the consumer in our Database and provide OAuth with the CustomerDetails.
- auth-handler-ref the reference to the bean that defines the authentication handler. Here we supply the DefaultAuthenticationHandler.
- token-services-ref the reference to the bean that defines the token services. The token-services is defined in it’s own element.
- token-id-param this overrides the default token parameter used when going through the OAuth process. The default value is requestToken but I’ve changed this to be oauth_token which is aligned with the OAuth specification.
- request-token-url this is the URL which is used to fetch the request token. This URL will be mapped to the UnauthenticatedRequestTokenProcessingFilter.
- access-token-url this is the URL which is used to fetch the access token. This URL will be mapped to the AccessTokenProcessingFilter.
- require10a whether the OAuth 1.0a specification should be used (this requires a callback and verifier to be used which in this case will not be necessary).
- oauthRequestTokenFilter this overrides the default UnauthenticatedRequestTokenProcessingFilter with our own custom PreauthenticatedRequestTokenProcessingFilter which returns a Request Token which is already authorized (i.e. the User does not need to manually authorize the Request Token in order for the Consumer to request an Access Token).
Let’s move onto the code:
<?xml version="1.0" encoding="UTF-8" ?> <b:beans xmlns:b="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.springframework.org/schema/security" xmlns:context="http://www.springframework.org/schema/context" xmlns:oauth="http://www.springframework.org/schema/security/oauth" xsi:schemaLocation="http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.1.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd http://www.springframework.org/schema/security/oauth http://www.springframework.org/schema/security/spring-security-oauth.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd"> <!-- Spring Annotation Driven Components --> <context:annotation-config /> <!-- Global Security Settings --> <global-method-security pre-post-annotations="enabled" /> <!-- OAuth 1.0 2-Legged Security --> <http pattern="/api/**" use-expressions="true" create-session="never" entry-point-ref="oauthProcessingFilterEntryPoint"> <intercept-url pattern="/**" requires-channel="https" access="hasRole('ROLE_OAUTH')" /> </http> <authentication-manager alias="authenticationManager" /> <b:bean id="oauthProcessingFilterEntryPoint" class="org.codehustler.oauth.OAuthProcessingFilterEntryPointImpl" /> <b:bean id="oAuthAuthenticationHandler" class="org.springframework.security.oauth.provider.DefaultAuthenticationHandler" /> <b:bean id="consumerDetailsService" class="org.codehustler.oauth.OAuthConsumerDetailsService"/> <oauth:provider consumer-details-service-ref="consumerDetailsService" token-id-param="oauth_token" token-services-ref="tokenServices" request-token-url="/api/request_token" access-token-url="/api/access_token" auth-handler-ref="oAuthAuthenticationHandler" require10a="false" /> <oauth:token-services id="tokenServices" /> <b:bean id="oauthRequestTokenFilter" class="org.codehustler.oauth.PreauthenticatedRequestTokenProcessingFilter"> <b:property name="filterProcessesUrl" value="/api/request_token"/> <b:property name="require10a" value="false"/> </b:bean> </b:beans>
The provider above only specifies the request-token-url and the access-token-url, this is because will not be needing the URL to authenticate the token as the token will be pre-authenticated.
2.4 Provider: Implementing the OAuth ConsumerDetailsService
Yet again, this is the same as the 0-Legged OAuth post.
package org.codehustler.oauth; import java.util.ArrayList; import java.util.List; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.oauth.common.OAuthException; import org.springframework.security.oauth.provider.ConsumerDetails; import org.springframework.security.oauth.provider.ConsumerDetailsService; public class OAuthConsumerDetailsService implements ConsumerDetailsService { String consumerName = "John"; String consumerKey = "3a4393c3da1a4e316ee66c0cc61c71"; String consumerSecret = "fe1372c074185b19c309964812bb8f3f2256ba514aea8a318"; public OAuthConsumerDetailsService() {} @Override public ConsumerDetails loadConsumerByConsumerKey( String consumerKey ) throws OAuthException { if( consumerKey == null ) throw new OAuthException("No credentials found for the consumer key [" + consumerKey + "]"); if( !consumerKey.equals( this.consumerKey ) ) throw new OAuthException("No credentials found for the consumer key [" + consumerKey + "]"); List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>(); authorities.add( new SimpleGrantedAuthority("ROLE_OAUTH") ); return new OAuthConsumerDetails( consumerName, consumerKey, consumerSecret, authorities ); } }
Just as in the 0-Legged OAuth post, the OAuthConsumerDetails returned in the code above implements the ExtraTrustConsumerDetails rather than the ConsumerDetails. We’ll be using this in the following section to determine whether the Request Token should be pre-authenticated or not.
package org.codehustler.oauth; import java.util.List; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.oauth.common.signature.SharedConsumerSecretImpl; import org.springframework.security.oauth.common.signature.SignatureSecret; import org.springframework.security.oauth.provider.ConsumerDetails; import org.springframework.security.oauth.provider.ExtraTrustConsumerDetails; public class OAuthConsumerDetails implements ExtraTrustConsumerDetails { private static final long serialVersionUID = 1L; private final String consumerName; private final String consumerKey; private final SignatureSecret signatureSecret; private final List<GrantedAuthority> authorities; public OAuthConsumerDetails( String consumerName, String consumerKey, String signatureSecret, List<GrantedAuthority> authorities ) { this.consumerName = consumerName; this.consumerKey = consumerKey; this.signatureSecret = new SharedConsumerSecretImpl(signatureSecret); this.authorities = authorities; } public String getConsumerName() { return consumerName; } public String getConsumerKey() { return consumerKey; } public SignatureSecret getSignatureSecret() { return signatureSecret; } public List<GrantedAuthority> getAuthorities() { return authorities; } public boolean isRequiredToObtainAuthenticatedToken() { return false; } }
Now we can customize the UnauthenticatedRequestTokenProcessingFilter to allow pre-authenticated Request Tokens.
2.5 Provider: Implementing the Pre-Authenticated Request Token Filter
In this section we’re going to modify the original UnauthenticatedRequestTokenProcessingFilter with our own custom PreauthenticatedRequestTokenProcessingFilter. Our filter is going to check whether the ConsumerDetails implement the ExtraTrustConsumerDetails interface and doesn’t require the token to be explicitly authenticated by the end user. The custom filter can actually be used with the regular 3-Legged OAuth and based on whether the user is required to authenticate the token the additional step will be required.
package org.codehustler.oauth; import java.io.IOException; import javax.servlet.FilterChain; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth.common.OAuthCodec; import org.springframework.security.oauth.common.OAuthConsumerParameter; import org.springframework.security.oauth.common.OAuthProviderParameter; import org.springframework.security.oauth.provider.ConsumerAuthentication; import org.springframework.security.oauth.provider.ExtraTrustConsumerDetails; import org.springframework.security.oauth.provider.filter.AccessTokenProcessingFilter; import org.springframework.security.oauth.provider.filter.ProtectedResourceProcessingFilter; import org.springframework.security.oauth.provider.filter.UnauthenticatedRequestTokenProcessingFilter; import org.springframework.security.oauth.provider.filter.UserAuthorizationProcessingFilter; import org.springframework.security.oauth.provider.token.OAuthProviderToken; import org.springframework.security.oauth.provider.verifier.OAuthVerifierServices; public class PreauthenticatedRequestTokenProcessingFilter extends UnauthenticatedRequestTokenProcessingFilter { private OAuthVerifierServices verifierServices; protected void onValidSignature( HttpServletRequest request, HttpServletResponse response, FilterChain chain ) throws IOException { //signature is verified; create the token, send the response. ConsumerAuthentication authentication = (ConsumerAuthentication) SecurityContextHolder.getContext().getAuthentication(); OAuthProviderToken authToken = createOAuthToken(authentication); if (!authToken.getConsumerKey().equals(authentication.getConsumerDetails().getConsumerKey())){ throw new IllegalStateException("The consumer key associated with the created auth token is not valid for the authenticated consumer."); } /////////////////////////////////////////////////////////////////// // Pre-authorize the Request Token for 2-Legged OAuth. // This will only be done if the Consumer has ExtraTrust and is not required // to obtain an authenticated token (see isRequiredToObtainAuthenticatedToken()) /////////////////////////////////////////////////////////////////// if( (authentication.getConsumerDetails() instanceof ExtraTrustConsumerDetails) && !((ExtraTrustConsumerDetails)authentication.getConsumerDetails()).isRequiredToObtainAuthenticatedToken() ) { String verifier = getVerifierServices().createVerifier(); getTokenServices().authorizeRequestToken( authToken.getValue(), verifier, authentication ); } //////////////////////////////////////////////////////////// String tokenValue = authToken.getValue(); String callback = authentication.getOAuthParameters().get(OAuthConsumerParameter.oauth_callback.toString()); StringBuilder responseValue = new StringBuilder(OAuthProviderParameter.oauth_token.toString()) .append('=') .append(OAuthCodec.oauthEncode(tokenValue)) .append('&') .append(OAuthProviderParameter.oauth_token_secret.toString()) .append('=') .append(OAuthCodec.oauthEncode(authToken.getSecret())); if( callback != null ) { responseValue.append('&') .append(OAuthProviderParameter.oauth_callback_confirmed.toString()) .append("=true"); } response.setContentType(getResponseContentType()); response.getWriter().print(responseValue.toString()); response.flushBuffer(); } public OAuthVerifierServices getVerifierServices() { return verifierServices; } @Autowired public void setVerifierServices( OAuthVerifierServices verifierServices ) { this.verifierServices = verifierServices; } }
This concludes the Provider section of the post. If you wish to setup custom error handling you can look at the previous 0-Legged OAuth post or download the source code associated to this post.
3. Configuring the 2-Legged OAuth 1.0 Consumer
The Consumer is the web application which uses the REST resource protected by OAuth. The Spring Security OAuth library comes with the classes necessary to configure / implement the Consumer side of the OAuth process. However, out of the box their OAuth Consumer supports only the 3-Legged process so we will be modifying it in order to fit the 2-Legged process. Note that just like our modification to the Provider side, the changes required here are minimal and the majority of the modified class will be the same as the original.
Note that the source code for the post additionally explains how to configure a custom error handler on the consumer side which can be used to convert the error responses to JSON.
This section is broken down into 4 parts:
- Configuring the Web.xml
- Configuring the Spring Security Application Context
- Implementing the Proxy REST Resource
- Implementing the Custom Consumer Context Filter
3.1 Consumer: Configuring the Web.xml
Let’s start with the web.xml. Unlike the 0-Legged OAuth post, we will be using the Spring Security OAuth consumer classes so we need to initialise the DelegatingFilterProxy. This is required as the Consumer classes depend on the Security Filters being in place.
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" version="3.0" metadata-complete="true"> <!-- Spring Config File --> <context-param> <param-name>contextConfigLocation</param-name> <param-value> classpath:security-applicationContext.xml </param-value> </context-param> <!-- Spring Listeners --> <listener> <listener-class>org.springframework.web.context.request.RequestContextListener</listener-class> </listener> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <!-- Security 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.context.WebApplicationContext.ROOT</param-value> </init-param> </filter> <filter-mapping> <filter-name>springSecurityFilterChain</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <!--Jersey Servlet --> <servlet> <servlet-name>RestService</servlet-name> <servlet-class>com.sun.jersey.spi.spring.container.servlet.SpringServlet</servlet-class> <init-param> <param-name>com.sun.jersey.config.property.packages</param-name> <param-value>org.codehustler.resource</param-value> </init-param> <init-param> <param-name>com.sun.jersey.config.feature.DisableWADL</param-name> <param-value>true</param-value> </init-param> <init-param> <param-name>com.sun.jersey.api.json.POJOMappingFeature</param-name> <param-value>true</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>RestService</servlet-name> <url-pattern>/api/*</url-pattern> </servlet-mapping> </web-app>
3.2 Consumer: Configuring the Spring Security Application Context
This part contains the core of the 2-Legged OAuth 1.0 Consumer configuration. We are going to use the simplest configuration possible but if you wish to read about all the different options check out the OAuth 1 Developers Guide. Let’s start with the elements being used in the application context:
- http this is the container element for the HTTP security configuration. As you’ll notice we’re allowing access to anyone and we’re using a custom AuthenticationEntryPoint which does nothing. The reason we’re doing this is that the OAuth Consumer package has a dependency on the Spring Security filters. Without them we would get a number of exceptions when booting the Consumer webapp.
- authenticationEntryPoint this is an empty implementation of the AuthenticationEntryPoint.
- authentication-manager registers the AuthenticationManager instance and allows its list of AuthenticationProviders to be defined. This is required in order for the Spring Security to startup correctly.
- oauth:consumer this element configures the OAuth 1.0 consumer mechanism (i.e. the appropriate consumer filters):
- resource-details-service-ref the reference to the resource details service. This is explained below in more detail.
- requireAuthenticated whether the user needs to be authenticated (i.e. there is an Authentication object mapped to the current user) in order to use the resources. In this case we want the simplest configuration so we’ve set this to false.
- oauth:url these are the URLs against which the consumer filters will be applied. In our example we have a single URL but you can configure multiple different URLs.
- pattern the URL matching pattern.
- resources the comma-separated list of the ids of the protected resources that the URL requires access to.
- oauth:resource-details-service this element is used to configure an in-memory implementation of the resource details. Each resource is individually configured using the attributes below.
- oauth:resource this element configure a specific protected resource:
- id unique ID used to identify the resource. This is the ID used in the oauth:url above.
- key this is the consumer key.
- secret this is the consumer secret.
- request-token-url the URL used to retrieve the Request Token (Step 1).
- user-authorization-url the URL used to authorize the Request Token. In our 2-Legged implementation this step is not actually performed, but in order to correctly fit the XSD we need to add this attribute.
- access-token-url the URL used to retrieve the Access Token (Step 2).
- use10a whether we’re using OAuth 1.0a.
- oauthConsumerContextFilter this is used to extend the default OAuthConsumerContextFilter and override the doFilter() method with our own custom implementation which removes the Request Token authentication step of the OAuth process.
- oauthRestTemplate this extends the Spring RestTemplate in order to support REST calls made to APIs which are protected using OAuth.
Now let’s take a look at the actual application context:
<?xml version="1.0" encoding="UTF-8" ?> <b:beans xmlns:b="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.springframework.org/schema/security" xmlns:context="http://www.springframework.org/schema/context" xmlns:oauth="http://www.springframework.org/schema/security/oauth" xsi:schemaLocation="http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.1.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd http://www.springframework.org/schema/security/oauth http://www.springframework.org/schema/security/spring-security-oauth.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd"> <!-- Spring Annotation Driven Components --> <context:annotation-config /> <context:component-scan base-package="org.codehustler" /> <!-- Client Security Configuration --> <http use-expressions="true" entry-point-ref="authenticationEntryPoint"> <intercept-url pattern="/**" access="permitAll()" /> </http> <b:bean id="authenticationEntryPoint" class="org.codehustler.oauth.AuthenticationEntryPointImpl" /> <authentication-manager alias="authenticationManager" /> <!-- OAuth 1.0 2-Legged Client --> <oauth:consumer resource-details-service-ref="resourceDetails" requireAuthenticated="false"> <oauth:url pattern="/api/**" resources="protectedResource"/> </oauth:consumer> <oauth:resource-details-service id="resourceDetails"> <oauth:resource id="protectedResource" key="3a4393c3da1a4e316ee66c0cc61c71" secret="fe1372c074185b19c309964812bb8f3f2256ba514aea8a318f05f9d703d524b8" request-token-url="https://localhost:8443/api/request_token" user-authorization-url="https://localhost:8443/api/authorize_token" access-token-url="https://localhost:8443/api/access_token" use10a="false"/> </oauth:resource-details-service> <b:bean id="oauthConsumerContextFilter" class="org.codehustler.oauth.OAuthConsumerContextFilterImpl" /> <b:bean id="oauthRestTemplate" class="org.springframework.security.oauth.consumer.client.OAuthRestTemplate"> <b:constructor-arg ref="protectedResource" /> </b:bean> </b:beans>
3.3 Consumer: Implementing the Proxy REST Resource
This is the REST resource on the Consumer side which will make the request to the protected resource on the Provider. This REST endpoint will make use of the OAuthRestTemplate in order to make OAuth authenticated requests to the Provider. The initSSL() method ensures we can make a request to the Provider server even though we’re using a test SSL certificate.
package org.codehustler.resource; import java.net.URI; import java.security.cert.X509Certificate; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSession; import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; import javax.ws.rs.Consumes; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.oauth.consumer.client.OAuthRestTemplate; import org.springframework.stereotype.Component; import com.sun.jersey.spi.resource.Singleton; @Singleton @Component @Path( "/proxy") @Consumes( MediaType.APPLICATION_JSON ) @Produces( MediaType.APPLICATION_JSON ) public class ProxyResource { private String endpointUrl = "https://localhost:8443/api/protected"; // RestTemplate specific for OAuth authenticated requests @Autowired private OAuthRestTemplate oauthRestTemplate; public ProxyResource() { initSSL(); } @GET public String fetchOAuthProtectedData() { try { return oauthRestTemplate.getForObject( URI.create( endpointUrl ), String.class ); } catch( Exception ex ) { return ex.toString(); } } private void initSSL() { try { SSLContext sc = SSLContext.getInstance("SSL"); HostnameVerifier hv = new HostnameVerifier() { public boolean verify( String urlHostName, SSLSession session ) { return true; } }; TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager() { public X509Certificate[] getAcceptedIssuers() { return null; } public void checkClientTrusted( X509Certificate[] certs, String authType ) {} public void checkServerTrusted( X509Certificate[] certs, String authType ) {} }}; sc.init( null, trustAllCerts, new java.security.SecureRandom() ); SSLSocketFactory sslSocketFactory = sc.getSocketFactory(); HttpsURLConnection.setDefaultSSLSocketFactory(sslSocketFactory); HttpsURLConnection.setDefaultHostnameVerifier(hv); } catch ( Exception e ) { // Do some handling } } }
As you can see above, the OAuthRestTemplate abstracts away all the complexity involved in making an OAuth authenticated request.
3.4 Consumer: Implementing the Custom Consumer Context Filter
This is the last piece of the puzzle on the Consumer side. In order to perform a 2-Legged request we need to modify the OAuthConsumerContextFilter and make sure it skips the Request Token Authentication step. The majority of the class below is the same as the original, all we has to do was comment out the section related to the authentication of the Request Token.
package org.codehustler.oauth; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; import java.util.TreeMap; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.security.oauth.common.OAuthProviderParameter; import org.springframework.security.oauth.consumer.OAuthConsumerToken; import org.springframework.security.oauth.consumer.OAuthRequestFailedException; import org.springframework.security.oauth.consumer.OAuthSecurityContextHolder; import org.springframework.security.oauth.consumer.OAuthSecurityContextImpl; import org.springframework.security.oauth.consumer.ProtectedResourceDetails; import org.springframework.security.oauth.consumer.filter.OAuthConsumerContextFilter; public class OAuthConsumerContextFilterImpl extends OAuthConsumerContextFilter { private final static Logger LOG = LoggerFactory.getLogger( OAuthConsumerContextFilterImpl.class ); public void doFilter( ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain ) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) servletRequest; HttpServletResponse response = (HttpServletResponse) servletResponse; OAuthSecurityContextImpl context = new OAuthSecurityContextImpl(); context.setDetails(request); Map<String, OAuthConsumerToken> rememberedTokens = getRememberMeServices().loadRememberedTokens(request, response); Map<String, OAuthConsumerToken> accessTokens = new TreeMap<String, OAuthConsumerToken>(); Map<String, OAuthConsumerToken> requestTokens = new TreeMap<String, OAuthConsumerToken>(); if (rememberedTokens != null) { for (Map.Entry<String, OAuthConsumerToken> tokenEntry : rememberedTokens.entrySet()) { OAuthConsumerToken token = tokenEntry.getValue(); if (token != null) { if (token.isAccessToken()) { accessTokens.put(tokenEntry.getKey(), token); } else { requestTokens.put(tokenEntry.getKey(), token); } } } } context.setAccessTokens(accessTokens); OAuthSecurityContextHolder.setContext(context); if (LOG.isDebugEnabled()) { LOG.debug("Storing access tokens in request attribute '" + getAccessTokensRequestAttribute() + "'."); } try { try { request.setAttribute(getAccessTokensRequestAttribute(), new ArrayList<OAuthConsumerToken>(accessTokens.values())); chain.doFilter(request, response); } catch (Exception e) { try { ProtectedResourceDetails resourceThatNeedsAuthorization = checkForResourceThatNeedsAuthorization(e); String neededResourceId = resourceThatNeedsAuthorization.getId(); while (!accessTokens.containsKey(neededResourceId)) { OAuthConsumerToken token = requestTokens.remove(neededResourceId); if (token == null) { token = getTokenServices().getToken(neededResourceId); } String verifier = request.getParameter(OAuthProviderParameter.oauth_verifier.toString()); // if the token is null OR // if there is NO access token and (we're not using 1.0a or the verifier is not null) if (token == null || (!token.isAccessToken() && (!resourceThatNeedsAuthorization.isUse10a() || verifier == null))) { //no token associated with the resource, start the oauth flow. //if there's a request token, but no verifier, we'll assume that a previous oauth request failed and we need to get a new request token. if (LOG.isDebugEnabled()) { LOG.debug("Obtaining request token for resource: " + neededResourceId); } //obtain authorization. String callbackURL = response.encodeRedirectURL(getCallbackURL(request)); token = getConsumerSupport().getUnauthorizedRequestToken(neededResourceId, callbackURL); if (LOG.isDebugEnabled()) { LOG.debug("Request token obtained for resource " + neededResourceId + ": " + token); } //okay, we've got a request token, now we need to authorize it. requestTokens.put(neededResourceId, token); getTokenServices().storeToken(neededResourceId, token); //////////////////////////////////////////////////////////// // EDIT: Removed the Authentication Step for 2-Legged OAuth. // If we implemented a custom version of the ProtectedResourceDetails we could // specify whether it is required to obtain an authenticated token. // This would be similar to the ExtraTrustConsumerDetails. // We could then use ProtectedResourceDetails.isRequiredToObtainAuthenticatedToken() // and if true is returned, we would execute the step below // (re-introducing the authentication step) //////////////////////////////////////////////////////////// // String redirect = getUserAuthorizationRedirectURL(resourceThatNeedsAuthorization, token, callbackURL); // // if (LOG.isDebugEnabled()) { // LOG.debug("Redirecting request to " + redirect + " for user authorization of the request token for resource " + neededResourceId + "."); // } // // request.setAttribute("org.springframework.security.oauth.consumer.AccessTokenRequiredException", e); // this.redirectStrategy.sendRedirect(request, response, redirect); // return; // } else if (!token.isAccessToken()) { //////////////////////////////////////////////////////////// } if (!token.isAccessToken()) { //we have a presumably authorized request token, let's try to get an access token with it. if (LOG.isDebugEnabled()) { LOG.debug("Obtaining access token for resource: " + neededResourceId); } //authorize the request token and store it. try { token = getConsumerSupport().getAccessToken(token, verifier); } finally { getTokenServices().removeToken(neededResourceId); } if (LOG.isDebugEnabled()) { LOG.debug("Access token " + token + " obtained for resource " + neededResourceId + ". Now storing and using."); } getTokenServices().storeToken(neededResourceId, token); } accessTokens.put(neededResourceId, token); try { //try again if (!response.isCommitted()) { request.setAttribute(getAccessTokensRequestAttribute(), new ArrayList<OAuthConsumerToken>(accessTokens.values())); chain.doFilter(request, response); } else { //dang. what do we do now? throw new IllegalStateException("Unable to reprocess filter chain with needed OAuth2 resources because the response is already committed."); } } catch (Exception e1) { resourceThatNeedsAuthorization = checkForResourceThatNeedsAuthorization(e1); neededResourceId = resourceThatNeedsAuthorization.getId(); } } } catch (OAuthRequestFailedException eo) { fail(request, response, eo); } catch (Exception ex) { Throwable[] causeChain = getThrowableAnalyzer().determineCauseChain(ex); OAuthRequestFailedException rfe = (OAuthRequestFailedException) getThrowableAnalyzer().getFirstThrowableOfType(OAuthRequestFailedException.class, causeChain); if (rfe != null) { fail(request, response, rfe); } else { // Rethrow ServletExceptions and RuntimeExceptions as-is if (ex instanceof ServletException) { throw (ServletException) ex; } else if (ex instanceof RuntimeException) { throw (RuntimeException) ex; } // Wrap other Exceptions. These are not expected to happen throw new RuntimeException(ex); } } } } finally { OAuthSecurityContextHolder.setContext(null); HashMap<String, OAuthConsumerToken> tokensToRemember = new HashMap<String, OAuthConsumerToken>(); tokensToRemember.putAll(requestTokens); tokensToRemember.putAll(accessTokens); getRememberMeServices().rememberTokens(tokensToRemember, request, response); } } }
With this the whole Consumer side is configured and ready to be used. All you need to do now is access the Consumer REST endpoint and a 2-Legged OAuth authentication process will take place prior to the protected resource being accessed.
4. Conclusion
Luckily modifying the Spring Security OAuth library to fit the 2-Legged OAuth process is pretty simple. If you have any questions please leave a comment and I’ll try to answer as soon as possible.
The sample code used in this post (and all other posts with code samples) is available by subscribing below. The downloaded source code usually comes with a few goodies which aren’t in the post!
The source code is all configured using Maven, so you can be up and running by simply typing “mvn clean install tomcat7:run-war-only”. The README contained in the archive explains all you need to know.
- Spring Security Tutorial: Form Login Java Configuration - September 27, 2014
- Spring Security Tutorial: 3-Legged OAuth 1.0 - June 9, 2014
- Spring Security Tutorial: 2-Legged OAuth 1.0 - June 3, 2014