I needed to create a web app using Spring MVC and secure it using OAuth2 with Google as a provider for authentication. Saket's Blog (posted back in September 2014) provided a good guide. But I needed something slightly different. I needed one that uses Maven (not Gradle) and minus Spring Boot. So, I thought it would just be a simple thing to do. But as I found out, it was not so simple, and I'm writing some details here to help others in using OAuth2 to secure their Spring MVC web apps.
Here's what I configured to make my web application use OAuth2 with Google as the provider.
- Enable Spring Security with
@EnableWebSecurity
. - Add an
OAuth2ClientAuthenticationProcessingFilter
bean to the security filter chain just before the filter security interceptor. This authentication processing filter is configured to know where the authorization code resource can be found. This makes it possible for it to throw an exception that redirects the user to the authorization server for authentication and authorization. - Set an authentication entry point (specifically a
LoginUrlAuthenticationEntryPoint
) that redirects to the same URL as the one being detected by theOAuth2ClientAuthenticationProcessingFilter
. Say, we choose the path “/oauth2/callback”. This path should be the one used by both authentication entry point and authentication processing filter. - Add
@EnableOAuth2Client
to create anOAuth2ClientContextFilter
bean and make anOAuth2ClientContext
available in request scope. To make request scope possible in the security filter chain, add aRequestContextListener
orRequestContextFilter
. - Add the
OAuth2ClientContextFilter
bean to the security filter chain just after the exception translation filter. This filter handles the exception that redirects the user (thrown by the authentication process filter). It handles this exception by sending a redirect.
Authorization Code Resource
The authentication processing filter needs to know where to redirect the user for authentication. So, a bean is configured and injected into the authentication process filter.
@Configuration @EnableWebSecurity @EnableOAuth2Client @PropertySource("classpath:google-oauth2.properties") public class ... extends WebSecurityConfigurerAdapter { ... @Value("${oauth2.clientId}") private String clientId; @Value("${oauth2.clientSecret}") private String clientSecret; @Value("${oauth2.userAuthorizationUri}") private String userAuthorizationUri; @Value("${oauth2.accessTokenUri}") private String accessTokenUri; @Value("${oauth2.tokenName}") private String tokenName; @Value("${oauth2.scope}") private String scope; @Value("${oauth2.userInfoUri}") private String userInfoUri; @Value("${oauth2.filterCallbackPath}") private String oauth2FilterCallbackPath; @Bean @Description("Authorization code resource") public OAuth2ProtectedResourceDetails authorizationCodeResource() { AuthorizationCodeResourceDetails details = new AuthorizationCodeResourceDetails(); ... details.setClientId(clientId); details.setClientSecret(clientSecret); details.setUserAuthorizationUri(userAuthorizationUri); details.setAccessTokenUri(accessTokenUri); details.setTokenName(tokenName); String commaSeparatedScopes = scope; details.setScope(parseScopes(commaSeparatedScopes)); details.setAuthenticationScheme(AuthenticationScheme.query); details.setClientAuthenticationScheme(AuthenticationScheme.form); return details; } private List<String> parseScopes(String commaSeparatedScopes) {...} ... @Bean @Description("Enables ${...} expressions in the @Value annotations" + " on fields of this configuration. Not needed if one is" + " already available.") public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() { return new PropertySourcesPlaceholderConfigurer(); } }
Note that the authorization code resource details are externalized. These details include the URI for authentication, the URI to exchange an authorization code with an access token, client ID, and client secret.
Authentication Processing Filter
With an authorization code resource bean configured, we configure an authentication processing filter bean that will redirect to the authorization code resource when the incoming request is not yet authenticated. Note that the authentication processing filter is injected with an OAuth2RestTemplate
that points to the authorization code resource.
@Configuration @EnableWebSecurity @EnableOAuth2Client @PropertySource("classpath:google-oauth2.properties") public class ... extends WebSecurityConfigurerAdapter { @Autowired private OAuth2ClientContext oauth2ClientContext; ... @Bean @Description("Authorization code resource") public OAuth2ProtectedResourceDetails authorizationCodeResource() { AuthorizationCodeResourceDetails details = new AuthorizationCodeResourceDetails(); ... return details; } @Bean @Description("Filter that checks for authorization code, " + "and if there's none, acquires it from authorization server") public OAuth2ClientAuthenticationProcessingFilter oauth2ClientAuthenticationProcessingFilter() { // Used to obtain access token from authorization server (AS) OAuth2RestOperations restTemplate = new OAuth2RestTemplate( authorizationCodeResource(), oauth2ClientContext); OAuth2ClientAuthenticationProcessingFilter filter = new OAuth2ClientAuthenticationProcessingFilter(oauth2FilterCallbackPath); filter.setRestTemplate(restTemplate); // Set a service that validates an OAuth2 access token // We can use either Google API's UserInfo or TokenInfo // For this, we chose to use UserInfo service filter.setTokenServices(googleUserInfoTokenServices()); return filter; } @Bean @Description("Google API UserInfo resource server") public GoogleUserInfoTokenServices googleUserInfoTokenServices() { GoogleUserInfoTokenServices userInfoTokenServices = new GoogleUserInfoTokenServices(userInfoUri, clientId); return userInfoTokenServices; } ... }
Note that the access token is further checked by using it to access a secured resource (provided by a resource server). In this case, the Google API to retrieve user information like email and photo is used.
Arguably, the authorization code resource does not need to be configured as a bean, since it is only used by the authentication processing filter.
Authentication Entry Point
The authentication processing filter and the authentication entry point are configured to detect the same request path.
@Configuration @EnableWebSecurity @EnableOAuth2Client @PropertySource("classpath:google-oauth2.properties") public class ... extends WebSecurityConfigurerAdapter { ... public OAuth2ProtectedResourceDetails authorizationCodeResource() {...} @Bean @Description("Filter that checks for authorization code, " + "and if there's none, acquires it from authorization server") public OAuth2ClientAuthenticationProcessingFilter oauth2ClientAuthenticationProcessingFilter() { ... OAuth2ClientAuthenticationProcessingFilter filter = new OAuth2ClientAuthenticationProcessingFilter(oauth2FilterCallbackPath); ... return filter; } ... @Bean public AuthenticationEntryPoint authenticationEntryPoint() { return new LoginUrlAuthenticationEntryPoint(oauth2FilterCallbackPath); } ... }
So, how does this all work?
This is how the security filter chain will look like with the added custom filters. Note that for brevity, not all filters were included.
So, here's what happens at runtime. The client referred to here, is a web application that uses OAuth2 for authentication.
-
Request for a secured resource on the client is received. It travels through the security filter chain until
FilterSecurityInterceptor
. The request has not been authenticated yet (i.e. security context does not contain an authentication object), and theFilterSecurityInterceptor
throws an exception (AuthenticationCredentialsNotFoundException
). This authentication exception travels up the security filter chain, and is handled byExceptionTranslationFilter
. It detects that an authentication exception occured, and delegates to the authentication entry point. The configured authentication entry point (LoginUrlAuthenticationEntryPoint
) redirects the user to a new location (e.g.“/oauth2/callback”
). The request for a secured resource is saved, and request processing completes.Web Browser↓SecurityContextPersistenceFilter↓LogoutFilter↓ExceptionTranslationFilter Delegate to authentication entry point↓ ↑OAuth2ClientContextFilter ↓ ↑OAuth2ClientAuthenticationProcessingFilter ↓ ↑FilterSecurityInterceptor Throws exception!Secured Resource - Since a redirect is the response of the previous request, a request to the new location is made. This request travels through the security filter chain until
OAuth2ClientAuthenticationProcessingFilter
determines that it is a request for authentication (e.g. it matches“/oauth2/callback”
). Upon checking the request, it determines that there’s no authorization code, and throws an exception (UserRedirectRequiredException
) that contains a URL to the authorization code resource (e.g.https://accounts.google.com/o/oauth2/v2/auth?client_id=https://accounts.google.com/o/oauth2/v2/auth?client_id=…&redirect_uri=http://…/…/oauth2/callback&response_type=code&scope=…&state=…
). This exception is handled byOAuth2ClientContextFilter
. And request processing completes.Web Browser↓SecurityContextPersistenceFilter↓LogoutFilter↓ExceptionTranslationFilter↓OAuth2ClientContextFilter Handle exception by sending redirect↓ ↑OAuth2ClientAuthenticationProcessingFilter Throws exception!FilterSecurityInterceptorSecured Resource - Just as before, the redirect is followed. This time, it is a redirect to the authorization server (e.g.
https://accounts.google.com/o/oauth2/v2/auth
). The user is asked to authenticate (if not yet authenticated). - Next, the user is asked to allow/authorize the client to have access to his/her information. After the user decides to allow/authorize the client, the authorization server redirects back to the client (based on the
redirect_uri
parameter). - Request on the client is received. It travels through the security filter chain until
OAuth2ClientAuthenticationProcessingFilter
determines that it is a request for authentication (e.g. it matches“/oauth2/callback”
). It finds that the request contains an authorization code, and proceeds to exchange the authorization code for an access token. Furthermore, it validates the access token by accessing a resource (on a resource server), and creates anAuthentication
object (withPrincipal
andGrantedAuthority
objects). This will be stored in the session and in the security context. And request processing completes with a redirect to the saved request (from #1).Web Browser↓SecurityContextPersistenceFilter↓LogoutFilter↓ExceptionTranslationFilter↓OAuth2ClientContextFilter ↓OAuth2ClientAuthenticationProcessingFilter Exchanges authorization code with access token; creates authentication object and stores it in sessionFilterSecurityInterceptorSecured Resource - Just as before, the redirect is followed. It travels through the security filter chain. This time, the
FilterSecurityInterceptor
allows the request to proceed, since there is an authentication object in the security context (retrieved from session). The secured resource is provided to the user (e.g. render a view/page of the secured resource).Web Browser↓SecurityContextPersistenceFilter↓LogoutFilter↓ExceptionTranslationFilter↓OAuth2ClientContextFilter ↓OAuth2ClientAuthenticationProcessingFilter ↓FilterSecurityInterceptor↓Secured Resource :)
Code and Credits
The code for the sample web application can be found here at my GitHub account.
Again, thanks to Saket's Blog.
Also, check out Security Architecture with Spring at Java Code Geeks.
how to take principal object and stores in to the database ? after running the application shows authentication object is not in the security context?
ReplyDeleteHi. Didyou manage to get the principal (authentication object) into the security context? I'm having the same issue.
DeleteThanks
This comment has been removed by the author.
ReplyDeleteI clone the application. But not able to run on server. Can you please let me know how to run the application after clone.
ReplyDeletehow to take principal object and stores in to the database ?
ReplyDelete