Mensaje de error
unexpected "iss" claim value
jose (JavaScript) — JWTClaimValidationFailed, code ERR_JWT_CLAIM_VALIDATION_FAILED
Qué significa
El token superó la verificación criptográfica, pero su claim (declaración) `iss` no es igual a la opción `issuer` que pasas a `jwtVerify`. jose compara las cadenas de forma exacta (también acepta un array de emisores permitidos), así que cualquier diferencia —una barra final, otra ruta de inquilino, otro entorno— hace fallar la comprobación. La fijación del emisor es lo que impide que se acepte un token de otro proveedor de identidad o de otro inquilino del mismo proveedor.
Por qué ocurre
La barra final o el esquema no coinciden
frecuenteLos proveedores no son coherentes entre sí: Auth0 emite `https://tenant.auth0.com/` con barra final, Keycloak y Entra ID v2.0 (`…/v2.0`) emiten URL sin ella, y los tokens v1.0 de Entra ID llevan `https://sts.windows.net/<tenant>/` con barra. Para una comparación exacta, `https://login.example.com` y `https://login.example.com/` son emisores distintos.
Inquilino, realm o servidor de autorización incorrectos
frecuenteLos proveedores multiinquilino incluyen el inquilino en el emisor: Entra ID `https://login.microsoftonline.com/<tenant-id>/v2.0`, Keycloak `https://host/realms/<realm>`, Okta `https://<org>.okta.com/oauth2/<server-id>`. Verificar contra el servidor predeterminado mientras los tokens vienen de uno personalizado (o confundir el endpoint v1.0 de Entra con el v2.0) produce este error.
Token de preproducción en un verificador de producción
ocasionalLos tokens de un inquilino de preproducción o de desarrollo llevan el emisor de ese inquilino. Cuando la configuración se copia de un entorno a otro, el verificador y el origen de los tokens se desincronizan.
Varios emisores legítimos, uno solo configurado
ocasionalLos servicios que aceptan tokens de más de un proveedor de identidad a veces configuran solo el primero. La solución es una lista de permitidos deliberada con un conjunto de claves por emisor, no eliminar la opción `issuer`.
Cómo solucionarlo
- 1.
Tomar la cadena del emisor del documento de descubrimiento
El valor `issuer` de `/.well-known/openid-configuration` es, por especificación, exactamente lo que el proveedor escribe en `iss`. Cópialo byte a byte en tu configuración.
bash # The issuer string is defined by the provider's discovery document. curl -s https://login.example.com/.well-known/openid-configuration | jq -r .issuer - 2.
Fijar ese valor exacto en jwtVerify
Mantén la opción `issuer` —sin ella, jose no comprueba `iss` en absoluto— y guárdala en una única constante que compartan la URL del JWKS y el verificador.
js import { createRemoteJWKSet, jwtVerify } from "jose"; const ISSUER = "https://login.example.com/"; // exactly as in "issuer" from discovery const jwks = createRemoteJWKSet(new URL("https://login.example.com/.well-known/jwks.json")); const { payload } = await jwtVerify(token, jwks, { algorithms: ["ES256"], issuer: ISSUER }); - 3.
Aceptar varios emisores, cada uno con su conjunto de claves
Si tu servicio confía realmente en más de un emisor, selecciona el conjunto de claves según el `iss` (aún sin verificar) del token a partir de una lista de permitidos explícita y, después, verifica con ese emisor fijado. Nunca dejes que las claves de un emisor verifiquen los tokens de otro.
js import { createRemoteJWKSet, decodeJwt, jwtVerify } from "jose"; // Each trusted issuer gets its own key set; an unknown iss is rejected before any key is used. const trusted = new Map([ ["https://login.example.com/", createRemoteJWKSet(new URL("https://login.example.com/.well-known/jwks.json"))], ["https://partner.example.org", createRemoteJWKSet(new URL("https://partner.example.org/jwks"))], ]); const { iss } = decodeJwt(token); const keys = trusted.get(iss); if (!keys) throw new Error("untrusted issuer"); await jwtVerify(token, keys, { algorithms: ["ES256", "RS256"], issuer: iss });