Tushar Desai
Tushar Desai

Reputation: 73

Spring Security OAuth2/OIDC RP-initiated logout does not work

I'm trying to implement RP-initiated logout using Spring Security, from Spring Cloud Gateway to Keycloak. My Spring Security configuration is almost identical to that provided in the Spring Security Reference document, reproduced below,

@EnableWebFluxSecurity
public class SCGSecurityConfig {
    
    @Autowired
    private ReactiveClientRegistrationRepository clientRegistrationRepository;

    private ServerLogoutSuccessHandler keycloakLogoutSuccessHandler() {

        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("{baseUrl}");

        return oidcLogoutSuccessHandler;
    }
    
    @Bean
    public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {

        http.authorizeExchange(authorize -> authorize.anyExchange().authenticated())
            .oauth2Login(withDefaults())
            .logout(logout -> logout.logoutSuccessHandler(keycloakLogoutSuccessHandler()));
                    
        //need to disable on gateway, since we have backend services
        http.csrf().disable();

        return http.build();
    }
}

Hitting the /logout endpoint only does a local logout on the gateway. I see no traffic to Keycloak to logout at the OP. Keycloak console shows the user session is still active. Keycloaks Discovery Metadata does show end_session_endpoint as true.

Here's the relevant logging for Spring Security. I don't see any TRACE level logs, only DEBUGs (maybe the code-path is not hitting any TRACE messages?)

04:01:27.027 reactor-http-epoll-2 DEBUG anduril PathPatternParserServerWebExchangeMatcher: Request 'GET /logout' doesn't match 'null /oauth2/authorization/{registrationId}'
04:01:27.027 reactor-http-epoll-2 DEBUG anduril PathPatternParserServerWebExchangeMatcher: Request 'GET /logout' doesn't match 'null /login/oauth2/code/{registrationId}'
04:01:27.027 reactor-http-epoll-2 DEBUG anduril OrServerWebExchangeMatcher: Trying to match using PathMatcherServerWebExchangeMatcher{pattern='/login', method=GET}
04:01:27.027 reactor-http-epoll-2 DEBUG anduril PathPatternParserServerWebExchangeMatcher: Request 'GET /logout' doesn't match 'GET /login'
04:01:27.027 reactor-http-epoll-2 DEBUG anduril OrServerWebExchangeMatcher: No matches found
04:01:27.027 reactor-http-epoll-2 DEBUG anduril OrServerWebExchangeMatcher: Trying to match using PathMatcherServerWebExchangeMatcher{pattern='/logout', method=GET}
04:01:27.027 reactor-http-epoll-2 DEBUG anduril PathPatternParserServerWebExchangeMatcher: Checking match of request : '/logout'; against '/logout'
04:01:27.027 reactor-http-epoll-2 DEBUG anduril OrServerWebExchangeMatcher: matched
04:01:29.029 reactor-http-epoll-2 DEBUG anduril PathPatternParserServerWebExchangeMatcher: Request 'POST /logout' doesn't match 'null /oauth2/authorization/{registrationId}'
04:01:29.029 reactor-http-epoll-2 DEBUG anduril PathPatternParserServerWebExchangeMatcher: Request 'POST /logout' doesn't match 'null /login/oauth2/code/{registrationId}'
04:01:29.029 reactor-http-epoll-2 DEBUG anduril OrServerWebExchangeMatcher: Trying to match using PathMatcherServerWebExchangeMatcher{pattern='/login', method=GET}
04:01:29.029 reactor-http-epoll-2 DEBUG anduril PathPatternParserServerWebExchangeMatcher: Request 'POST /logout' doesn't match 'GET /login'
04:01:29.029 reactor-http-epoll-2 DEBUG anduril OrServerWebExchangeMatcher: No matches found
04:01:29.029 reactor-http-epoll-2 DEBUG anduril OrServerWebExchangeMatcher: Trying to match using PathMatcherServerWebExchangeMatcher{pattern='/logout', method=GET}
04:01:29.029 reactor-http-epoll-2 DEBUG anduril PathPatternParserServerWebExchangeMatcher: Request 'POST /logout' doesn't match 'GET /logout'
04:01:29.029 reactor-http-epoll-2 DEBUG anduril OrServerWebExchangeMatcher: No matches found
04:01:29.029 reactor-http-epoll-2 DEBUG anduril OrServerWebExchangeMatcher: Trying to match using PathMatcherServerWebExchangeMatcher{pattern='/logout', method=POST}
04:01:29.029 reactor-http-epoll-2 DEBUG anduril PathPatternParserServerWebExchangeMatcher: Checking match of request : '/logout'; against '/logout'
04:01:29.029 reactor-http-epoll-2 DEBUG anduril OrServerWebExchangeMatcher: matched
04:01:29.029 reactor-http-epoll-2 DEBUG anduril WebSessionServerSecurityContextRepository: Found SecurityContext 'SecurityContextImpl [Authentication=OAuth2AuthenticationToken [Principal=Name: [[email protected]], Granted Authorities: [[ROLE_USER, SCOPE_email, SCOPE_profile]], User Attributes: [{sub=400d12ab-dd15-47ab-b023-35a046e28a75, email_verified=true, name=John Doe, [email protected], given_name=John, family_name=Doe, [email protected]}], Credentials=[PROTECTED], Authenticated=true, Details=null, Granted Authorities=[ROLE_USER, SCOPE_email, SCOPE_profile]]]' in WebSession: 'org.springframework.web.server.session.InMemoryWebSessionStore$InMemoryWebSession@142a2883'
04:01:29.029 reactor-http-epoll-2 DEBUG anduril LogoutWebFilter: Logging out user 'OAuth2AuthenticationToken [Principal=Name: [[email protected]], Granted Authorities: [[ROLE_USER, SCOPE_email, SCOPE_profile]], User Attributes: [{sub=400d12ab-dd15-47ab-b023-35a046e28a75, email_verified=true, name=John Doe, [email protected], given_name=John, family_name=Doe, [email protected]}], Credentials=[PROTECTED], Authenticated=true, Details=null, Granted Authorities=[ROLE_USER, SCOPE_email, SCOPE_profile]]' and transferring to logout destination
04:01:29.029 reactor-http-epoll-2 DEBUG anduril WebSessionServerSecurityContextRepository: Removed SecurityContext stored in WebSession: 'org.springframework.web.server.session.InMemoryWebSessionStore$InMemoryWebSession@142a2883'
04:01:29.029 reactor-http-epoll-2 DEBUG anduril DefaultServerRedirectStrategy: Redirecting to '/login?logout'
04:01:29.029 reactor-http-epoll-2 DEBUG anduril PathPatternParserServerWebExchangeMatcher: Request 'GET /login' doesn't match 'null /oauth2/authorization/{registrationId}'
04:01:29.029 reactor-http-epoll-2 DEBUG anduril PathPatternParserServerWebExchangeMatcher: Request 'GET /login' doesn't match 'null /login/oauth2/code/{registrationId}'
04:01:29.029 reactor-http-epoll-2 DEBUG anduril OrServerWebExchangeMatcher: Trying to match using PathMatcherServerWebExchangeMatcher{pattern='/login', method=GET}
04:01:29.029 reactor-http-epoll-2 DEBUG anduril PathPatternParserServerWebExchangeMatcher: Checking match of request : '/login'; against '/login'
04:01:29.029 reactor-http-epoll-2 DEBUG anduril OrServerWebExchangeMatcher: matched

Here's a redacted verion of application.yml,

logging.level:
  reactor.netty: INFO
  org.springframework.cloud.gateway: INFO
  org.springframework.security: TRACE
  org.springframework.web.FilterChainProxy: INFO
  org.springframework.web.reactive.function.client: INFO
spring.cloud.gateway:
  httpclient:
    wiretap: true
  httpserver:
    wiretap: true
  default-filters:
  - name: BasicAuthFilter
  routes:
    - id: adminservice
      uri: http://${ADMIN_SERVICE}/
      predicates:
        - Path=/admin/**
    - id: appservice
      uri: http://${APP_SERVICE}/
      predicates:
        - Path=/app/**
spring.security.oauth2.client:
  provider:
    keycloak:
      issuer-uri: http://${AUTHSERVER}/auth/realms/${REALM}
      user-name-attribute: preferred_username
  registration:
    keycloak-registration:
      provider: keycloak
      client-id: ${CLIENT_ID}
      client-secret: ${CLIENT_SECRET}
      authorization-grant-type: authorization_code
      redirect-uri: "{baseUrl}/login/oauth2/code/keycloak"

Upvotes: 2

Views: 1986

Answers (1)

Robert Strauch
Robert Strauch

Reputation: 12876

Could you try to update your application.yml and set an explicit scope: openid (and any other scopes your client requires) like this?

spring.security.oauth2.client:
  provider:
    keycloak:
      issuer-uri: http://${AUTHSERVER}/auth/realms/${REALM}
      user-name-attribute: preferred_username
  registration:
    keycloak-registration:
      provider: keycloak
      client-id: ${CLIENT_ID}
      client-secret: ${CLIENT_SECRET}
      scope: openid
      authorization-grant-type: authorization_code
      redirect-uri: "{baseUrl}/login/oauth2/code/keycloak"

There is this issue in Spring Security which deals with exactly this problem. Please see also my comment to this issue.

My understanding of the issue is the following:

  • The scope parameter is OPTIONAL with regards to the OAuth2 spec. This is why Spring Security allows it to be empty/null and doesn't complain.
  • However, the oauth2Login() and logout() features, e.g. OidcClientInitiatedServerLogoutSuccessHandler, require the scope parameter to be at least openid.

I'm not sure yet if my understanding is correct.

Upvotes: 3

Related Questions