Cuando un usuario cierra sesión en su OpenID Provider (Google, Keycloak, Entra ID), las aplicaciones que dependen de él deberían enterarse y cerrar también su sesión local. Sin esa propagación, una persona puede creer que ha hecho logout cuando en realidad sigue autenticada en una decena de portales internos.
OpenID Connect Back-Channel Logout (especificación de 2022) resuelve este problema con un flujo server-to-server: el provider notifica directamente al backend de cada Relying Party sin pasar por el navegador.
Diferencia con Front-Channel Logout
OIDC define dos especificaciones de logout:
- Front-Channel Logout: el provider devuelve una página con
<iframe>a cada RP. El navegador del usuario carga esos iframes y cada RP destruye la cookie del lado cliente. Depende de cookies de terceros, cada vez más bloqueadas por los navegadores. - Back-Channel Logout: el provider hace un
POSTdirecto a un endpoint de cada RP con unlogout_tokenJWT. No depende de cookies ni del navegador. Es la opción moderna y recomendada.
sequenceDiagram
participant U as Usuario
participant OP as OpenID Provider
participant RP1 as App 1 (RP)
participant RP2 as App 2 (RP)
U->>OP: GET /logout
OP->>OP: Identificar sesiones afectadas
par Notificar RPs
OP->>RP1: POST /logout/connect/back-channel/{id}<br>logout_token=eyJ...
OP->>RP2: POST /logout/connect/back-channel/{id}<br>logout_token=eyJ...
end
RP1->>RP1: Invalidar SecurityContext
RP2->>RP2: Invalidar SecurityContext
OP->>U: Logout confirmado
Back-Channel Logout es completamente invisible para el usuario. Cuando vuelva a entrar a cualquier RP, será redirigido al login del OP.
El logout_token
El logout_token es un JWT firmado por el OP con un conjunto reducido de claims:
{
"iss": "https://auth.demo.local",
"aud": "spring-rp",
"iat": 1736006400,
"jti": "logout-12345",
"events": {
"http://schemas.openid.net/event/backchannel-logout": {}
},
"sub": "user-789",
"sid": "session-abc"
}
eventsdebe contener exactamente la URLhttp://schemas.openid.net/event/backchannel-logout.subosid(al menos uno) identifican qué sesión cerrar.nonceestá prohibido (a diferencia del id_token).
Activar Back-Channel Logout en Spring
Spring Security 7 soporta back-channel logout out-of-the-box. La configuración mínima es declarativa.
@Bean
SecurityFilterChain http(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/login/**", "/logout/**").permitAll()
.anyRequest().authenticated())
.oauth2Login(Customizer.withDefaults())
.oidcLogout(oidc -> oidc.backChannel(Customizer.withDefaults()));
return http.build();
}
oidcLogout().backChannel() registra el OidcBackChannelLogoutFilter que escucha en /logout/connect/back-channel/{registrationId}.
spring:
security:
oauth2:
client:
registration:
empresa:
client-id: spring-rp
client-secret: ${RP_SECRET}
scope: openid, profile
provider: empresa-auth
provider:
empresa-auth:
issuer-uri: "https://auth.demo.local"
El provider debe configurarse con la URL de back-channel del cliente, p. ej. https://app.demo.local/logout/connect/back-channel/empresa.
Validación del logout_token
Spring valida automáticamente:
- Firma: con el JWK Set del OP.
iss: debe coincidir con elissuer-uriconfigurado.aud: debe contener elclient_iddel RP.iat: presente y no demasiado antiguo.events: presente con la clave canónica.subosid: al menos uno.nonce: ausente (su presencia rechaza el token).
Si cualquier validación falla, devuelve HTTP 400 Bad Request y no toca el SecurityContext.
Mapeo de logout_token a sesión local
Por defecto, Spring busca todas las sesiones HTTP del usuario y las invalida. Esto funciona bien con HttpSession clásica.
@Bean
SessionRegistry sessionRegistry() {
return new SessionRegistryImpl();
}
Para Spring Session con Redis (caso típico en producción distribuida):
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
spring:
session:
store-type: redis
redis:
namespace: rp:session
Spring Session gestiona el SessionRegistry distribuido y la invalidación se propaga a todos los nodos automáticamente.
Sesión por usuario, sesión por sid
Si el OP envía el sid, lo correcto es invalidar solo esa sesión, no todas las del usuario. Esto permite que un usuario con varias pestañas o dispositivos cierre solo una.
Spring asocia el sid a la sesión local en el momento del login y lo guarda como atributo. Cuando llega un back-channel logout con sid, busca y mata solo esa.
HttpSession httpSession = request.getSession(false);
if (httpSession != null) {
String storedSid = (String) httpSession.getAttribute("oidc.sid");
if (storedSid.equals(claimSid)) {
httpSession.invalidate();
}
}
Logout iniciado por el RP (RP-Initiated Logout)
Complementario al back-channel: el usuario clica "Cerrar sesión" en la app, esta destruye su sesión local y redirige al end_session_endpoint del OP para que también cierre la sesión central.
.oauth2Login(Customizer.withDefaults())
.logout(logout -> logout
.logoutUrl("/auth/logout")
.logoutSuccessHandler(oidcLogoutSuccessHandler()))
@Bean
LogoutSuccessHandler oidcLogoutSuccessHandler() {
OidcClientInitiatedLogoutSuccessHandler handler =
new OidcClientInitiatedLogoutSuccessHandler(clientRegistrationRepository);
handler.setPostLogoutRedirectUri("{baseUrl}/");
return handler;
}
Combinar RP-Initiated y Back-Channel da la mejor experiencia: el usuario puede iniciar logout desde cualquier app y el resto de RPs se enteran sin acción del usuario.
Configuración en proveedores comunes
Keycloak
En la sección Settings del cliente, añade https://app.demo.local/logout/connect/back-channel/empresa en Backchannel Logout URL y activa Backchannel Logout Session Required.
Entra ID
En App registrations, Token configuration, añade el atributo opcional sid al ID Token. En Authentication, configura Front-channel logout URL o usa el endpoint de Microsoft Graph para gestionar back-channel.
Auth0
En la pestaña Endpoints del tenant, añade https://app.demo.local/logout/connect/back-channel/empresa en Back-Channel Logout URLs.
Auditoría y observabilidad
Cada back-channel logout debería loguearse con el jti (para detectar replays), el sub y el resultado (éxito o fallo).
@EventListener
public void onLogout(LogoutSuccessEvent event) {
AUDIT.info("BACK_CHANNEL_LOGOUT user={}", event.getAuthentication().getName());
}
Métrica útil para Grafana: oidc_back_channel_logout_total{result="success|failure"}.
Errores frecuentes
- No exponer la URL públicamente: el OP debe poder hacer POST al endpoint. Si está detrás de un firewall interno, el back-channel no llega.
- Aceptar
nonceen el logout_token: lo prohíbe la especificación; un token connoncedebe rechazarse. - Olvidar HTTPS: el back-channel debe ir sobre TLS. En desarrollo conviene usar
mkcerto un dominio real. - No persistir el
sid: sin él, no puedes invalidar selectivamente y cualquier logout cierra todas las sesiones del usuario.
Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Spring Security es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de Spring Security
Explora más contenido relacionado con Spring Security y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
Implementar OpenID Connect Back-Channel Logout en Spring Security 7, validar el logout token JWT enviado por el provider y propagar la invalidación a sesiones distribuidas.