I have created Spring Authorization Server and Spring Cloud Gateway and everything looks perfect, but I found a problem in the gateway and specifically in the flow based on the refresh token. When the user logs in, SAS returns an access token, an oidc token, and a refresh token, which are stored in the SCG memory. The SAS session cookie has a duration of 30 minutes and it is used by SCG to prolong the time of access token. After the cookie expires, SCG starts using the refresh token. Each exchange request also replaces the oidc token in SAS, but SCG does not replace it in its memory. When the user decides to log out (RP-Initiated Logout), SCG sends the first id_token returned by SAS. For SAS, this token is invalid and throws an exception.
Is there a way to configure the SAS not to refresh the id_token, or to configure the SCG to replace it in memory on a refresh request?
Here is my SAS configuration:
@EnableWebSecurity
public class SecurityConfig {
@Bean
@Order(1)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http)
throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
http
.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
.oidc(Customizer.withDefaults()); // Enable OpenID Connect 1.0
RequestMatcher endpointsMatcher = http.getConfigurer(OAuth2AuthorizationServerConfigurer.class).getEndpointsMatcher();
http
// Redirect to the login page when not authenticated from the
// authorization endpoint
.exceptionHandling((exceptions) -> exceptions
.defaultAuthenticationEntryPointFor(
new LoginUrlAuthenticationEntryPoint("/login"),
new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
)
)
.cors(Customizer.withDefaults())
.csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher))
// Accept access tokens for User Info and/or Client Registration
.oauth2ResourceServer((resourceServer) -> resourceServer
.jwt(Customizer.withDefaults()));
return http.build();
}
@Bean
@Order(2)
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http)
throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().authenticated()
)
// Form login handles the redirect to the login page from the
// authorization server filter chain
.formLogin(Customizer.withDefaults());
return http.build();
}
@Bean
public UserDetailsService userDetailsService() {
UserDetails userDetails = User.withDefaultPasswordEncoder()
.username("HDonev")
.password("123456")
.roles("DNSPR", "SIGSPR")
.build();
return new InMemoryUserDetailsManager(userDetails);
}
@Bean
public RegisteredClientRepository registeredClientRepository(PasswordEncoder passwordEncoder) {
List<String> aud = List.of("http://localhost:8080");
RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("aca392b5-e4a7-43fa-aa94-3c6f5b5211f1")
.clientSecret(passwordEncoder.encode("123456"))
.clientName("some client name")
.clientAuthenticationMethods(c -> c.addAll(List.of(ClientAuthenticationMethod.CLIENT_SECRET_POST, ClientAuthenticationMethod.CLIENT_SECRET_BASIC)))
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.redirectUris(strings -> strings.addAll(Set.of("http://localhost:9090/home.html", "http://localhost:9090/login/oauth2/code/oauth")))
.tokenSettings(TokenSettings.builder()
.accessTokenTimeToLive(Duration.ofMinutes(60))
.accessTokenFormat(OAuth2TokenFormat.SELF_CONTAINED)
.refreshTokenTimeToLive(Duration.ofSeconds(43200))
.idTokenSignatureAlgorithm(SignatureAlgorithm.RS256)
.reuseRefreshTokens(false)
.build())
.scope(OidcScopes.OPENID)
.clientSettings(ClientSettings.builder()
.requireAuthorizationConsent(false)
.requireProofKey(true)
.setting("subsystem", "13900")
.setting("client.allowed.resources", aud)
.build())
.postLogoutRedirectUri("http://localhost:9090")
.build();
return new InMemoryRegisteredClientRepository(registeredClient);
}
@Bean
public OAuth2TokenCustomizer<JwtEncodingContext> oAuth2TokenCustomizer() {
return context -> {
if (context.getTokenType() == OAuth2TokenType.ACCESS_TOKEN && context.getPrincipal() instanceof UsernamePasswordAuthenticationToken authenticationToken) {
RegisteredClient registeredClient = context.getRegisteredClient();
Set<String> authorities = authenticationToken.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toSet());
context.getClaims().claims(claims -> {
List<String> updateAUD = new ArrayList<>();
updateAUD.addAll((Collection<? extends String>) claims.get(JwtClaimNames.AUD));
updateAUD.addAll(registeredClient.getClientSettings().getSetting("client.allowed.resources"));
claims.put(JwtClaimNames.AUD, updateAUD);
if (!authorities.isEmpty()) {
claims.put("roles", authorities);
}
});
}
if (OidcParameterNames.ID_TOKEN.equals(context.getTokenType().getValue())) {
UsernamePasswordAuthenticationToken authenticationToken = context.getPrincipal();
User user = (User) authenticationToken.getPrincipal();
JwtClaimsSet.Builder claims = context.getClaims();
claims.claim("user", user);
claims.expiresAt(Instant.now().plusSeconds(3600));
}
};
}
@Bean
public JWKSource<SecurityContext> jwkSource() throws GeneralSecurityException, IOException {
RSAKey rsaKey = generateRsa();
JWKSet jwkSet = new JWKSet(rsaKey);
return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
}
public RSAKey generateRsa() {
KeyPair keyPair = this.generateRsaKey();
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
// @formatter:off
return new RSAKey.Builder(publicKey)
.privateKey(privateKey)
.keyID(UUID.randomUUID().toString())
.build();
// @formatter:on
}
private KeyPair generateRsaKey() {
KeyPair keyPair;
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
keyPair = keyPairGenerator.generateKeyPair();
} catch (Exception ex) {
throw new IllegalStateException(ex);
}
return keyPair;
}
@Bean
public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
}
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
@Bean
public AuthorizationServerSettings providerSettings() {
return AuthorizationServerSettings.builder()
.authorizationEndpoint("/oauth2/v1/authorize")
.tokenEndpoint("/oauth2/v1/token")
.tokenIntrospectionEndpoint("/oauth2/v1/introspect")
.deviceAuthorizationEndpoint("/oauth2/v1/device_authorization")
.tokenRevocationEndpoint("/oauth2/v1/revoke")
.jwkSetEndpoint("/oauth2/v1/jwks")
.oidcUserInfoEndpoint("/connect/v1/userinfo")
.oidcLogoutEndpoint("/connect/v1/logout")
.setting("settings.authorization-server.change-password-endpoint", "/oauth2/v1/change")
.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.setMaxAge(Duration.ofMinutes(60));
corsConfiguration.setAllowedOrigins(List.of("http://localhost:9000"));
corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedMethod("*");
corsConfiguration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", corsConfiguration);
return source;
}
}
Аnd here ere is my SCG application.yml:
server:
port: 9090
spring:
application:
name: gateway
cloud:
gateway:
routes:
- id: backend
predicates:
- Path=/api/book/**
uri: http://localhost:8080/api/book
filters:
- TokenRelay
- SaveSession
- id: change-password
uri: http://localhost:9000/oauth2/v1/change-password
predicates:
- Path=/oauth2/v1/change-password
filters:
- AddRequestParameter=client_id, ${spring.security.oauth2.client.registration.oauth.client-id}
- AddRequestParameter=redirect_uri, http://localhost:9090/home.html
- TokenRelay
- id: static
uri: http://localhost:5500
predicates:
- Path=/**
security:
oauth2:
client:
provider:
oauth:
authorization-uri: http://localhost:9000/oauth2/v1/authorize
issuer-uri: http://localhost:9000
jwk-set-uri: http://localhost:9000/oauth2/v1/jwks
token-uri: http://localhost:9000/oauth2/v1/token
user-info-uri: http://localhost:9000/connect/v1/userinfo
registration:
oauth:
authorization-grant-type: authorization_code
client-authentication-method: client_secret_basic
client-id: aca392b5-e4a7-43fa-aa94-3c6f5b5211f1
client-secret: 123456
redirect-uri: '{baseUrl}/login/oauth2/code/{registrationId}'
scope: openid
threads:
virtual:
enabled: true
Set-Cookie
header. This header instructs the browser (NOT the gateway) to set the cookie. So it would be the browser that fails to replace the cookie, if that is indeed the case. It could also be that you are not providing the correct domain for the refresh token cookies which is why these don't get correctly set / sent by the browser. Since you have not provided any code (and also request / response for the refresh request) can't help you any further.