Saltar al contenido principal

Cierre de sesión iniciado por RP y cierre de sesión SSO en OpenID Connect: cómo Casdoor cierra la sesión de los usuarios

· 13 min de lectura
Yang Luo
Casdoor Maintainer

Cerrar la sesión de un usuario en una aplicación OpenID Connect requiere más que eliminar tu propia cookie de sesión. El usuario sigue conectado en el proveedor de identidad, por lo que el siguiente clic en "Sign in" pasa directamente sin contraseña, y las otras aplicaciones que comparten esa sesión del proveedor de identidad también permanecen conectadas. Un cierre de sesión adecuado tiene tres partes: limpiar la sesión local de tu aplicación, enviar el navegador al end_session_endpoint del proveedor (cierre de sesión iniciado por RP) para que finalice la sesión del proveedor, y permitir que el proveedor informe a las otras aplicaciones mediante el cierre de sesión por canal posterior. Con Casdoor, el endpoint de fin de sesión es /api/logout, el cierre de sesión por canal posterior se configura por aplicación y los tokens de actualización pueden revocarse en /api/login/oauth/revoke.

Mecanismos de cierre de sesión de un vistazo​

MecanismoQuién lo desencadenaQué limpiaSoporte de Casdoor
Cierre de sesión local de la appTu appSolo la cookie de sesión de tu app y los tokens almacenadosTu código
Cierre de sesión iniciado por RPTu app, redirigiendo el navegadorLa sesión del navegador de Casdoor y el token de id_token_hintSí, GET o POST /api/logout, anunciado como end_session_endpoint
Cierre de sesión por canal posteriorCasdoor, de servidor a servidorLas sesiones de otras apps, cada app finaliza la suyaSí, por aplicación Backchannel logout URL
Cierre de sesión por canal frontalEl proveedor, mediante iframes ocultosLas sesiones de otras apps, a través del navegadorNo, Casdoor no anuncia frontchannel_logout_supported
Revocación de tokens (RFC 7009)Tu app, servidor o clienteUn token de acceso o de actualización, y los tokens que comparten su token de actualizaciónSí, POST /api/login/oauth/revoke, anunciado como revocation_endpoint
Cierre de sesión SSO de CasdoorTu app, con el token o cookie del usuarioTodas las sesiones y tokens del usuario, o solo la sesión actualSí, /api/sso-logout, específico de Casdoor
Cierre de sesión único SAMLCasdoor o un proveedor de servicios SAMLSesiones en apps SAMLSí, LogoutRequest firmado

Sesión local vs. sesión del proveedor de identidad​

Hay al menos dos sesiones involucradas en cada inicio de sesión OIDC, y a menudo tres:

  1. La sesión de tu app. Generalmente una cookie que mapea al usuario y los tokens que obtuviste del endpoint de tokens.
  2. La sesión de Casdoor. Una cookie en el dominio de Casdoor. Mientras sea válida, Casdoor inicia sesión al usuario en cualquier aplicación de la organización sin contraseña, después de un selector de cuentas con un clic o silenciosamente con inicio de sesión automático. Eso es lo que hace funcionar el inicio de sesión único.
  3. Una sesión de un proveedor de identidad upstream, cuando el usuario inició sesión en Casdoor a través de Google, GitHub, un IdP empresarial, etc.

Auth0 describe las mismas tres capas en su documentación sobre el cierre de sesión (capas de aplicación, Auth0 y proveedor de identidad), y el modelo se aplica a cualquier proveedor OIDC.

"Terminación local de la sesión de la aplicación" finaliza solo la primera: su aplicación olvida al usuario, pero la sesión de Casdoor continúa. Esto es lo que ocurre cuando simplemente borra una cookie. "Cierre de sesión centralizado de inicio de sesión único" finaliza la sesión de Casdoor e indica a todas las aplicaciones del usuario que finalicen las suyas propias. La mayoría de los usuarios que hacen clic en "Cerrar sesión" esperan el segundo comportamiento, especialmente en un equipo compartido.

Para la tercera capa, Casdoor llama a la URL de cierre de sesión de un proveedor OAuth personalizado cuando la aplicación inicia sesión a través de uno y el proveedor tiene configurada una URL de cierre de sesión. No cierra la sesión del usuario en Google o GitHub.

Cierre de sesión iniciado por RP con el endpoint de fin de sesión​

OpenID Connect RP-Initiated Logout 1.0 define cómo una parte confiada (su aplicación) solicita al proveedor que finalice su sesión. El proveedor publica el endpoint como end_session_endpoint en su documento de descubrimiento, y la solicitud incluye estos parámetros:

  • id_token_hint: el token de ID emitido por el proveedor a su aplicación. La especificación lo marca como RECOMENDADO. Casdoor lo utiliza para encontrar y expirar ese token exacto.
  • post_logout_redirect_uri: adónde va el navegador después. La especificación requiere que esté registrada previamente. Casdoor lo verifica contra los Redirect URLs de la aplicación y se niega a redirigir a cualquier otra cosa.
  • state: un valor opaco que Casdoor añade a la redirección, para que su aplicación pueda distinguir su propio ciclo de cierre de sesión de uno forjado.
  • client_id: identifica la aplicación cuando no puede enviar un token de ID.

La especificación también define logout_hint y ui_locales. Casdoor no los lee.

El documento de descubrimiento de Casdoor en /.well-known/openid-configuration anuncia end_session_endpoint como https://<your-casdoor>/api/logout, por lo que las bibliotecas OIDC que admiten el cierre de sesión iniciado por RP lo detectan sin configuración. El endpoint acepta GET y POST, según lo requiere la especificación.

Lo que Casdoor hace con la solicitud, brevemente (la referencia de cierre de sesión iniciado por RP tiene cada rama):

  • Con id_token_hint, Casdoor busca el token, envía tokens de cierre de sesión por canal posterior a las aplicaciones del usuario, expira el token (su token de actualización deja de funcionar también), borra la sesión de Casdoor del navegador y redirige a post_logout_redirect_uri con state.
  • Sin id_token_hint, Casdoor cierra la sesión actual del navegador y expira los tokens emitidos en ella. Si el navegador no tiene ninguna sesión restante, aún así redirige si conoce la aplicación, ya sea desde la sesión o desde client_id.
  • Sin post_logout_redirect_uri, el endpoint devuelve una respuesta JSON en lugar de una redirección.

Ejemplo: botón de cierre de sesión en una aplicación de navegador​

Una aplicación de página única que inició sesión con el flujo de código de autorización y PKCE mantiene el token de ID después del intercambio de tokens. El controlador de cierre de sesión limpia primero el estado local, luego entrega el navegador a Casdoor:

const ISSUER = "https://door.example.com";
const CLIENT_ID = "your-client-id";

async function signOut() {
const idToken = sessionStorage.getItem("id_token");

const discovery = await fetch(`${ISSUER}/.well-known/openid-configuration`)
.then((r) => r.json());

// 1. Forget the user locally, whatever happens next
sessionStorage.clear();

// 2. Remember a state value to check when Casdoor sends the browser back
const state = crypto.randomUUID();
sessionStorage.setItem("logout_state", state);

// 3. End the Casdoor session
const url = new URL(discovery.end_session_endpoint);
if (idToken) {
url.searchParams.set("id_token_hint", idToken);
} else {
url.searchParams.set("client_id", CLIENT_ID);
}
url.searchParams.set("post_logout_redirect_uri", "https://app.example.com/signed-out");
url.searchParams.set("state", state);
window.location.assign(url.toString());
}

// On https://app.example.com/signed-out
const returned = new URLSearchParams(window.location.search).get("state");
if (returned !== sessionStorage.getItem("logout_state")) {
console.warn("Unexpected logout redirect");
}
sessionStorage.removeItem("logout_state");

Agregue https://app.example.com/signed-out a los Redirect URLs de la aplicación en Casdoor, o Casdoor devolverá un error en lugar de redirigir. Una aplicación renderizada en servidor hace lo mismo desde su ruta de cierre de sesión: destruye la sesión del servidor, luego responde con un 302 a la URL de fin de sesión. Para Spring Security, vea la publicación de SSO con Spring Boot.

Cierre de sesión por canal posterior: informar a las otras aplicaciones​

Finalizar la sesión de Casdoor detiene nuevos inicios de sesión silenciosos, pero la aplicación B todavía tiene su propia cookie de sesión de hace una hora. Algo debe informar a la aplicación B. OpenID Connect Back-Channel Logout 1.0 hace esto de servidor a servidor: el proveedor envía un POST con un parámetro de formulario logout_token, un JWT firmado que nombra al usuario, a la URI de canal posterior registrada de cada aplicación.

En Casdoor, configure Backchannel logout URL en la página de edición de la aplicación. Cuando un usuario cierra sesión a través de /api/logout, Casdoor envía un token de cierre de sesión a cada aplicación que posee un token activo para ese usuario y tiene esta URL configurada. El token está firmado con el certificado de la aplicación, la misma clave que sus tokens de ID, y contiene:

  • iss y aud: el mismo emisor y client ID que los tokens de ID de la aplicación
  • sub: el ID del usuario, el mismo valor que sub en el token de ID
  • iat y jti
  • events con el miembro http://schemas.openid.net/event/backchannel-logout
  • sid solo para algunos cierres de sesión, como cierre de sesión SSO, y no para /api/logout

Casdoor's ID tokens don't carry a sid claim, so key your local sessions by sub and end all of them for that user. The logout token expires two minutes after iat, so a validator that requires exp, as the spec does, accepts it. Casdoor sends each token once, with a 30-second timeout, and doesn't retry. For applications outside the built-in organization, it refuses to call loopback and link-local addresses, so http://localhost won't receive logout tokens during development.

Ejemplo: un receptor de cierre de sesión por canal posterior​

Este controlador Express sigue los pasos de validación de la especificación, utilizando la biblioteca jose:

import express from "express";
import { createRemoteJWKSet, jwtVerify } from "jose";

// Take both values from the discovery document of your Casdoor
const ISSUER = "https://door.example.com";
const JWKS = createRemoteJWKSet(new URL(`${ISSUER}/.well-known/jwks`));
const CLIENT_ID = process.env.CASDOOR_CLIENT_ID;
const EVENT = "http://schemas.openid.net/event/backchannel-logout";

const seenJti = new Map();
const app = express();

app.post(
"/oidc/backchannel-logout",
express.urlencoded({ extended: false }),
async (req, res) => {
res.set("Cache-Control", "no-store");
try {
const { payload } = await jwtVerify(req.body.logout_token, JWKS, {
issuer: ISSUER,
audience: CLIENT_ID,
maxTokenAge: "2m",
});

if (!payload.events || typeof payload.events[EVENT] !== "object") {
throw new Error("not a logout token");
}
if (!payload.sub && !payload.sid) {
throw new Error("no sub or sid");
}
if ("nonce" in payload) {
throw new Error("nonce is not allowed");
}
if (seenJti.has(payload.jti)) {
throw new Error("replayed token");
}
seenJti.set(payload.jti, Date.now());

// Your own helper: delete every session row that belongs to this user
await destroySessionsForUser(payload.sub);
res.sendStatus(200);
} catch (err) {
res.status(400).json({ error: "invalid_request", error_description: err.message });
}
}
);

jwtVerify verifica la firma, iss, aud, y con maxTokenAge la reclamación iat. Para encontrar sesiones por usuario, almacene sub del token de ID en cada registro de sesión al iniciar sesión. Poda seenJti periódicamente, o guárdelo en Redis.

Revocación de tokens de actualización​

Un token de actualización sobrevive a la sesión que lo creó. Si su aplicación conserva uno, un cierre de sesión que nunca lo toca deja una credencial que puede acuñar nuevos tokens de acceso durante días.

La llamada de fin de sesión con id_token_hint ya expira ese token, incluido el token de actualización. Revoque explícitamente cuando el cierre de sesión no pase por el navegador, por ejemplo en una CLI, una aplicación de escritorio o móvil, o cuando el usuario abandone solo su aplicación y mantenga la sesión de Casdoor. Casdoor implementa RFC 7009 en el revocation_endpoint de su documento de descubrimiento:

curl -X POST https://door.example.com/api/login/oauth/revoke \
-u "$CLIENT_ID:$CLIENT_SECRET" \
-d token="$REFRESH_TOKEN" \
-d token_type_hint=refresh_token

Un cliente público que usa PKCE puede enviar client_id en el cuerpo sin un secreto. Revocar un token de actualización también caduca los tokens de acceso emitidos con él. Después, el endpoint de actualización responde invalid_grant y la introspección devuelve "active": false; When Casdoor revokes tokens lista todos los eventos que hacen esto.

Un límite se aplica a cada proveedor que emite tokens de acceso JWT: una API que solo verifica la firma y exp sigue aceptando un token de acceso revocado hasta que caduca. Mantén los tokens de acceso de vida corta, o haz que la API llame a token introspection cuando deba notar un cierre de sesión de inmediato.

Cierre de sesión único entre varias aplicaciones​

Con las piezas anteriores, el cierre de sesión único funciona así cuando el usuario hace clic en "Sign out" en la app A:

  1. La App A borra su sesión y redirige a /api/logout con id_token_hint.
  2. Casdoor envía tokens de cierre de sesión a las URLs back-channel de las otras apps del usuario, caduca el token y termina la sesión de Casdoor en el navegador.
  3. Las Apps B y C reciben el token de cierre de sesión y eliminan sus sesiones para ese sub.
  4. El navegador llega a la página de cierre de sesión de la app A. Abrir la app B ahora muestra la página de inicio de sesión de Casdoor.

Una app sin una URL back-channel permanece iniciada hasta que su propia sesión o token de acceso caducan. Si no puedes añadir un receptor a una app, mantén sus sesiones cortas o haz que valide el token en cada solicitud.

Para "cerrar mi sesión en todas partes" después de perder un portátil, usa el SSO logout endpoint de Casdoor con logoutAll=true: caduca cada token y elimina cada sesión del usuario, y también envía notificaciones firmadas a través de proveedores de notificación a apps que no hablan OIDC. Los administradores pueden terminar sesiones individuales en la Sessions page. Para apps SAML, consulta SAML single logout. Lectura adicional: single logout y SAML single logout en www.casdoor.com.

Referencias​