KeycloakLogoutHandler.java
package com.taxonomy.security.keycloak;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Profile;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import org.springframework.web.util.UriComponentsBuilder;
import java.io.IOException;
/**
* RP-Initiated Logout: after Spring session invalidation, redirect the user
* to Keycloak's logout endpoint so the SSO session is terminated across all
* applications.
* <p>
* Uses {@link ServletUriComponentsBuilder} to correctly derive the
* post-logout redirect URI from the current request, which respects
* {@code X-Forwarded-*} / {@code Forwarded} headers when
* {@code server.forward-headers-strategy=framework} is enabled.
* <p>
* Keycloak end_session_endpoint:
* <pre>
* {issuer}/protocol/openid-connect/logout?
* id_token_hint={id_token}&
* post_logout_redirect_uri={app_url}
* </pre>
*/
@Component
@Profile("keycloak")
public class KeycloakLogoutHandler implements LogoutSuccessHandler {
@Value("${spring.security.oauth2.client.provider.keycloak.issuer-uri:http://localhost:8180/realms/taxonomy}")
private String issuerUri;
/** Visible for testing — sets the issuer URI. */
void setIssuerUri(String issuerUri) {
this.issuerUri = issuerUri;
}
@Override
public void onLogoutSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
// Invalidate the HTTP session
if (request.getSession(false) != null) {
request.getSession().invalidate();
}
// Build the Keycloak end_session_endpoint URL
String logoutUrl = issuerUri + "/protocol/openid-connect/logout";
UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(logoutUrl);
// Include id_token_hint for Keycloak to identify the session
if (authentication != null && authentication.getPrincipal() instanceof OidcUser oidcUser) {
String idToken = oidcUser.getIdToken().getTokenValue();
builder.queryParam("id_token_hint", idToken);
}
// Derive the post-logout redirect URI from the current request.
// ServletUriComponentsBuilder respects X-Forwarded-* headers behind a
// reverse proxy (when server.forward-headers-strategy=framework is set).
String postLogoutRedirectUri = ServletUriComponentsBuilder.fromRequest(request)
.replacePath(request.getContextPath() + "/")
.replaceQuery(null)
.fragment(null)
.build()
.toUriString();
builder.queryParam("post_logout_redirect_uri", postLogoutRedirectUri);
response.sendRedirect(builder.build().toUriString());
}
}