I’m using Spring Authorization Server with Keycloak for social login, and it’s set up as OIDC (OpenID Connect). My issue is with the logout endpoint. I haven’t customized any endpoints, just the ID and access tokens.
I follow:
-
doc
-
this question
-
YouTube tutorial
but nothing work with me.
I have four main entities involved:
- Client: A React web app registered as a confidential OIDC client on the Spring Authorization Server.
- Resource Server: A Spring Boot application with secure REST endpoints that uses the Spring Authorization Server as its identity provider.
- Keycloak: Acts as the social login provider.
- Spring Authorization Server: the authorization server for the React/resource server and a confidential client in Keycloak for authentication.
I am using Maven with the following versions in my `pom.xml`:
-
spring boot 3.3.3
-
Spring Authorization Server: 1.3.2
-
OAuth2 Client (Spring Security): 6.3.3
When I test the logout endpoint (/connect/logout) in Postman, it redirects to the login page without logging out. The access token still works with the resource server(I expected a 403 error).
Also when using logout In frontend, it takes me back to the home page without clearing the user session or showing the login page.
I’ve tried to debug this, and I find SecurityContextHolder.getContext().getAuthentication();
returns null, and the principal is ANONYMOUS_AUTHENTICATION
.
What am I missing? Any help would be greatly appreciated!
My configs as follow:
@Configuration
@EnableWebSecurity
@EnableJdbcHttpSession
public class AuthorizationServerConfig {
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authorizationSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
.oidc(Customizer.withDefaults()); // Enable OpenID Connect 1.0
http
.sessionManagement(sessionManagement ->
sessionManagement
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.maximumSessions(1)
.sessionRegistry(sessionRegistry())
)
.exceptionHandling((exceptions) -> exceptions
.defaultAuthenticationEntryPointFor(
new LoginUrlAuthenticationEntryPoint("/oauth2/authorization/keycloak"),
new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
)
)
.oauth2ResourceServer((oauth2) -> oauth2.jwt(Customizer.withDefaults()));
return http.build();
}
@Bean
@Order(2)
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http, ClientRegistrationRepository clientRegistrationRepository)
throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().authenticated()
)
.csrf(csrf -> csrf.ignoringRequestMatchers(
"/userinfo",
"/oauth2/token",
"/oauth2/jwks"
))
.oauth2Login(Customizer.withDefaults())
.logout(logoutConfigurer -> logoutConfigurer
.logoutUrl("/connect/logout")
.clearAuthentication(true)
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID")
.logoutSuccessHandler(oidcLogoutSuccessHandler(clientRegistrationRepository)));
return http.build();
}
@Bean
public JWKSource<SecurityContext> jwkSource() {
KeyPair keyPair = generateRsaKey();
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
RSAKey rsaKey = new RSAKey.Builder(publicKey)
.privateKey(privateKey)
.keyID("2978b2d6-1205-40b5-98df-7e6fbf5f10d0")
// .algorithm(com.nimbusds.jose.JWSAlgorithm.RS256)
.build();
JWKSet jwkSet = new JWKSet(rsaKey);
return new ImmutableJWKSet<>(jwkSet);
}
private static 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 AuthorizationServerSettings authorizationServerSettings() {
return AuthorizationServerSettings.builder()
.multipleIssuersAllowed(true)
.build();
}
@Bean
public OAuth2TokenCustomizer<JwtEncodingContext> idTokenCustomizer() {
return new FederatedIdentityIdTokenCustomizer();
}
@Bean
OAuth2AuthorizationService jdbcOAuth2AuthorizationService(
JdbcOperations jdbcOperations,
RegisteredClientRepository registeredClientRepository) {
return new JdbcOAuth2AuthorizationService(jdbcOperations, registeredClientRepository);
}
@Bean
OAuth2AuthorizationConsentService jdbcOAuth2AuthorizationConsentService(
JdbcOperations jdbcOperations,
RegisteredClientRepository registeredClientRepository) {
return new JdbcOAuth2AuthorizationConsentService(jdbcOperations, registeredClientRepository);
}
@Bean
public SessionRegistry sessionRegistry() {
return new SessionRegistryImpl();
}
@Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
}
private LogoutSuccessHandler oidcLogoutSuccessHandler(ClientRegistrationRepository clientRegistrationRepository) {
OidcClientInitiatedLogoutSuccessHandler oidcClientInitiatedLogoutSuccessHandler = new OidcClientInitiatedLogoutSuccessHandler(clientRegistrationRepository);
oidcClientInitiatedLogoutSuccessHandler.setPostLogoutRedirectUri("/oauth2/authorization/keycloak");
return oidcClientInitiatedLogoutSuccessHandler;
}
}
the client registration bean:
@Bean
public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
RegisteredClient exampleClient = RegisteredClient.withId("2bee2e72-af4t-43ad-tb2q-0b11501b4a15")
.clientId("example-client")
.clientSecret("{noop}password")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST)
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.redirectUri("http://localhost:3000/api/auth/callback/keycloak")
.redirectUri("https://oauth.pstmn.io/v1/callback")
.scope(OidcScopes.OPENID)
.scope(OidcScopes.PROFILE).scope(OidcScopes.EMAIL)
.tokenSettings(TokenSettings.builder().accessTokenTimeToLive(Duration.ofDays(10))
.refreshTokenTimeToLive(Duration.ofDays(8)).reuseRefreshTokens(false)
.accessTokenFormat(OAuth2TokenFormat.SELF_CONTAINED)
.x509CertificateBoundAccessTokens(true)
.build()).build();
JdbcRegisteredClientRepository registeredClientRepository =
new JdbcRegisteredClientRepository(jdbcTemplate);
registeredClientRepository.save(exampleClient);
return registeredClientRepository;
}
id token and access customization:
public final class FederatedIdentityIdTokenCustomizer implements OAuth2TokenCustomizer<JwtEncodingContext> {
private static final Set<String> ID_TOKEN_CLAIMS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
IdTokenClaimNames.ISS,
IdTokenClaimNames.SUB,
IdTokenClaimNames.AUD,
IdTokenClaimNames.EXP,
IdTokenClaimNames.IAT,
IdTokenClaimNames.AUTH_TIME,
IdTokenClaimNames.NONCE,
IdTokenClaimNames.ACR,
IdTokenClaimNames.AMR,
IdTokenClaimNames.AZP,
IdTokenClaimNames.AT_HASH,
IdTokenClaimNames.C_HASH
)));
@Override
public void customize(JwtEncodingContext context) {
if (OidcParameterNames.ID_TOKEN.equals(context.getTokenType().getValue())) {
Map<String, Object> thirdPartyClaims = extractClaims(context.getPrincipal());
context.getClaims().claims(existingClaims -> {
// Remove conflicting claims set by this authorization server
existingClaims.keySet().forEach(thirdPartyClaims::remove);
// Remove standard id_token claims that could cause problems with clients
ID_TOKEN_CLAIMS.forEach(thirdPartyClaims::remove);
// Add all other claims directly to id_token
existingClaims.putAll(thirdPartyClaims);
});
}
if (OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType())) {
Authentication principal = context.getPrincipal();
if (principal.getPrincipal() instanceof OAuth2User) {
OAuth2User oauth2User = (OAuth2User) principal.getPrincipal();
Map<String, Object> attributes = oauth2User.getAttributes();
context.getClaims().claim("email", attributes.get("email"));
context.getClaims().claim("name", attributes.get("name"));
context.getClaims().claim("email_verified", attributes.get("email_verified"));
context.getClaims().claim("family_name", attributes.get("family_name"));
}
}
}
private Map<String, Object> extractClaims(Authentication principal) {
Map<String, Object> claims;
if (principal.getPrincipal() instanceof OidcUser) {
OidcUser oidcUser = (OidcUser) principal.getPrincipal();
OidcIdToken idToken = oidcUser.getIdToken();
claims = idToken.getClaims();
} else if (principal.getPrincipal() instanceof OAuth2User) {
OAuth2User oauth2User = (OAuth2User) principal.getPrincipal();
claims = oauth2User.getAttributes();
} else {
claims = Collections.emptyMap();
}
return new HashMap<>(claims);
}
}
My Attempts and Expectations
I think the issue might be that the session isn’t being saved, since SecurityContextHolder.getContext().getAuthentication();
returns null. I’m using JDBC to store the session, but it still doesn’t delete the session, and the user isn’t logged out.
I’m sorry for the long message, and I really appreciate any help you can provide!
1