Token is not active after some time using Spring Security with Keycloak

72 views Asked by At

I have created a Spring based Vaadin application and connected it to a Keycloak instance to handle the user authentication. The application will also get an access-key from Keycloak and use it to call an API.

This works fine so far - the user is sent to Keycloaks login page and after authentication the application is able to make API calls using the access token. However, I noticed that sometimes the refresh-token expires, when not doing any interaction with the application for a while.

My guess is that is due to the timeout settings in keycloak. When trying to interact with the Vaadin-application after expiry, I get an exception and have to restart the browser in order to login again.

Trying to fetch data will throw the following exception:

org.springframework.security.oauth2.client.ClientAuthorizationException: [invalid_grant] Token is not active
    at org.springframework.security.oauth2.client.RefreshTokenOAuth2AuthorizedClientProvider.getTokenResponse(RefreshTokenOAuth2AuthorizedClientProvider.java:105) ~[spring-security-oauth2-client-6.2.1.jar:6.2.1]
    Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: 
Error has been observed at the following site(s):
    *__checkpoint ⇢ Request to GET http://127.0.0.1:8085/secure [DefaultWebClient]
Original Stack Trace:
        at org.springframework.security.oauth2.client.RefreshTokenOAuth2AuthorizedClientProvider.getTokenResponse(RefreshTokenOAuth2AuthorizedClientProvider.java:105) ~[spring-security-oauth2-client-6.2.1.jar:6.2.1]
        at org.springframework.security.oauth2.client.RefreshTokenOAuth2AuthorizedClientProvider.authorize(RefreshTokenOAuth2AuthorizedClientProvider.java:93) ~[spring-security-oauth2-client-6.2.1.jar:6.2.1]
        at org.springframework.security.oauth2.client.DelegatingOAuth2AuthorizedClientProvider.authorize(DelegatingOAuth2AuthorizedClientProvider.java:71) ~[spring-security-oauth2-client-6.2.1.jar:6.2.1]
        at org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager.authorize(DefaultOAuth2AuthorizedClientManager.java:176) ~[spring-security-oauth2-client-6.2.1.jar:6.2.1]
        at org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction.lambda$authorizeClient$22(ServletOAuth2AuthorizedClientExchangeFilterFunction.java:486) ~[spring-security-oauth2-client-6.2.1.jar:6.2.1]
        at reactor.core.publisher.MonoSupplier.call(MonoSupplier.java:67) ~[reactor-core-3.6.2.jar:3.6.2]
        at reactor.core.publisher.FluxSubscribeOnCallable$CallableSubscribeOnSubscription.run(FluxSubscribeOnCallable.java:228) ~[reactor-core-3.6.2.jar:3.6.2]
        at reactor.core.scheduler.SchedulerTask.call(SchedulerTask.java:68) ~[reactor-core-3.6.2.jar:3.6.2]
        at reactor.core.scheduler.SchedulerTask.call(SchedulerTask.java:28) ~[reactor-core-3.6.2.jar:3.6.2]
        at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:317) ~[na:na]
        at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:304) ~[na:na]
        at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144) ~[na:na]
        at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642) ~[na:na]
        at java.base/java.lang.Thread.run(Thread.java:1583) ~[na:na]

and

org.springframework.security.oauth2.client.ClientAuthorizationRequiredException: [client_authorization_required] Authorization required for Client Registration Id: keycloak
    at org.springframework.security.oauth2.client.AuthorizationCodeOAuth2AuthorizedClientProvider.authorize(AuthorizationCodeOAuth2AuthorizedClientProvider.java:57) ~[spring-security-oauth2-client-6.2.1.jar:6.2.1]
    Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: 
Error has been observed at the following site(s):
    *__checkpoint ⇢ Request to GET http://127.0.0.1:8085/analysis/8 [DefaultWebClient]
Original Stack Trace:
        at org.springframework.security.oauth2.client.AuthorizationCodeOAuth2AuthorizedClientProvider.authorize(AuthorizationCodeOAuth2AuthorizedClientProvider.java:57) ~[spring-security-oauth2-client-6.2.1.jar:6.2.1]
        at org.springframework.security.oauth2.client.DelegatingOAuth2AuthorizedClientProvider.authorize(DelegatingOAuth2AuthorizedClientProvider.java:71) ~[spring-security-oauth2-client-6.2.1.jar:6.2.1]
        at org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager.authorize(DefaultOAuth2AuthorizedClientManager.java:176) ~[spring-security-oauth2-client-6.2.1.jar:6.2.1]
        at org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction.lambda$authorizeClient$22(ServletOAuth2AuthorizedClientExchangeFilterFunction.java:486) ~[spring-security-oauth2-client-6.2.1.jar:6.2.1]
        at reactor.core.publisher.MonoSupplier.call(MonoSupplier.java:67) ~[reactor-core-3.6.2.jar:3.6.2]
        at reactor.core.publisher.FluxSubscribeOnCallable$CallableSubscribeOnSubscription.run(FluxSubscribeOnCallable.java:228) ~[reactor-core-3.6.2.jar:3.6.2]
        at reactor.core.scheduler.SchedulerTask.call(SchedulerTask.java:68) ~[reactor-core-3.6.2.jar:3.6.2]
        at reactor.core.scheduler.SchedulerTask.call(SchedulerTask.java:28) ~[reactor-core-3.6.2.jar:3.6.2]
        at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:317) ~[na:na]
        at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:304) ~[na:na]
        at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144) ~[na:na]
        at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642) ~[na:na]
        at java.base/java.lang.Thread.run(Thread.java:1583) ~[na:na]
    Suppressed: java.lang.Exception: #block terminated with an error

The SecurityConfiguration looks like this:

@EnableWebSecurity
@Configuration
public class SecurityConfiguration extends VaadinWebSecurity {

    private final KeycloakLogoutHandler keycloakLogoutHandler;

    public SecurityConfiguration(KeycloakLogoutHandler keycloakLogoutHandler) {
        this.keycloakLogoutHandler = keycloakLogoutHandler;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http.authorizeHttpRequests(
                authorize -> authorize.requestMatchers(new AntPathRequestMatcher("/images/*.png")).permitAll());

        // Icons from the line-awesome addon
        http.authorizeHttpRequests(authorize -> authorize
                .requestMatchers(new AntPathRequestMatcher("/line-awesome/**/*.svg")).permitAll());

        http.oauth2Login(Customizer.withDefaults()).logout( logout ->
                logout.addLogoutHandler(keycloakLogoutHandler)
        );

        http.oauth2Client(Customizer.withDefaults());

        super.configure(http);

        setOAuth2LoginPage(http, "/oauth2/authorization/keycloak");
    }

    @Bean
    public GrantedAuthoritiesMapper userAuthoritiesMapperForKeycloak() {
        return authorities -> {
            Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
            var authority = authorities.iterator().next();
            if (authority instanceof OidcUserAuthority oidcUserAuthority) {
                var userInfo = oidcUserAuthority.getUserInfo();
                if (userInfo.hasClaim("realm_access")) {
                    var realmAccess = userInfo.getClaimAsMap("realm_access");
                    var roles = (Collection<String>) realmAccess.get("roles");
                    mappedAuthorities.addAll(roles.stream()
                            .map(role -> new SimpleGrantedAuthority("ROLE_" + role.toUpperCase()))
                            .toList());
                }
            }
            return mappedAuthorities;
        };
    }
}

I created a WebClientConfig to contain the necessary information and to set the Authentication:

@Configuration
public class WebClientConfig {

    @Bean
    public WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) {
        ServletOAuth2AuthorizedClientExchangeFilterFunction filter =
                new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
        return WebClient.builder()
                .apply(filter.oauth2Configuration())
                .baseUrl("http://127.0.0.1:8085")
                .build();
    }
}

And I am able to use it like that:


@Service
public class ApiClient {

    private final WebClient webClient;

    @Autowired
    public ApiClient(WebClient webClient) {
        this.webClient = webClient;
    }

    public ResponseEntity<String> fetchDataFromRestEndpoint() {
        return webClient.get()
                .uri("http://127.0.0.1:8085/secure")        
                //setting clientRegistrationId manually 
                .attributes(clientRegistrationId("keycloak"))
                .retrieve()
                .toEntity(String.class)
                .block();

    }
}

(I had to manually set the clientRegistrationId, even though Springs docs said that this would not be necessary since information should be derived from the logged in user: https://docs.spring.io/spring-security/reference/servlet/oauth2/index.html#oauth2-client-access-protected-resources-current-user)

Now to my questions:

  1. Why do I have to manually set the clientRegistrationId in my webclient? Did I misconfigure something, so that the "automatic" detection doesn't work?
  2. And more important: How should I handle the token-timeout? Probably the user should be redirected to the login page. How could I configure that?
1

There are 1 answers

0
Loahrs On

I found a solution in this answer for now. The answer contains a TokenExpirationFilter that checks for invalid tokens after each request. When Keycloak invalidated the Session due to SSO Idle Timeout, the Filter will invalidate the Browser Session and bring the user back to the login page.