Reactive spring cloud security (with Keycloak): Session expiration?

Viewed 3277

I try implementing the following: I want a distributed environment with one or more spring cloud gateway(s), behind these are several micro services (partly) exposed to the outside. For user authentication I want to use OIDC (just moved to Keycloak).

I actually just sticked to the standard configuration from the reference documentations of spring security, webflux and boot.

In detail, I have in the gateway:

private final ReactiveClientRegistrationRepository clientRegistrationRepository;

@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
    http
            .csrf().disable()
            .authorizeExchange()
            .pathMatchers("/login").permitAll()
            .pathMatchers("/actuator/**").permitAll()
            .anyExchange().authenticated()
            .and()
            .oauth2Login()
            .and()
            .logout(logout -> logout.logoutSuccessHandler(oidcLogoutSuccessHandler()));
    return http.build();
}

    private ServerLogoutSuccessHandler oidcLogoutSuccessHandler() {
    OidcClientInitiatedServerLogoutSuccessHandler oidcLogoutSuccessHandler =
            new OidcClientInitiatedServerLogoutSuccessHandler(clientRegistrationRepository);

    oidcLogoutSuccessHandler.setPostLogoutRedirectUri(URI.create("https://<host>/login?logout"));

    return oidcLogoutSuccessHandler;
}

with application.yml (only essential part):

spring:
  cloud:
    gateway:
      default-filters: 
        - TokenRelay=
        - RemoveRequestHeader=Cookie
      discovery:
        locator:
          enabled: true
    consul:
      host: 127.0.0.1
      port: 8500
    loadbalancer:
      ribbon:
        enabled: false
  security:
    oauth2:
      client:
        provider:
          keycloak:
            issuer-uri: ${KEYCLOAK_ISSUER_URI}
        registration:
          keycloak:
            client-id: ${KEYCLOAK_CLIENT_ID}
            client-secret: ${KEYCLOAK_CLIENT_SECRET}
      resourceserver:
        jwt:
          jwk-set-uri: ${KEYCLOAK_ISSUER_URI}/protocol/openid-connect/certs
server:
  forward-headers-strategy: framework

For the proof of concept I have an "environment-test-application" with a controller just returning the claims as a json. It is configured like:

@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
    http
            .authorizeExchange()
            .pathMatchers("/actuator/**").permitAll()
            .anyExchange().authenticated()
            .and()
            .oauth2ResourceServer()
            .jwt(Customizer.withDefaults());

    return http.build();
}

The application.yml only contains the consul and security part from above.

Basically this works. If I try to access the environment-test-application, I get redirected to Keycloak, I have to enter my credentials and get redirected back to the environment-test-application and see the claims. I can logout by means of "/logout".

The problem: After 5 minutes the keycloak session expires, I get a 401 and chrome says "this page is not working". This happens even, if I am reloading all the time (user activity).

To my understanding the session should:

  • be extended as long as there is activity (I thought this is what the refresh token is used for, and assumed spring security to handle this automatically)
  • expire when the user is inactive for some time. When the user is active again, he/she should be redirected to the login and then back to the original resource.
  • when hitting "remember me", the an expired session should be reinitiated without user activity (I think, this is what the offline token from Keycloak is meant for).

I guess all this is possible by adding just some simple lines of configuration, I just can't figure which. The reference documentation wasn't entirely clear to me, but I got the feeling this is all handled like so, by default. However, it is not.

Note: I am using spring reactive stack with the default Reactor Netty. Thus, neither the spring security, nor the spring boot plugin provided by Keycloak, can be used. Plus, it is not clear to me, how they interact with the standard configuration scheme.

Note: Before I was trying all this with okta (using their starter). The problem described above magically seemed to be working with okta. However, I had different issues and thus moved to open source now.

1 Answers

TokenRelayGatewayFilterFactory adds the access token but doesn't refresh it when it expires...which is why you get the 401 I believe. There's an open spring cloud gateway issue asking for a solution that refreshes as well. One of the comments on that issue provides an implementation: https://github.com/spring-cloud/spring-cloud-security/issues/175#issuecomment-557135243.

When the refresh token filter is working, the keycloak session only becomes important after your spring cloud gateway "session" expires because, if the keycloak session is still good, it allows the oauth2 redirect to re-establish the session seemlessly (i.e. without having to enter your credentials again).

If you're looking to customize the session of your webflux application, there are some solutions including this one: https://stackoverflow.com/a/62344617/1098564

Not sure what you're using for your frontend but he's how I use spring security with spring cloud gateway and react: https://sdoxsee.github.io/blog/2019/12/17/merry-microservices-part2-ui-gateway

Related