-1

I am implementing a microservices application and for the authentication issue I have used something similar to what @ch4mp proposed in his https://www.baeldung.com/spring-cloud-gateway-bff-oauth2 tutorial. The difference is that my oauth2Client works also as a gateway, I also have an angular application.

The problem I have is that I don't know how to handle the session issue, since the login works as it should, the authorization is requested to my authorization server, it is validated and through a cookieSession I can enter to my dashboard. My accessToken and RefreshToken are set with a duration of 5 minutes and 1 hour respectively. And for testing purposes I have set the session to last 30 minutes. All the logout flow works as it should, for this I use the OIDC Logout standard in which the id_token is used. The problem arises that when the session expires, I have to refresh the page so that I reridiga to /home but at the time of logging in again no longer asks me the credentials but enters directly as if I had already entered the credentials.

So according to this what would be the best solution? I thought that the session should not I was thinking that the session should not expire but by doing this then I should limit the number of sessions a user should have?

My SecurityConfigClient:

@Configuration
@EnableWebFluxSecurity
public class ClientSecurityConfig {

    @Autowired
    private ReactiveClientRegistrationRepository clientRegistrationRepository;

    @Value("${intechbo.server.gateway}")
    private String gatewayUrl;

    @Bean
    public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http,
                                                         ServerOAuth2AuthorizationRequestResolver resolver) {
        http
                .cors(ServerHttpSecurity.CorsSpec::disable)
                .csrf(ServerHttpSecurity.CsrfSpec::disable)
                .authorizeExchange(
                        exchanges -> exchanges
                                .pathMatchers(SecurityConstants.AUTH_WHITELIST).permitAll()
                                .pathMatchers("/*.js", "/*.css", "/*.ico", "/*.jpg", "/*.png", "/*.html", "/*.svg").permitAll()
                                .pathMatchers(SecurityConstants.AUTH_ANGULAR_COMPILER_WHITELIST).permitAll()
                                .pathMatchers("/backoffice/home/**").permitAll()
                                .pathMatchers("/backoffice/home").permitAll()
                                .pathMatchers("/backoffice/authentication/logout").permitAll()
                                .pathMatchers("/backoffice/profile/**").authenticated()
                                .pathMatchers("/logged-out").permitAll()
                                .pathMatchers("/authenticate").authenticated()
                                .anyExchange().authenticated()
                )
                .oauth2Login(auth ->
                        auth.authorizationRequestResolver(resolver)
                                .authenticationSuccessHandler(new CustomServerAuthenticationSuccessHandler("/backoffice/authentication/login"))
                )
                .oauth2Client(Customizer.withDefaults())
                .logout(
                        logout -> logout
                                .logoutUrl("/logout")
                                .logoutSuccessHandler(oidcLogoutSuccessHandler())
                )
                .exceptionHandling(
                        exceptionHandlingSpec -> exceptionHandlingSpec
                                .authenticationEntryPoint((swe, e) -> {
                                    ServerHttpResponse response = swe.getResponse();
                                    response.setStatusCode(HttpStatus.SEE_OTHER);
                                    response.getHeaders().setLocation(URI.create("/backoffice/home"));
                                    return response.setComplete();
                                })
                );
        return http.build();
    }

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

        // Sets the location that the End-User's User Agent will be redirected to
        // after the logout has been performed at the Provider
        oidcLogoutSuccessHandler.setPostLogoutRedirectUri(gatewayUrl + "/logged-out");

        return oidcLogoutSuccessHandler;
    }

    @Bean
    public ServerOAuth2AuthorizationRequestResolver pkceResolver(ReactiveClientRegistrationRepository repo) {
        DefaultServerOAuth2AuthorizationRequestResolver resolver = new DefaultServerOAuth2AuthorizationRequestResolver(repo);
        resolver.setAuthorizationRequestCustomizer(OAuth2AuthorizationRequestCustomizers.withPkce());
        return resolver;
    }

    @Bean
    public WebClient webClient(ReactiveOAuth2AuthorizedClientManager authorizedClientManager) {
        ServerOAuth2AuthorizedClientExchangeFilterFunction oauth2Client =
                new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
        oauth2Client.setDefaultOAuth2AuthorizedClient(true);
        return WebClient.builder()
                .filter(oauth2Client)
                .build();
    }

    @Bean
    public ReactiveOAuth2AuthorizedClientManager authorizedClientManager(ReactiveClientRegistrationRepository clientRegistrationRepository,
                                                                         ServerOAuth2AuthorizedClientRepository authorizedClientRepository) {
        ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider =
                ReactiveOAuth2AuthorizedClientProviderBuilder.builder()
                        .authorizationCode()
                        .refreshToken()
                        .build();
        DefaultReactiveOAuth2AuthorizedClientManager authorizedClientManager =
                new DefaultReactiveOAuth2AuthorizedClientManager(clientRegistrationRepository, authorizedClientRepository);
        authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
        return authorizedClientManager;
    }

}

My Configuration to set maxInactiveIntervalInSeconds:

@Configuration
@EnableRedisWebSession(redisNamespace = "inclub:session", maxInactiveIntervalInSeconds = 600)
public class SessionConfig {

}

My application.yml:

logging:
  level:
    org.springframework.cloud.gateway.handler.RoutePredicateHandlerMapping: DEBUG
    org:
      springframework:
        security: DEBUG
        session: DEBUG
        web: DEBUG


spring:
#  cache:
#    redis:
#      time-to-live: 60000
  application:
    name: bo-gateway-server
  session:
    redis:
      repository-type: default
#    timeout: 10m
  security:
    oauth2:
      client:
        registration:
          backoffice-gateway:
            provider: spring
            client-id: example-client
            client-secret: 
            authorization-grant-type: authorization_code
            redirect-uri: ${intechbo.server.gateway}/login/oauth2/code/backoffice-gateway
            scope: read,write,openid,profile
        provider:
          spring:
            issuer-uri: ${intechbo.server.oauth}

  cloud:
    gateway:
      default-filters:
        - DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin
        - TokenRelay=
        - SaveSession
      routes:
        - id: pets-service-route
          uri: uri
          predicates:
            - Path=/api/v1/breeds/**
          filters:
            - name: Retry
              args:
                retries: 5
                methods: GET
                backoff:
                  firstBackoff: 50ms
                  maxBackOff: 400ms
            - name: CircuitBreaker
              args:
                name: petsService
                fallbackUri: forward:/pets-service-fallback
            - name: RequestRateLimiter
              args:
                key-resolver: "#{@userKeyResolver}"
                redis-rate-limiter.replenishRate: 2
                redis-rate-limiter.burstCapacity: 2

        -  id: account-service-route
#           uri: http://localhost:8776
           uri: uri
           predicates:
             - Path=/api/v1/account/**
           filters:
             - name: Retry
               args:
                 retries: 5
                 methods: GET
                 backoff:
                   firstBackoff: 50ms
                   maxBackOff: 400ms
             - name: CircuitBreaker
               args:
                 name: accountService
                 fallbackUri: forward:/account-service-fallback
             - name: RequestRateLimiter
               args:
                 key-resolver: "#{@userKeyResolver}"
                 redis-rate-limiter.replenishRate: 2
                 redis-rate-limiter.burstCapacity: 2

        - id: membership-service-route
          uri: uri
          predicates:
            - Path=/api/v1/membership/**, /api/v1/pay/** , /api/v1/store/**
          filters:
            - name: RequestRateLimiter
              args:
                key-resolver: "#{@userKeyResolver}"
                redis-rate-limiter.replenishRate: 2
                redis-rate-limiter.burstCapacity: 2

        - id: treepointrange-service-route
          uri: uri
          predicates:
            - Path=/api/v1/three/**, /api/v1/placement/**
          filters:
            - name: RequestRateLimiter
              args:
                key-resolver: "#{@userKeyResolver}"
                redis-rate-limiter.replenishRate: 2
                redis-rate-limiter.burstCapacity: 2

        - id: wallet-service-route
          uri: uri
          predicates:
            - Path=/api/v1/wallet/**, /api/v1/wallettransaction/**, /api/v1/withdrawalrequest/**, /api/v1/tokenwallet/**, /api/v1/electronicpurse/**, /api/v1/accountbank/**
          filters:
            - name: RequestRateLimiter
              args:
                key-resolver: "#{@userKeyResolver}"
                redis-rate-limiter.replenishRate: 2
                redis-rate-limiter.burstCapacity: 2

#        - id: angular
#          uri: ${intechbo.server.webapp}
#          predicates:
#            - Path=/backoffice/**
#          filters:
##            - RewritePath=/backoffice(?<segment>/?.*), /$\\{segment}
#            - RewritePath=/backoffice(?<segment>/?.*), "/\\$\\{segment}"
        - id: angular
          uri: ${intechbo.server.webapp}
          predicates:
            - Path=/
          filters:
            - RewritePath=/, /backoffice
        - id: static
          uri: ${intechbo.server.webapp}
          predicates:
            - Path=/**

  data:
    redis:
      port: ${REDIS_SERVER_PORT:6379}
      host: ${REDIS_SERVER_HOST:localhost}
      password: ${REDIS_SERVER_PASSWORD:}
      timeout: 5000
      lettuce:
        pool:
          max-idle: 9
          min-idle: 1
          max-active: 9
          max-wait: 5000

eureka:
  client:
    service-url:
      defaultZone: ${intechbo.server.discover}
    fetch-registry: true
    register-with-eureka: true

server:
  port: 8090
#  reactive:
#    session:
#      timeout: 1m
  reactive:
    session:
      timeout: 10m
      cookie:
        name: INTECHBOSESSION
        max-age: 10m

resilience4j:
  circuitbreaker:
    configs:
      default:
        slidingWindowSize: 10
        slidingWindowType: COUNT_BASED
        permittedNumberOfCallsInHalfOpenState: 6
        failureRateThreshold: 50
        waitDurationInOpenState: 10s
        registerHealthIndicator: true
        automaticTransitionFromOpenToHalfOpenEnabled: true

    instances:
      petsService:
        baseConfig: default

  retry:
    instances:
      authorizationServer:
        maxAttempts: 3
        waitDuration: 2500ms
        enableExponentialBackoff: true
        exponentialBackoffMultiplier: 2

  timelimiter:
    configs:
      values:
        timeout-duration: 80s
    instances:
      offersTimeLimiter: # Unique name for TimeLimiter
        base-config: values

management:
  health:
    circuitbreakers:
      enabled: true
  endpoints:
    web:
      exposure:
        include: "*"
  endpoint:
    health:
      show-details: always

If I were to choose to never invalidate at some point I may have problems in my redis database where I am storing this?

1
  • When your session is expired, you should get a 401 right? Can you catch that 401 response code from the Angular app and do a token refresh call? Commented Jul 9 at 4:08

1 Answer 1

1

Most probably, a new login completes successfully, silently because the user session is still valid on the authorization server.

You have two different sessions: one your BFF (the Spring Cloud Gateway configured withoauth2Login) and a different one on the authorization server.

Also, remember that when a Spring client session expires, what happens is token deletion on the client, not a logout from the authorization server (RP initiated Logout involves the user agent, which is not there when a session times out).

In your scenario, it is very likely that the BFF session had expired, but that the session on the authorization server is still valid. As a consequence, because the BFF session expired, the tokens kept in session were lost and the user is redirected to the authorization server. But as the session on the authorization server is still valid (no Logout was performed and authorization servers sessions are usually very long), the authorization server returns an authorization code without displaying login form).

8
  • Hi @ch4mp, thanks for the answer. But in my securityFilterChain of my spring authorization server I have not specified anything regarding sessionManagement, so according to your answer if I don't specify this configuration a default session is created? If this is the case then I would have to change the session duration time to match the one in the client? Commented Jul 10 at 17:27
  • 1
    If the authorization server is Springs one, you have configured it with formLogin, which requires sessions. Actually, the only way for an authorization server to be stateless (no session) would be to offload this state to a "rich" frontend, which would be pretty unsafe (reason for no authorization server I know to do that). So yes, if you want to force login forms to be displayed after a delay of inactivity, tune refresh token and authorization server session durations.
    – ch4mp
    Commented Jul 10 at 18:19
  • But what is the point of using SSO if you want to force user login when he did not explicitly Logout?
    – ch4mp
    Commented Jul 10 at 18:23
  • I mean the idea that I had was to not have a lot of open sessions, was to limit the user to have a single session but not expire and when the user wants to start another instance, close the one that is active. But I really wanted to know your opinion about it, what do you think would be the best strategy? Commented Jul 10 at 19:55
  • You need these two sessions (different Spring applications => different sessions). The length of the authorization server session is a matter of user experience: after how long of inactivity does he have to enter credentials again for the client (the BFF) to be provided with an authorization code. For most clients, I'd set it with a long period (only very sensitive apps like banking or Healthcare might require short lived sessions). The session on the BFF can be shorter lived as you can create new ones with the "auto-login" you're experiencing, but I'd set it to a long period too.
    – ch4mp
    Commented Jul 10 at 20:58