错误信息
unexpected "iss" claim value
jose (JavaScript) — JWTClaimValidationFailed, code ERR_JWT_CLAIM_VALIDATION_FAILED
含义
令牌通过了密码学验证,但其 `iss` 声明不等于你传给 `jwtVerify` 的 `issuer` 选项。jose 按字符串精确比较(也接受由允许的颁发者组成的数组),因此任何差异——末尾斜杠、不同的租户路径、另一个环境——都会导致检查失败。正是固定颁发者,才能阻止来自另一个身份提供商(或同一提供商的另一个租户)的令牌被接受。
原因
末尾斜杠或协议不同
常见各提供商的做法并不一致:Auth0 的颁发者是带斜杠的 `https://tenant.auth0.com/`,Keycloak 和 Entra ID v2.0(`…/v2.0`)的颁发者 URL 不带斜杠,而 Entra ID v1.0 令牌携带的 `https://sts.windows.net/<tenant>/` 又带斜杠。对精确比较来说,`https://login.example.com` 和 `https://login.example.com/` 是两个不同的颁发者。
租户、领域(realm)或授权服务器有误
常见多租户提供商会把租户写进颁发者:Entra ID 是 `https://login.microsoftonline.com/<tenant-id>/v2.0`,Keycloak 是 `https://host/realms/<realm>`,Okta 是 `https://<org>.okta.com/oauth2/<server-id>`。令牌来自自定义授权服务器,验证时却以默认服务器为准(或者混用了 Entra 的 v1.0 和 v2.0 端点),就会出现这个错误。
生产环境的验证方收到了预发布环境的令牌
偶见来自预发布租户或开发租户的令牌携带的是该租户的颁发者。在环境之间复制配置时,验证方和令牌来源就会逐渐对不上。
合法的颁发者有多个,却只配置了一个
偶见接受多个身份提供商令牌的服务,有时只配置了第一个。正确的做法是明确配置一份允许列表,每个颁发者对应一个密钥集,而不是去掉 `issuer` 选项。
修复方法
- 1.
从发现文档中获取颁发者字符串
按照规范,`/.well-known/openid-configuration` 中的 `issuer` 值与提供商写入 `iss` 的值完全相同。把它逐字节复制到你的配置中。
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.
在 jwtVerify 中固定这个确切的值
保留 `issuer` 选项——没有它,jose 根本不会检查 `iss`——并把它放在同一个常量中,供 JWKS URL 和验证方共用。
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.
接受多个颁发者,每个颁发者各用一个密钥集
如果你的服务确实信任多个颁发者,就根据令牌中(尚未验证的)`iss` 从显式的允许列表中选出密钥集,然后在验证时固定该颁发者。切勿让一个颁发者的密钥验证另一个颁发者的令牌。
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 });