There are a lot of great articles out there about OAuth 2.0, Spring Security, REST especially when you are about to have both the Resource and the Authorization server in the same application.
However, there are cases when you want to separate these. This is mainly true when you have microservices and the most straightforward way implementing these two is to use Spring Boot because what you need to do there is mostly configuration.
In this article, I’ll walk you through the implementation of a simple, secured resource server using Google OAuth (Implicit grant flow) with standard Spring. In case you are not familiar with OAuth, make sure you check it out before you continue reading.
First of all, let’s define the responsibilities. We will have a simple stateless backend with an endpoint which we want to secure with Google OAuth. The frontend can be any application which can consume the resources provided by the backend – for example an Angular application. The stateless nature of the backend is especially important when you are creating a REST backend as it’s a constraint. Stateless means that an incoming request have to contain all the necessary information for the server to send back a response. The consequence is that the client application have to send the access token to the backend when querying any resource and the server should validate whether the sent token is valid. This is done by going to the authorization server to a specific endpoint which validates the token.
An additional requirement for a real life application is to associate a google user with our user entity thus we need to query the user information from google (id, email, etc.). I’ll show you how you can create a custom Authentication object for this case.
Let’s start with a simple Spring MVC controller which defines one endpoint.
@RestController public class SecuredAPIController { @RequestMapping(value = "/secure") public Response secure() { return new Response("example"); } public static class Response { private String str; public Response(String str) { this.str = str; } public String getStr() { return str; } public void setStr(String str) { this.str = str; } } }
Now we want to secure all the endpoints – in this case only one – so we have the following configuration:
@Configuration @EnableResourceServer @EnableWebSecurity public class ResourceServerConfig extends ResourceServerConfigurerAdapter { @Autowired private OAuthProperties oAuthProperties; @Override public void configure(final HttpSecurity http) throws Exception { http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); http.authorizeRequests().anyRequest().hasRole("USER"); } @Override public void configure(ResourceServerSecurityConfigurer resources) throws Exception { resources.resourceId(oAuthProperties.getClientId()); } }
I’m not gonna go into every detail but what is important here is that we have the @EnableResourceServer annotation on the configuration class and we have the HTTP security configured. The first thing we have to do is to make the security stateless and the second thing is that we want to secure every endpoint on our server to be accessible for users which have USER role. Of course you can customize this and check whether a specific OAuth scope is present and so on.
The only thing left is to define a ResourceServerTokenServices bean. This is the place where you have to return an OAuth2Authentication object based on an access token which is passed in the request’s header.
For remote authorization server, you have the option to use Spring’s RemoteTokenServices class but as OAuth 2.0 is not specifying how to validate the access token with a remote authorization server, this implementation won’t fit in all the cases. Unfortunately the latter is the case for Google, but it’s not that complicated to implement on your own.
First of all, we have to create a custom ResourceServerTokenServices implementation, let’s call it GoogleTokenServices. There are 2 methods which you have to override, loadAuthentication and readAccessToken. For our case, only the first one is important.
public class GoogleTokenServices implements ResourceServerTokenServices, InitializingBean { private String userInfoUrl; private RestTemplate restTemplate = new RestTemplate(); private AccessTokenConverter tokenConverter = new DefaultAccessTokenConverter(); private AccessTokenValidator tokenValidator; public GoogleTokenServices(AccessTokenValidator tokenValidator) { this.tokenValidator = tokenValidator; } @Override public OAuth2Authentication loadAuthentication(String accessToken) throws AuthenticationException, InvalidTokenException { AccessTokenValidationResult validationResult = tokenValidator.validate(accessToken); if (!validationResult.isValid()) { throw new UnapprovedClientAuthenticationException("The token is not intended to be used for this application."); } Map<String, ?> tokenInfo = validationResult.getTokenInfo(); OAuth2Authentication authentication = getAuthentication(tokenInfo, accessToken); return authentication; } private OAuth2Authentication getAuthentication(Map<String, ?> tokenInfo, String accessToken) { OAuth2Request request = tokenConverter.extractAuthentication(tokenInfo).getOAuth2Request(); Authentication authentication = getAuthenticationToken(accessToken); return new OAuth2Authentication(request, authentication); } private Authentication getAuthenticationToken(String accessToken) { Map<String, ?> userInfo = getUserInfo(accessToken); String idStr = (String) userInfo.get("id"); if (idStr == null) { throw new InternalAuthenticationServiceException("Cannot get id from user info"); } return new UsernamePasswordAuthenticationToken(new GooglePrincipal(new BigInteger(idStr)), null, singleton(new SimpleGrantedAuthority("ROLE_USER"))); } private Map<String, ?> getUserInfo(String accessToken) { HttpHeaders headers = getHttpHeaders(accessToken); Map map = restTemplate.exchange(userInfoUrl, HttpMethod.GET, new HttpEntity<>(headers), Map.class).getBody(); return (Map<String, Object>) map; } private HttpHeaders getHttpHeaders(String accessToken) { HttpHeaders headers = new HttpHeaders(); headers.set("Authorization", "Bearer " + accessToken); return headers; } @Override public OAuth2AccessToken readAccessToken(String accessToken) { throw new UnsupportedOperationException("Not supported: read access token"); } public void setUserInfoUrl(String userInfoUrl) { this.userInfoUrl = userInfoUrl; } }
Let’s go through the loadAuthentication method. First of all, according to Google, validating the token is a mandatory step so that’s what we have to do through an AccessTokenValidator. This is a custom interface I introduced. I’ll get back to the Google implementation of this class in a few lines below.
After we are certain that the token is ready for use – meaning it’s valid – we have to construct the OAuth2Authentication object. This is done by the getAuthentication method which basically uses a DefaultAccessTokenConverter to extract a basic OAuth2Authentication. This implementation is enough if you don’t want to have any information about the user which the token belongs to. If you want to have the Google userId, email of the token owner, you have to send another request to Google and this is how this implementation looks like. The getAuthenticationToken returns an Authentication object with a GooglePrincipal – custom class – which contains only the Google id of the user plus we are giving a default USER role. If you need for example the email of the Google user, you can put that info into the GooglePrincipal as well as any other necessary data.
Now let’s get back to our AccessTokenValidator.
public class GoogleAccessTokenValidator implements AccessTokenValidator, InitializingBean { private String clientId; private String checkTokenUrl; private RestTemplate restTemplate = new RestTemplate(); public GoogleAccessTokenValidator() { restTemplate.setErrorHandler(new DefaultResponseErrorHandler() { @Override public void handleError(ClientHttpResponse response) throws IOException { if (response.getRawStatusCode() == 400) { throw new InvalidTokenException("The provided token is invalid"); } } }); } @Override public AccessTokenValidationResult validate(String accessToken) { Map<String, ?> response = getGoogleResponse(accessToken); boolean validationResult = validateResponse(response); return new AccessTokenValidationResult(validationResult, response); } private boolean validateResponse(Map<String, ?> response) throws AuthenticationException { String aud = (String) response.get("aud"); if (!StringUtils.equals(aud, clientId)) { return false; } return true; } private Map<String, ?> getGoogleResponse(String accessToken) { HttpEntity<Object> requestEntity = new HttpEntity<>(new HttpHeaders()); Map<String, String> variables = ImmutableMap.of("accessToken", accessToken); Map map = restTemplate.exchange(checkTokenUrl, HttpMethod.GET, requestEntity, Map.class, variables).getBody(); return (Map<String, Object>) map; } public void setClientId(String clientId) { this.clientId = clientId; } public void setCheckTokenUrl(String checkTokenUrl) { this.checkTokenUrl = checkTokenUrl; } }
This validator is written based on Google’s suggestion. That’s why we have a specialized ErrorHandler for the RestTemplate which checks for 400 status as it means that the token is expired, the permissions are revoked or something is not OK with the token. The core idea here is to check whether the returned JSON contains the application’s clientId as audiance or the token is intended to be used for other application.
The source can be found on GitHub. Feel free to reach me out in case of questions in the comments or on Twitter.
For some reason I end up with this error: Caused by: java.io.FileNotFoundException: class path resource [app.yml] cannot be opened because it does not exist
Any idea?
This is not enough information unfortunately. Did you deploy the app to a container? Did you include the src/main/resources folder on the classpath?
Hi Arnold, I think that you have to change this line:
yaml.setResources(new ClassPathResource(“app.yml”));
to
yaml.setResources(new ClassPathResource(“/app.yml”));
https://stackoverflow.com/questions/14045214/how-can-i-add-src-main-resources-to-the-classpath-when-using-mvn-execjava
Hi,
My app deploy without any problem, but i have no idea for testing the rest endpoint. (with postman or other).
Can you explain how to connect to the rest ?
Could you please elaborate a bit? Where are you stuck at? What did you try? etc.
Maybe it’s better if you ask your question on stackoverflow and link it here so I can check it out.
Hi,
After deploying the app, I tried to access the url ‘ http://localhost:8080/secure‘ the response I got is
‘Full authentication is required to access this resourceunauthorized’ which is correct.
Now, how the client application will make a call to this secure resource after obtaining id_token from the google sign in. Any help in this please. Basically if you update your article with an example using this will much helpful for others.
Thanks
Krishna
Hi,
OK, now I got it working after adding ‘Authorization’ header with value ‘Bearer ” + idtoken value’ while making a rest call to backend server.
Thanks very much Arnold for a great tutorial and providing source code. That really helped me.
Hi Krishna,
At Authorization header what type of authentication you have selected and what is the user name and password for same. can you please help me out?
Thanks & Regards
Dhaval Patel
Thanks a lot, Arnold. As part of my learning process, I was looking to secure my REST API with an external Authorization provider & your post was really helpful.
For those who are using this code in 2019 :
I used this code to protect my REST API. First, of all, you should get access token from Google(in Postman, go to “Authorization”, select Type as “OAuth 2.0”, then “Get New Access Token”). Sent this access token along with my rest API call.
Also, Google+ API should be activated in your Google API Console. Client Id for Google+ API would be different but no worries.
Is it a good practice to use idtoken to validate the user? Shouldnt all the subsequent requests be made using an access token?
A detailed step-by-step instruction on how to get the client working is necessary please.
Right, great intro, but for REST newbies like me, debugging is tough. First I replaced Google with PingFederate. Now all of that is working great. FYI, getGoogleResponse() becomes something like List<HttpMessageConverter> converters = restTemplate.getMessageConverters();
for (HttpMessageConverter converter : converters) {
if (converter instanceof StringHttpMessageConverter) {
StringHttpMessageConverter stringConverter = (StringHttpMessageConverter) converter;
stringConverter.setSupportedMediaTypes(ImmutableList.of(new MediaType(“text”, “html”, StringHttpMessageConverter.DEFAULT_CHARSET)));
}
}
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(checkTokenUrl)
.queryParam(“grant_type”, “urn:pingidentity.com:oauth2:grant_type:validate_bearer”)
.queryParam(“token”, accessToken);
String url = builder.build().encode().toUri().toString();
HttpEntity requestEntity = new HttpEntity(createHeaders(“my-trusted-client”, “secret”));
Map variables = ImmutableMap.of(“token”, accessToken);
Map map = restTemplate.exchange(url, HttpMethod.POST, requestEntity, Map.class, variables).getBody();
return (Map) map;
But now, after all the security code returns without error, I get a resource not found exception. So now I would like to know 1) how to disable security for testing
Please help in running this application.
Hi
After running this project I am getting the below error while hitting to http://localhost:9090/google-oauth2-spring/
11:53:10.864 [http-nio-9090-exec-2] DEBUG o.s.security.web.FilterChainProxy – / at position 1 of 11 in additional filter chain; firing Filter: ‘WebAsyncManagerIntegrationFilter’
11:53:10.864 [http-nio-9090-exec-2] DEBUG o.s.security.web.FilterChainProxy – / at position 2 of 11 in additional filter chain; firing Filter: ‘SecurityContextPersistenceFilter’
11:53:10.865 [http-nio-9090-exec-2] DEBUG o.s.security.web.FilterChainProxy – / at position 3 of 11 in additional filter chain; firing Filter: ‘HeaderWriterFilter’
11:53:10.865 [http-nio-9090-exec-2] DEBUG o.s.s.w.h.writers.HstsHeaderWriter – Not injecting HSTS header since it did not match the requestMatcher org.springframework.security.web.header.writers.HstsHeaderWriter$SecureRequestMatcher@6b62811e
11:53:10.865 [http-nio-9090-exec-2] DEBUG o.s.security.web.FilterChainProxy – / at position 4 of 11 in additional filter chain; firing Filter: ‘LogoutFilter’
11:53:10.865 [http-nio-9090-exec-2] DEBUG o.s.s.w.u.m.AntPathRequestMatcher – Checking match of request : ‘/’; against ‘/logout’
11:53:10.865 [http-nio-9090-exec-2] DEBUG o.s.security.web.FilterChainProxy – / at position 5 of 11 in additional filter chain; firing Filter: ‘OAuth2AuthenticationProcessingFilter’
11:53:10.866 [http-nio-9090-exec-2] DEBUG o.s.web.client.RestTemplate – Created GET request for “https://www.googleapis.com/oauth2/v3/tokeninfo?access_token=ya29.GmCTBq4dQN7lxg17HlJ5BWsACPm8lvhsPW4hWKP5gSkc9NB6jTMezejTa_p6QRyzUyO0San-AV7pBYhS3owrxSPBL67iaTI6_IGYHkFKuJFj6i-6ZAC3kGVaxUmOXPkMwDM”
11:53:10.866 [http-nio-9090-exec-2] DEBUG o.s.web.client.RestTemplate – Setting request Accept header to [application/json, application/*+json]
11:53:12.743 [http-nio-9090-exec-2] DEBUG o.s.web.client.RestTemplate – GET request for “https://www.googleapis.com/oauth2/v3/tokeninfo?access_token=ya29.GmCTBq4dQN7lxg17HlJ5BWsACPm8lvhsPW4hWKP5gSkc9NB6jTMezejTa_p6QRyzUyO0San-AV7pBYhS3owrxSPBL67iaTI6_IGYHkFKuJFj6i-6ZAC3kGVaxUmOXPkMwDM” resulted in 200 (OK)
11:53:12.748 [http-nio-9090-exec-2] DEBUG o.s.web.client.RestTemplate – Reading [interface java.util.Map] as “application/json;charset=UTF-8” using [org.springframework.http.converter.json.MappingJackson2HttpMessageConverter@5bbb606a]
11:53:13.052 [http-nio-9090-exec-2] DEBUG o.s.web.client.RestTemplate – Created GET request for “https://www.googleapis.com/plus/v1/people/me”
11:53:13.052 [http-nio-9090-exec-2] DEBUG o.s.web.client.RestTemplate – Setting request Accept header to [application/json, application/*+json]
11:53:14.463 [http-nio-9090-exec-2] DEBUG o.s.web.client.RestTemplate – GET request for “https://www.googleapis.com/plus/v1/people/me” resulted in 403 (Forbidden); invoking error handler
11:53:14.481 [http-nio-9090-exec-2] DEBUG o.s.s.w.c.SecurityContextPersistenceFilter – SecurityContextHolder now cleared, as request processing completed
Jan 16, 2019 11:53:14 AM org.apache.catalina.core.StandardWrapperValve invoke
SEVERE: Servlet.service() for servlet [dispatcher] in context with path [/google-oauth2-spring] threw exception
org.springframework.web.client.HttpClientErrorException: 403 Forbidden
at org.springframework.web.client.DefaultResponseErrorHandler.handleError(DefaultResponseErrorHandler.java:91)
at org.springframework.web.client.RestTemplate.handleResponse(RestTemplate.java:667)
at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:620)
at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:580)
at org.springframework.web.client.RestTemplate.exchange(RestTemplate.java:498)
at com.arnoldgalovics.api.oauth.GoogleTokenServices.getUserInfo(GoogleTokenServices.java:72)
at com.arnoldgalovics.api.oauth.GoogleTokenServices.getAuthenticationToken(GoogleTokenServices.java:62)
at com.arnoldgalovics.api.oauth.GoogleTokenServices.getAuthentication(GoogleTokenServices.java:57)
at com.arnoldgalovics.api.oauth.GoogleTokenServices.loadAuthentication(GoogleTokenServices.java:51)
at org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationManager.authenticate(OAuth2AuthenticationManager.java:83)
at org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationProcessingFilter.doFilter(OAuth2AuthenticationProcessingFilter.java:150)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:110)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:57)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:87)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:50)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:192)
at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:160)
at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:346)
at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:262)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:198)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96)
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:496)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:140)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:81)
at org.apache.catalina.valves.AbstractAccessLogValve.invoke(AbstractAccessLogValve.java:650)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:87)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:342)
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:803)
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66)
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:790)
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1468)
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:748)
what is the value of checkTokenUrl??
Hi Gerardo. It’s the tokeninfo API that Google provides for checking the validity of a token. More here: https://developers.google.com/identity/sign-in/web/backend-auth#calling-the-tokeninfo-endpoint
Hi,
I have noticed that in the link you provided it states, regarding the tokeninfo endpoint:
‘ It is not suitable for use in production code as requests may be throttled or otherwise subject to intermittent errors.’
What is the alternative then to validate the token and get the token info?
Thanks!
Nevermind!
I was just above 🙂
https://developers.google.com/identity/sign-in/web/backend-auth#verify-the-integrity-of-the-id-token
Where can I find the same code in Spring Boot?
Thanks for article!
If my project contain facebook login or any social login (twitter) and spring oauth2 server (Spring SSO). How to config resoure server for all?
I am not sure if we need to write that much of code given that Spring Security already has built in support for providers like google. I was thinking if we can get around that by just some configuration in SecurityWebFilterChain in a class annotated with @EnableWebFluxSecurity
Hi Saurav. You are right. The article and the code was created 4 years ago, and at the time the out-of-the-box support was quite limited. Also, WebFlux wasn’t even GA back then. Thanks for mentioning though.