Seguridad e identidad
OpenID Connect: ID token, discovery, nonce y gestión de sesiones
Intermedio8 min de lecturaActualizado
Esta página se tradujo del inglés con ayuda de IA y se cotejó con el original. Leer la versión en inglés
OpenID Connect convierte OAuth 2.0, un framework de acceso delegado, en un protocolo de inicio de sesión único al añadir algo que un access token nunca aportó: una afirmación verificable sobre quién ha iniciado sesión y cómo. Esta guía cubre el ID token y sus claims, el documento de discovery y el conjunto de claves, los scopes estándar y UserInfo, contra qué protegen nonce y state, el logout y la gestión de sesiones, y cómo validar un ID token sin atajos.
Lo que aprenderás
- Describir cómo OpenID Connect añade una capa de identidad sobre el flujo authorization code de OAuth 2.0
- Leer un ID token y explicar cada uno de los claims obligatorios que contiene
- Usar el documento de discovery y el endpoint JWKS en lugar de fijar en el código la configuración del proveedor
- Explicar qué impide cada uno de estos elementos: nonce, state y los claims c_hash y at_hash
- Validar correctamente un ID token y saber cuándo usarlo en lugar de un access token
- Elegir entre logout front-channel y back-channel para una sesión federada
OAuth 2.0 da a una aplicación permiso para llamar a una API. No le dice quién es el usuario, cuándo se autenticó ni si sigue ahí: el access token se emitió para una API y puede ser opaco para todos los demás. Aun así, las aplicaciones necesitaban justo esa información, así que durante años dedujeron la identidad llamando con el access token a un endpoint de perfil específico de cada proveedor, un enfoque con un fallo real: un access token obtenido en cualquier sitio, para cualquier cliente, responderá sin problema a esa llamada.
OpenID Connect lo resuelve definiendo un segundo token dirigido al propio cliente. El ID token es un JSON Web Token firmado cuya audiencia es tu aplicación, cuyo emisor es el que tú has fijado y cuyos claims describen el evento de autenticación. En torno a esa única idea, la especificación añade discovery, un conjunto estándar de claims y scopes, un endpoint UserInfo y una semántica de sesión y logout; lo suficiente para construir un inicio de sesión único interoperable entre productos de distintos fabricantes. Esta guía da por hecho que ya conoces el flujo authorization code de OAuth 2.0; todo lo que sigue es lo que OpenID Connect añade encima.
Qué añade el protocolo
Una petición OpenID Connect es una petición de autorización OAuth que incluye el scope openid. Ese único scope pone al proveedor en modo OpenID Connect, y la respuesta del token endpoint incorpora un id_token junto al access token.
GET /authorize?response_type=code
&scope=openid%20profile%20email
&client_id=web-portal
&redirect_uri=https%3A%2F%2Fportal.example.com%2Fcallback
&state=Rm9vYmFy
&nonce=n-0S6_WzA2Mj
&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
&code_challenge_method=S256 HTTP/1.1
Host: sso.example.com
El vocabulario cambia ligeramente: el servidor de autorización pasa a ser el OpenID Provider (OP) y el cliente, la Relying Party (RP). La respuesta del token endpoint resulta familiar, con un único añadido.
{
"access_token": "IiFwYXNzZWQgdG8gdGhlIEFQSSI",
"token_type": "Bearer",
"expires_in": 300,
"refresh_token": "8xLOxBtZp8",
"id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6InNpZy0yMDI2LTAzIn0.eyJpc3MiOi..."
}
Existen tres modos de respuesta. El flujo authorization code es el que hay que usar: todo lo sensible vuelve por el back channel. El flujo implícito y el flujo híbrido devuelven tokens a través del navegador y, en la práctica, han quedado descartados para desarrollos nuevos, por las mismas razones por las que OAuth eliminó el implicit grant.
Dentro del ID token
Decodificar el payload de un ID token —con un decodificador JWT del lado del cliente, sin fiarte nunca, en el código, de un valor sin verificar— revela los claims que concentran todo el significado del protocolo.
{
"iss": "https://sso.example.com",
"sub": "9f2c1b7e-4a55-4f2a-bd58-6f0f1a2c3d44",
"aud": "web-portal",
"exp": 1789012645,
"iat": 1789012345,
"auth_time": 1789012300,
"nonce": "n-0S6_WzA2Mj",
"acr": "urn:mace:incommon:iap:silver",
"amr": ["pwd", "hwk"],
"azp": "web-portal",
"email": "[email protected]",
"email_verified": true,
"name": "Jordan Lee",
"preferred_username": "jordan"
}
iss y sub juntos son el único identificador estable de un usuario. El sujeto es único y nunca se reutiliza dentro de un mismo emisor, así que una cuenta local debe identificarse por ese par, no por una dirección de email que la persona puede cambiar en cualquier momento. aud debe coincidir con tu identificador de cliente; azp indica la parte autorizada cuando un token se emite para varias audiencias. auth_time registra cuándo se autenticó realmente el usuario, no cuándo se emitió el token, y es lo que te permite exigir un inicio de sesión reciente para una operación sensible enviando max_age o prompt=login. acr y amr describen la robustez y los métodos de esa autenticación —pwd para una contraseña, otp para un código de un solo uso, hwk para una llave de hardware— y son la forma en que una relying party puede exigir autenticación multifactor en las pantallas de administración.
Los scopes estándar se corresponden con conjuntos estándar de claims: profile cubre el nombre, la foto, la configuración regional y datos similares; email cubre email y email_verified; address y phone cubren lo que su nombre indica. Pedir solo lo que necesitas mantiene honesta la pantalla de consentimiento y reduce los datos personales que tienes que custodiar.
Discovery y claves
Nada de un proveedor debería estar fijado en el código salvo la URL de su emisor. A partir de ella, el documento de discovery, publicado en una ruta conocida, proporciona todos los endpoints y capacidades.
GET /.well-known/openid-configuration HTTP/1.1
Host: sso.example.com
{
"issuer": "https://sso.example.com",
"authorization_endpoint": "https://sso.example.com/authorize",
"token_endpoint": "https://sso.example.com/token",
"userinfo_endpoint": "https://sso.example.com/userinfo",
"jwks_uri": "https://sso.example.com/.well-known/jwks.json",
"end_session_endpoint": "https://sso.example.com/logout",
"response_types_supported": ["code"],
"id_token_signing_alg_values_supported": ["RS256", "ES256"],
"scopes_supported": ["openid", "profile", "email", "offline_access"],
"code_challenge_methods_supported": ["S256"]
}
El jwks_uri sirve las claves públicas de firma del proveedor como un JSON Web Key Set. Cada clave lleva un kid, y la cabecera del token indica qué kid se usó, de modo que la rotación es transparente: el proveedor publica la nueva clave antes de empezar a firmar con ella, los clientes vuelven a descargar el conjunto cuando encuentran un kid desconocido y la clave antigua se retira cuando han caducado los tokens pendientes. Guarda en caché el conjunto de claves, respeta sus cabeceras de caché y limita la frecuencia de las recargas para que un kid desconocido no pueda usarse para saturar al proveedor.
Nonce, state y los claims de hash
Tres mecanismos distintos protegen frente a tres debilidades distintas, y se confunden a menudo.
state es tuyo. Lo generas, lo guardas en tu sesión y lo comparas cuando el navegador vuelve. Garantiza que el callback pertenece a un flujo iniciado por este navegador, lo que neutraliza el cross-site request forgery en el endpoint de redirección y el login CSRF, en el que un atacante intenta que inicies sesión sin darte cuenta con su cuenta.
nonce lo copia el proveedor dentro del ID token. Al comparar el nonce del token con el valor que guardaste, demuestras que el token se emitió para este intento de autenticación y que no puede reutilizarse desde otro. Es obligatorio en los flujos implícito e híbrido y muy recomendable en el flujo authorization code.
c_hash y at_hash aparecen cuando un código o un access token viaja junto a un ID token por el front channel. Son hashes truncados de esos valores, de modo que la relying party puede confirmar que el código y el token recibidos son los que el proveedor emitió junto con este ID token.
PKCE sigue vigente por encima de todo esto: protege el propio intercambio del código y las buenas prácticas actuales de OAuth lo exigen sea cual sea el tipo de cliente.
Validar un ID token como es debido
La validación es una checklist, y saltarse cualquiera de sus líneas ha sido la causa raíz de vulnerabilidades reales. Verifica la firma con una clave del JWKS del proveedor seleccionada por kid, restringiendo los algoritmos aceptados a los que anuncia el proveedor; comprueba que iss coincide exactamente; comprueba que aud contiene tu identificador de cliente y, si azp está presente, que es igual a él; comprueba exp e iat con un pequeño margen para el desfase de reloj; compara el nonce; y, si lo pediste, verifica auth_time frente a tu max_age.
// nimbus-jose-jwt: validator configured once from the discovery document
IDTokenValidator validator = new IDTokenValidator(
new Issuer("https://sso.example.com"),
new ClientID("web-portal"),
JWSAlgorithm.RS256,
new URL("https://sso.example.com/.well-known/jwks.json"));
try {
IDTokenClaimsSet claims = validator.validate(SignedJWT.parse(idToken), expectedNonce);
String subject = claims.getSubject(); // stable only together with the issuer
Date authenticatedAt = claims.getAuthenticationTime();
if (authenticatedAt.toInstant().isBefore(Instant.now().minus(Duration.ofMinutes(15)))) {
throw new ReauthenticationRequiredException(); // step-up for a sensitive action
}
} catch (BadJOSEException | JOSEException e) {
throw new AuthenticationFailedException("id_token rejected", e);
}
Escribe esto tú mismo solo si no tienes alternativa. Una biblioteca —o el cliente OpenID Connect integrado en Spring Security— implementa la lista completa, incluidas las restricciones de algoritmos y la gestión de la rotación de claves que el código escrito a mano suele omitir. Los escollos propios de verificar cualquier JWT, como la confusión de algoritmos, se tratan en proteger API REST con JWT.
UserInfo, y qué token va a cada sitio
El endpoint UserInfo devuelve claims sobre el usuario autenticado a cambio de un access token válido que lleve el scope openid.
GET /userinfo HTTP/1.1
Host: sso.example.com
Authorization: Bearer IiFwYXNzZWQgdG8gdGhlIEFQSSI
Es útil cuando los claims cambian durante una sesión o cuando mantienes el ID token pequeño a propósito, pero cuesta una llamada de red, y su respuesta debe tener un sub idéntico al del ID token; de lo contrario, se ha producido una sustitución de token. Para todo lo que necesites en cada petición, prefiere los claims del ID token.
Conviene dejar clara la división de responsabilidades, porque equivocarse aquí es el error de diseño más común con OpenID Connect:
| Token | Audiencia | Propósito | Se envía a |
|---|---|---|---|
| ID token | Tu cliente | Quién inició sesión, cómo y cuándo | A ningún sitio: lo consume el cliente |
| Access token | Una API | Qué puede hacer allí el portador | El servidor de recursos |
| Refresh token | El proveedor | Obtener nuevos tokens sin intervención del usuario | Solo el token endpoint |
Sesiones y logout
Un inicio de sesión federado crea al menos dos sesiones: una en el proveedor y otra en cada aplicación. Cerrarlas de forma coherente es la parte del inicio de sesión único que las implementaciones se saltan con más frecuencia.
El logout iniciado por la RP envía al usuario al end_session_endpoint del proveedor con id_token_hint y una post_logout_redirect_uri, lo que cierra la sesión en el proveedor y devuelve al usuario a una página que registraste de antemano.
Con el front-channel logout, el proveedor carga en un iframe oculto una URL de logout por cada aplicación. Es sencillo, pero depende de las cookies de terceros y de que cada iframe cargue correctamente, así que en los navegadores modernos falla sin avisar.
El back-channel logout es la opción fiable. El proveedor envía por POST un logout token firmado —un JWT con un sid o un sub y un claim events que nombra el evento de logout— directamente a cada cliente registrado, de servidor a servidor.
POST /backchannel-logout HTTP/1.1
Host: portal.example.com
Content-Type: application/x-www-form-urlencoded
logout_token=eyJhbGciOiJSUzI1NiIsImtpZCI6InNpZy0yMDI2LTAzIn0...
Valida ese token exactamente igual que un ID token, con la regla adicional de que no debe contener un nonce, y después destruye la sesión local identificada por sid. Como esto exige llevar un registro por sesión, las aplicaciones que dependen solo de cookies stateless tienen que añadir un índice de sesiones en el servidor, un precio razonable por un logout que funcione de verdad. Si te integras con un proveedor empresarial más antiguo, el mecanismo equivalente allí es el single logout de SAML, que se enfrenta a las mismas dificultades de forma más complicada.
Puntos clave
- OpenID Connect es OAuth 2.0 más el scope
openid, un ID token dirigido a tu cliente y claims de identidad estándar. - El ID token te dice quién se autenticó, cuándo y con qué métodos;
issysubjuntos son la única clave de usuario estable. - Lee todos los endpoints y claves del documento de discovery y del JWKS para que la rotación y los cambios del proveedor no requieran un nuevo despliegue.
statevincula la redirección a tu sesión,noncevincula el ID token a este intento de inicio de sesión y PKCE vincula el código a tu cliente: usa los tres.- Valida la firma, el emisor, la audiencia, la caducidad, el nonce y, cuando proceda,
auth_time; usa una biblioteca en lugar de comprobaciones escritas a mano. - Envía los access tokens a las API, mantén los ID tokens en el cliente e implementa el back-channel logout si el cierre de sesión único tiene que ser fiable.
Fuentes y lecturas recomendadas
Preguntas frecuentes
- ¿Debo enviar el ID token a mi API?
- No. El ID token va dirigido a la aplicación cliente —su audiencia es tu identificador de cliente— y existe para decirle a esa aplicación quién ha iniciado sesión. Las API deben aceptar access tokens, cuya audiencia nombra a la API y cuyos scopes describen lo que el llamante puede hacer. Enviar un ID token a una API la obliga a aceptar un token cuyos scopes nunca la contemplaron, lo que rompe la validación de la audiencia.
- ¿Qué diferencia hay entre nonce y state?
- State protege la redirección de vuelta a tu aplicación: lo generas, lo guardas en la sesión y lo comparas al volver, lo que impide que un atacante inicie un flujo que termine en el navegador de una víctima. Nonce protege el propio ID token: el proveedor lo copia dentro del token, así que un token reutilizado desde otro intento de inicio de sesión no supera la comparación. En la práctica, ambos son imprescindibles.
- ¿Puedo fiarme del claim email de cualquier proveedor?
- Solo cuando email_verified es true y confías en que ese emisor concreto lo haya verificado. Vincular cuentas a partir de un email no verificado es una vía conocida de toma de control de cuentas, porque un usuario puede registrar en un proveedor poco riguroso una dirección que no controla. Identifica las cuentas locales por el par emisor y sujeto, y trata el email como un atributo de visualización que puedes actualizar tras verificarlo.
- ¿Cómo sé cuándo ha terminado una sesión federada?
- No puedes deducirlo de la caducidad de los tokens. El back-channel logout es el mecanismo fiable: el proveedor envía por POST un logout token firmado a cada cliente que haya registrado un endpoint de logout, y el cliente destruye la sesión local correspondiente. El front-channel logout usa iframes ocultos; es más fácil de desplegar, pero poco fiable cuando las cookies de terceros o la red están restringidas.