I have a local orchestrated environment using spring cloud components (eureka, zuul, and an auth servers). These components are all implemented as separate standalone services. I then have a growing number of combined UI/resource services where the individual services all have their own UI. The UI is put together server side using thymeleaf templates but are essentially angularjs single page apps that run in the browser.
A single Zuul service fronts all the ui/resource services. I have annotated all the ui/resource services @EnableResourceServer
and added @EnableOAuth2Sso
to the Zuul server.
In the application.properties for Zuul I have the following properties:
security.oauth2.client.accessTokenUri=http://localhost:8771/uaa/oauth/token
security.oauth2.client.userAuthorizationUri=http://localhost:8771/uaa/oauth/authorize
security.oauth2.client.clientId=waharoa
security.oauth2.client.clientSecret=waharoa
security.oauth2.client.preEstablishedRedirectUri=http://localhost:81/login
security.oauth2.client.registeredRedirectUri=http://localhost:81/login
security.oauth2.client.useCurrentUri=false
security.oauth2.resource.jwt.keyValue=-----BEGIN PUBLIC KEY-----[ETC omitted]...
This all seems to work as advertised. My issue is when the token expires.
In the Auth server I have set the token to expire in 60 seconds and the refresh token to expire in 12 hours. When the token expires the zuul server is unable to get a new token.
At the zuul server this appears in the log:
BadCredentialsException : Cannot obtain valid access token thrown by OAuth2TokenRelayFilter.getAccessToken
Update: I turned on debugging for org.springframework.security.oauth in the Zuul service and got the following
17:12:33.279 DEBUG o.s.s.o.c.t.g.c.AuthorizationCodeAccessTokenProvider - Retrieving token from http://localhost:8771/uaa/oauth/token
17:12:33.289 DEBUG o.s.s.o.c.t.g.c.AuthorizationCodeAccessTokenProvider - Encoding and sending form: {grant_type=[refresh_token], refresh_token=[eyJhbGciOiJS[...deleted...]VgGRHGT8OJ2yDfNVvNA]}
17:12:37.279 WARN o.s.c.n.z.f.post.SendErrorFilter - Error during filtering
[blah blah stacktrace many lines omitted]
Caused by: org.springframework.security.authentication.BadCredentialsException: Cannot obtain valid access token
at org.springframework.cloud.security.oauth2.proxy.OAuth2TokenRelayFilter.getAccessToken(OAuth2TokenRelayFilter.java:99)
at org.springframework.cloud.security.oauth2.proxy.OAuth2TokenRelayFilter.run(OAuth2TokenRelayFilter.java:79)
at com.netflix.zuul.ZuulFilter.runFilter(ZuulFilter.java:112)
at com.netflix.zuul.FilterProcessor.processZuulFilter(FilterProcessor.java:193)
... 106 common frames omitted
On the Auth (uaa) service side I can see the zuul client (waharoa) authenticate, get the details of the correct user, and then print:
17:12:37.288 DEBUG o.s.s.w.c.SecurityContextPersistenceFilter - SecurityContextHolder now cleared, as request processing completed
I presume that means that the auth server has done what it needed to and replied to the request? It looks like something not set correctly on the Zuul service, any suggestions?
Could someone please advise what other information I'd need to post here to work out why the token refresh is not working. I am a spring cloud noob and this convention black magic is not very clear to me (I have searched and search for examples of what I thought would be a common use case but found nothing).
Note2: I already have the following bean on the Zuul side
@Bean
public OAuth2RestTemplate oauth2RestTemplate(OAuth2ProtectedResourceDetails resource, OAuth2ClientContext context) {
return new OAuth2RestTemplate(resource, context);
}
Following @AlexK advice I also added the following UserDetailsService Bean on the Auth side
@Bean
@Override
public UserDetailsService userDetailsServiceBean() throws Exception {
return super.userDetailsServiceBean();
}
And added that to my auth server config
@Autowired
private UserDetailsService userDetailsService;
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(tokenStore()).tokenEnhancer(jwtTokenEnhancer())
.authenticationManager(authenticationManager).userDetailsService(userDetailsService)
.reuseRefreshTokens(false);
}
But same outcome. The refresh_token takes place but it still seems to die when the response get to the Zuul filter.
Note 3:
@AlexK was actually spot on. What I found learnt is that when the token is refreshed it is not just refreshed from the token store, it requires a call to the underlying UserDetailsService to get the user details again. As I was getting the details from Active Directory this took a lot of trial and error to resolve but is now working as advertised. My (missing) simple UserDetailsService bean that was autowired into the configuration as shown in Note 2:
@Bean(name = "ldapUserDetailsService")
public UserDetailsService userDetailsService() {
FilterBasedLdapUserSearch userSearch = new FilterBasedLdapUserSearch(searchBase, "(sAMAccountName={0})",
contextSource());
LdapUserDetailsService result = new LdapUserDetailsService(userSearch);
result.setUserDetailsMapper(new InetOrgPersonContextMapper());
return result;
}
I think all the necessary clues are in this Q and A
In short:
After that you get your access_token refreshed automatically by refresh_token.
P.S. But when you refresh_token token get expired you still can get the same-looking error! To deal with it you can make your refresh_token be automatically renewed the same time you get a new access_token. Use reuseRefreshTokens(false) in configuration of AuthorizationServerEndpointsConfigurer at the auth-server code:
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints)
throws Exception {
endpoints
.authenticationManager(authenticationManager)
.userDetailsService(userDetailsService)
.reuseRefreshTokens(false); // <--that's the key to get new refresh_token at the same time as new access_token
}
More thoroughly explained here