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?