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?
Most probably, a new login completes successfully, silently because the user session is still valid on the authorization server (SSO auto-login).
You have two distinct sessions: one on the BFF (the Spring Cloud Gateway configured with oauth2Login
) 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 was still valid. Because the BFF session expired, the tokens kept in session were lost and the user was redirected to the authorization server. But as the session on the authorization server was still valid (no Logout was performed and authorization server sessions are usually very long), an authorization code was returned without displaying the login form (code with which the Spring client with oauth2Login
could authorize the new session with fresh tokens).