Search code examples
spring-bootspring-securityjwtnetflix-zuulcloudfoundry-uaa

Spring cloud Zuul and JWT refresh token


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;
}

Solution

  • I think all the necessary clues are in this Q and A

    In short:

    1. The clue in question - it's necessary to implement OAuth2RestTemplate on your Zuul/UIApp side. As it's said in Spring Boot reference it's not created by default
    2. The other part is inside that answer - it's necessary to do certain modification on OAuth-server side

    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