0

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
10
  • a server sets cookies with a 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.
    – J Asgarov
    Commented Jun 29 at 17:20
  • As I said, as long as the SAS cookie is active, there is no problem, but when it expires, the CCG starts exchanging the refresh token for an access token, and then the id_token is also refreshed. SCG does not save the new id_token. When the user logs out, SCG sends the old id_token and an exception is through in SAS.
    – HDonev
    Commented Jun 29 at 17:45
  • you just ignored my comment and repeated what you said before. Do your requests NOT come from a browser? show me the code where you think SCG saves/doesn't save stuff
    – J Asgarov
    Commented Jun 29 at 17:50
  • SCG is used as middleware between browser and SAS and it keeps all tokens in its own repository and does not provide them to the browser. It only sends its own cookie and the SAS cookie. This is the reason to prefer SCG in the BFF pattern when we use Spring Framework.
    – HDonev
    Commented Jun 29 at 18:01
  • If a SCG session expires, shouldn't it be invalidated and the tokens it contains, including the refresh token, be deleted? To me, SCG should not be able to refresh tokens of an expired session. The real problem stands probably there.
    – ch4mp
    Commented Jun 30 at 16:53

0