エラーメッセージ
unexpected "iss" claim value
jose (JavaScript) — JWTClaimValidationFailed, code ERR_JWT_CLAIM_VALIDATION_FAILED
このエラーの意味
トークンは暗号的な検証には成功しましたが、`iss` クレームが `jwtVerify` に渡した `issuer` オプションと一致しません。jose は文字列を完全一致で比較するため(許可する発行者の配列も指定できます)、末尾のスラッシュ、テナントのパスの違い、別の環境など、どんな違いがあっても確認に失敗します。発行者を固定することで、別の ID プロバイダーや、同じプロバイダーの別テナントのトークンが受け入れられるのを防げます。
発生する理由
末尾のスラッシュやスキームが異なる
よくあるプロバイダーによって扱いが異なります。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/` は別の発行者です。
テナント、レルム、認可サーバーの誤り
よくあるマルチテナントのプロバイダーは、発行者にテナントを含めます。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 のエンドポイントを取り違えると)、このエラーになります。
本番環境の検証者に届いたステージングのトークン
ときどきステージング用や開発者用のテナントから発行されたトークンには、そのテナントの発行者が入っています。環境間で設定をコピーすると、検証者とトークンの発行元が食い違っていきます。
正当な発行者が複数あるのに 1 つしか設定していない
ときどき複数の ID プロバイダーからトークンを受け付けるサービスで、最初の 1 つしか設定していないことがあります。修正方法は、発行者ごとに 1 つの鍵セットを対応させた許可リストを意図的に用意することであり、`issuer` オプションを削除することではありません。
修正方法
- 1.
ディスカバリドキュメントから発行者の文字列を取得する
`/.well-known/openid-configuration` の `issuer` の値は、仕様上、プロバイダーが `iss` に書き込む値とまったく同じです。これを 1 バイトも変えずに設定にコピーしてください。
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 と検証者で共有する 1 つの定数にまとめてください。
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 });