How to cache a JWT in Spring-Security from an OAuth2Client

Viewed 162

In my case I have an application for SpringBootAdmin. SpringBootAdmin sends requests to a lot of applications all the time. For these requests I set an access token (JWT) which I pull from Keycloak via the AuthorizedClientServiceOAuth2AuthorizedClientManager.

Now the problem is that this token is not cached, and Spring-Security sends about 100 requests per minute to Keycloak to get the access token.

So is there a way to cache this JWT ?

Here is my SecurityConfig:

@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter
{
    @Override
    public void configure(HttpSecurity http) throws Exception
    {
        http.authorizeRequests()
                .anyRequest()
                .permitAll()
                .and()
                .oauth2Client()
                .and()
                .csrf()
                .ignoringAntMatchers("/**");
    }
}

My ClientRegistration:

@Configuration
public class ClientRegistrationConfiguration
{
    private static final String KEYCLOAK = "keycloak";

    @Bean
    public ClientRegistration clientRegistration(OAuth2ClientProperties properties)
    {
        return withRegistrationId(KEYCLOAK)
                .tokenUri(properties.getProvider().get(KEYCLOAK).getTokenUri())
                .clientId(properties.getRegistration().get(KEYCLOAK).getClientId())
                .clientSecret(properties.getRegistration().get(KEYCLOAK).getClientSecret())
                .authorizationGrantType(CLIENT_CREDENTIALS)
                .build();
    }

    @Bean
    public ClientRegistrationRepository clientRegistrationRepository(ClientRegistration clientRegistration)
    {
        return new InMemoryClientRegistrationRepository(clientRegistration);
    }

    @Bean
    public OAuth2AuthorizedClientService oAuth2AuthorizedClientService(ClientRegistrationRepository clientRegistrationRepository)
    {
        return new InMemoryOAuth2AuthorizedClientService(clientRegistrationRepository);
    }

    @Bean
    public AuthorizedClientServiceOAuth2AuthorizedClientManager authorizedClientServiceOAuth2AuthorizedClientManager(
            ClientRegistrationRepository clientRegistrationRepository,
            OAuth2AuthorizedClientService authorizedClientService)
    {
        var authorizedClientProvider = builder().clientCredentials().build();
        var authorizedClientManager = new AuthorizedClientServiceOAuth2AuthorizedClientManager(clientRegistrationRepository, authorizedClientService);
        authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
        return authorizedClientManager;
    }
}

And my RequestConfiguration:

@Configuration
public class HttpRequestConfiguration
{
    @Bean
    public HttpHeadersProvider customHttpHeadersProvider(AuthorizedClientServiceOAuth2AuthorizedClientManager clientManager)
    {
        return instance ->
        {
            var httpHeaders = new HttpHeaders();
            var token = Objects.requireNonNull(clientManager.authorize(withClientRegistrationId("keycloak").principal("Keycloak").build())).getAccessToken();
            httpHeaders.add("Authorization", "Bearer " + token.getTokenValue());
            return httpHeaders;
        };
    }
}
1 Answers

Your headers provider is a bean, so you can simply cache the token there. If you write your provider like that:

    @Bean
    public HttpHeadersProvider customHttpHeadersProvider(AuthorizedClientServiceOAuth2AuthorizedClientManager clientManager)
    {
        var token = Objects.requireNonNull(clientManager.authorize(withClientRegistrationId("keycloak").principal("Keycloak").build())).getAccessToken();

        return instance ->
        {
            var httpHeaders = new HttpHeaders();
            httpHeaders.add("Authorization", "Bearer " + token.getTokenValue());
            return httpHeaders;
        };
    }

Then the token will be requested once, only when the bean is created. This will fix calling Keycloak on each request but has the usual problem of caching - at some point the access token will expire and you need a way to get a new token. One way to do it would be to catch 401 errors from your client and force recreation of the customHttpHeadersProvider bean when that happens.

Another way would be to create an object which will be a "token provider" and a bean itself. That object would keep the token in memory and have a method to refresh the token. Then you could create the authorization header near the request itself, instead of using a headers provider bean. You will have more control then over when do you want to refresh the access token.

Related