エラーメッセージ
missing required "aud" claim
別の表記
- missing required "iss" claim
- missing required "exp" claim
- missing required "sub" claim
- missing required "iat" claim
クレーム名は欠けているクレームに応じて変わります(`"aud"`、`"iss"`、`"exp"`、`"sub"`、`"iat"` など)。`err.claim` にその名前が入り、`err.reason` は `"missing"` になります。
jose (JavaScript) — JWTClaimValidationFailed, code ERR_JWT_CLAIM_VALIDATION_FAILED, reason "missing"
このエラーの意味
検証オプションで必須となるクレームが、トークンに含まれていません。jose がクレームを必須として扱うのは、`requiredClaims` に指定した場合だけではありません。そのクレームの確認を求めた場合も同様で、`audience` を指定すると `aud`、`issuer` では `iss`、`subject` では `sub`、`maxTokenAge` では `iat` が必須になります。引用符で囲まれたクレーム名から、どのクレームが欠けているかがわかります。署名はすでに有効と確認されています。
発生する理由
検証オプションがそのクレームを前提としている
よくある`audience: "https://api.example.com"` を設定することは、「トークンはこの API 向けでなければならない」という意味です。`aud` のないトークンはこの条件を満たせないため、jose は黙って通すのではなく、クレームが欠けていると報告します。`issuer`、`subject`、`maxTokenAge` も同様です。
発行者がそのクレームを書き込んでいない
よくある自作のトークンサービスや社内のトークンサービスは、`aud` や `exp` を省略していることがよくあります。`exp` のないトークンは期限切れになりません。`aud` のないトークンは、同じ発行者を信頼するどのサービスに対してもリプレイできてしまいます。どちらも発行側で修正する価値があります。
そのクレームが requiredClaims に含まれている
ときどき`requiredClaims: ["exp", "jti"]` を指定すると、値に関係なく、これらのクレームが必須になります。正当な理由でそのいずれかを持たない種類のトークン(たとえば `exp` のない長期間有効なサービストークン)は、設計どおりここで失敗します。
別の種類のトークンが検証者に届いている
ときどきリフレッシュトークン、ログアウトトークン、プロバイダー固有のトークンも JWT ですが、含まれるクレームの組み合わせが異なります。クレームの構成に見覚えがなければ、誤ったトークンが送られています。
修正方法
- 1.
欠けているクレームと存在するクレームをログに出力する
エラーオブジェクトは `claim`、`reason`、デコード済みの `payload` を公開しています。これらをログに出力すれば、「発行者がそのクレームを書き込んでいない」のか「誤ったトークンが送られた」のかを、1 回のリクエストで判別できます。
js import { errors, jwtVerify } from "jose"; try { await jwtVerify(token, jwks, { algorithms: ["ES256"], audience: "https://api.example.com" }); } catch (err) { if (err instanceof errors.JWTClaimValidationFailed && err.reason === "missing") { console.error("token lacks claim:", err.claim, "claims present:", Object.keys(err.payload)); } throw err; } - 2.
発行者にクレームを含めてもらう
検証者の条件を緩めるより、トークンを修正することを優先してください。JWT を安全に受け入れられるのは、`aud`、`iss`、`exp` があるからこそです。RFC 8725 は、すべてのトークンにこれらを含めることを推奨しています。
js import { SignJWT } from "jose"; const token = await new SignJWT({ scope: "orders:read" }) .setProtectedHeader({ alg: "ES256", kid }) .setIssuer("https://issuer.example.com") .setAudience("https://api.example.com") // the claim the verifier requires .setIssuedAt() .setExpirationTime("10m") .sign(privateKey);