JWT の検証は 1 つのチェックではなく 6 つのチェックからなり、署名はそのうちの 1 つにすぎません。要するに、トークンをどう検証するかを決めるのは、トークンではなく検証者です。アルゴリズムはサーバー側で固定し、鍵はその発行者についてすでに信頼している鍵の中から探し、exp を必須にし、iss と aud は完全一致で比較し、ある種類のトークンが別の種類のトークンの代わりに使われないよう typ をチェックし、許容する時刻のずれは最大 1 分にとどめてください。JWKS はキャッシュし、未知の kid の値による再取得にはレート制限をかけてください。以下では、これらのどれかが欠けたときに何が起きるかを、実際のライブラリで測定した結果とともに説明します。
このページの出力はすべて、この記事のために新しく生成した ES256 鍵で発行したトークンを使ったものです。検証には Node.js 24.22 上の jose 6.2.13 と jsonwebtoken 9.0.3(jwks-rsa 4.1.0 と併用)、Python 3.14.8 上の PyJWT 2.15.1、Go 1.27.3 上の golang-jwt 5.3.1 と keyfunc 3.8.2 を使い、それぞれ公式の Docker イメージで実行しました。
鍵だけを渡したときにライブラリがチェックする項目
JWT のバグの大半は、ライブラリのバグではありません。署名を検証しただけで終わっている呼び出しです。どの発行者、オーディエンス、アルゴリズムを想定しているかを、呼び出し側がライブラリに伝えていないのです。有効な公開鍵を渡し、それ以外のオプションを指定せずに呼び出したとき、各ライブラリが 3 つのトークンをどう扱ったかを次の表に示します(PyJWT は algorithms なしでは動作しないため、["ES256"] を渡しました)。
| トークン | jose jwtVerify | jsonwebtoken verify | PyJWT decode | golang-jwt Parse |
|---|---|---|---|---|
正しい鍵で署名、ただし iss と aud は別のサービスのもの | 受理 | 受理 | 拒否:Invalid audience | 受理 |
正しい鍵で署名、exp クレームなし | 受理 | 受理 | 受理 | 受理 |
alg: "none"、署名が空 | 拒否 | 拒否 | 拒否 | 拒否 |
ここから 2 つの教訓が得られます。1 つ目に、4 つのライブラリはいずれも署名のないトークンをデフォルトで拒否するため、有名な none の問題は、現在では主に自前で書いた検証コードに残っています。2 つ目に、exp のないトークンは 4 つすべてで永久に有効です。さらに 4 つのうち 3 つは、同じ鍵で署名されてさえいれば、別のサービス向けに発行されたトークンも受け入れます。exp、nbf、iss、aud は、RFC 7519 §4.1 では任意のクレームです。ライブラリがこれらをチェックするのは、トークンに含まれていて、かつそのように設定されている場合です。それ以外は自分で対処する必要があります。
間違い 1:トークンにアルゴリズムを選ばせる
alg ヘッダーは、まだ検証していないトークンの中に入って届きます。これを読んで処理を振り分ける検証者は、ルールを送信者に選ばせていることになります。JWT のベストカレントプラクティスである RFC 8725 §3.1 は、はっきりとこう定めています。ライブラリは、サポートするアルゴリズムを呼び出し側が指定できるようにしなければならず、それ以外のアルゴリズムを使ってはいけません。また、各鍵はちょうど 1 つのアルゴリズムでのみ使わなければなりません。
典型的な失敗例が HS/RS(または HS/ES)のアルゴリズム混同です。RFC 8725 §2.1 で説明されており、jsonwebtoken では CVE-2015-9235 として追跡されています。サービスは公開鍵を 1 つ持ち、「ヘッダーが指定するとおりに」検証します。誰かが公開鍵の PEM テキストをシークレットとして使い、"alg": "HS256" を含むヘッダーに対して HMAC を計算します。公開鍵は公開されているので、これは誰にでもできます。まさにこの形をした自前の検証コードを次に示します。
import { createHmac, timingSafeEqual, verify } from "node:crypto";
import { readFileSync } from "node:fs";
// Anti-pattern: the token picks the algorithm, one key object serves both families.
function naiveVerify(token, publicKeyPem) {
const [h, p, s] = token.split(".");
const { alg } = JSON.parse(Buffer.from(h, "base64url"));
const input = Buffer.from(`${h}.${p}`);
const sig = Buffer.from(s, "base64url");
if (alg === "HS256") {
const mac = createHmac("sha256", publicKeyPem).update(input).digest();
return mac.length === sig.length && timingSafeEqual(mac, sig);
}
if (alg === "ES256") {
return verify("sha256", input, { key: publicKeyPem, dsaEncoding: "ieee-p1363" }, sig);
}
throw new Error(`unsupported alg ${alg}`);
}
const pem = readFileSync("out/public.pem", "utf8");
const tokens = JSON.parse(readFileSync("out/tokens.json", "utf8"));
console.log("ES256 token from the issuer:", naiveVerify(tokens.ok, pem));
console.log("HS256 token keyed with the public PEM:", naiveVerify(tokens["HS256 keyed with the public PEM"], pem));
ES256 token from the issuer: true
HS256 token keyed with the public PEM: true
どちらも true を返します。2 つ目のトークンは、秘密鍵なしで作られたものです。定数時間の比較が役に立っていない点にも注意してください。このコードはタイミングには気を配っていますが、何を信頼するかについては間違っています。
対策は、検証者の側で固定した許可リスト(理想的には値 1 つだけ)と、使うアルゴリズムが紐付いた鍵です。algorithms: ["ES256"] を指定すると、4 つのライブラリはいずれも偽造トークンを拒否し、それぞれ独自のメッセージを返しました。jose は "alg" (Algorithm) Header Parameter value not allowed、jsonwebtoken は invalid algorithm、PyJWT は The specified alg value is not allowed、golang-jwt は signing method HS256 is invalid です。各ライブラリには第 2 の防御線もあり、そのため現在のライブラリのコードでこの混同が起きることはまれです。
- jsonwebtoken 9.x(2022年12月の 9.0.0 以降)は、
algorithmsを省略すると、鍵の種類から許可するアルゴリズムを導き出します。EC 鍵で許可されるのはES256、ES384、ES512だけです。 - PyJWT は、PEM 形式の公開鍵や証明書を HMAC のシークレットとして使うことを拒否します。エラーは The specified key is an asymmetric key or x509 certificate and should not be used as an HMAC secret. です。
- golang-jwt は、鍵の Go の型をチェックします。HMAC 方式には
[]byteが必要なので、*ecdsa.PublicKeyを渡すと key is of invalid type で失敗します。 - jose は、鍵の種類がアルゴリズムに合っているかをチェックします。
algorithmsオプションを指定しなくても、偽造トークンを EC 公開鍵で検証するとKeyObject instances must be of type "secret" for the HS256 algorithmで失敗しました。
いずれも許可リストの代わりにはなりません。これらが守るのは一般的なケースだけで、生のバイト列をライブラリに渡す独自の鍵ローダーは守れません。
間違い 2:自前の処理経路で「none」を受け入れる
RFC 7518 §3.6 は、完全性がほかの手段で保護されているトークンのために "alg": "none" を登録しています。上の表のとおり、どのライブラリもデフォルトではこれを拒否しました。リスクはライブラリの周りのコードにあります。検証をスキップする「デバッグモード」、空の署名を「まだ署名されていない」とみなす分岐、あるいは alg を大文字と小文字を区別して比較する拒否リストです。この BCP(ベストカレントプラクティス)の改訂版である draft-ietf-oauth-rfc8725bis(2026年10月時点で RFC Editor のキューにあります)は、まさにこの点を取り上げています。noNE と書かれたヘッダーがすり抜けてしまう、none 用の拒否リストです。許可リストには、このような穴はありません。
間違い 3:kid、jku、x5u、埋め込まれた jwk を信頼する
kid は、すでに信頼している複数の鍵から 1 つを選ぶためのものです。トークンが提供する鍵を指すポインターではありません。RFC 8725 §3.10 は、受け取ったヘッダーを信頼できない入力として扱うよう求めています。
kidはクエリやパスではなくマップで引く:SQL、LDAP、ファイル名にまで届くkidは、インジェクションの入り口になります。この記事で扱う JWKS ライブラリは、設定した URL から取得した鍵のkidの値と比較するだけで、これが正しい形です。jkuとx5uは無視する:どちらもトークンの中にある URL です。これらを取得すると、サーバーがどこに接続し、どの鍵を使うかを送信者に選ばせることになります。代わりに、発行者ごとに JWKS の URL を設定してください。どうしても使う必要があるなら、RFC 8725 は、そうした URL を許可リストと照合することを提案しています。- アクセストークンを、そのトークン自身のヘッダーに埋め込まれた
jwkやx5cで決して検証しない:トークン自身に含まれる鍵で署名されたトークンが証明するのは、送信者が何らかの鍵を持っていることだけです。DPoP のプルーフのように埋め込まれた鍵を使うプロトコルでは、その鍵を別の何か(アクセストークンのcnfクレーム)に結び付けています。DPoP の解説を参照してください。 kidがない場合を明示的に処理する:jwks-rsa は、鍵セットに複数の鍵があり、トークンがどの鍵も指定していない場合、推測せずにエラーにします(No KID specified and JWKS endpoint returned more than 1 key)。「最初の鍵」を返す自前の検索処理は、ローテーションの後、何の警告もなく間違った鍵を選んでしまいます。
間違い 4:aud と iss をチェックしない、またはチェックが甘い
ID プロバイダーは通常、多数のクライアントや API 向けのトークンに同じ鍵で署名します。API が aud をチェックしなければ、同じプロバイダーのほかのアプリケーション向けに発行されたトークンが、その API でも通ってしまいます。これが表の 1 行目です。RFC 8725 §3.9 は、オーディエンスがない JWT や自分宛てでない JWT を受信者が拒否することを求めており、RFC 9068 §4 は、OAuth のアクセストークンについて iss の完全一致を求めています。
実際に重要になる詳細は 3 つあります。
- 文字列の完全一致で比較する:
audとissは大文字と小文字を区別する文字列で、https://id.example.comとhttps://id.example.com/は別物です。発行者の値は、たまたま手元にあるトークンからではなく、プロバイダーのディスカバリドキュメントからコピーしてください。 audは配列の場合がある:ライブラリは、いずれかの要素が一致すればトークンを受け入れます。これは RFC 7519 に照らして正しい動作ですが、["https://api.example.com", "https://admin.example.com"]向けのトークンは両方の API で通ることになります。自分の API だけを対象に発行されたトークンのみを受け入れるべき場合、PyJWT にはoptions={"strict_aud": True}があります。それ以外のライブラリでは、配列の長さを自分でチェックしてください。- 鍵はその発行者のものに限る:RFC 8725 §3.8 によれば、トークンに
issがある場合、そのトークンを検証した鍵は、その発行者の鍵でなければなりません。発行者が複数ある場合は、発行者ごとに JWKS クライアントを 1 つずつ用意し、設定に基づいて選んでください。トークンの未検証のissだけで選んではいけません。テナント間で 1 つの鍵セットを共有するマルチテナントのプロバイダー(よくある例が Microsoft Entra ID です)では、自分のテナントとほかのすべてのテナントを隔てるものはissのチェックしかありません。詳しくは OIDC の ID トークン検証で説明しています。
間違い 5:exp のないトークンと、大きすぎる許容時間
ライブラリは、exp があればチェックします。ない場合、4 つのライブラリはいずれもトークンを受け入れました。発行者がこのクレームを付け忘れたり、このクレームなしでトークンを発行するコードパスを誰かが見つけたりすると、そのトークンは期限切れになりません。exp は必須にしてください。jose では requiredClaims、PyJWT では options={"require": [...]}、golang-jwt では jwt.WithExpirationRequired() を使います。jsonwebtoken にはこのようなオプションがないため、明示的にチェックしてください。
時刻のずれ(クロックスキュー)は実際に起きますが、その幅は小さなものです。RFC 7519 §4.1.4 が認めているのは「わずかな許容時間(通常は数分以内)」です。NTP で同期されているサーバーなら、30〜60 秒で十分です。不安定なテストを通すためによく追加される 1 時間の許容時間は、すべてのトークンの有効期間を 1 時間延ばします。使えなくしたつもりのトークンも例外ではありません。オプションは、jose と jsonwebtoken では clockTolerance(秒単位)、PyJWT では leeway、golang-jwt では jwt.WithLeeway です。同じ許容時間が nbf にも適用されます。
有効な「はず」のトークンの検証が、有効期限切れや有効期間前のエラーで失敗したら、許容時間に手を付ける前に、サーバーの時計を発行者の時計と比べてください。サスペンドから復帰して時計が古い時刻のままになっているコンテナーや VM は、まさに下の出力にある jwt not active や "nbf" claim timestamp check failed のエラーを起こします。
間違い 6:別の種類のトークンを受け入れる
OpenID Connect のプロバイダーは、ID トークンとアクセストークンに同じ鍵で署名し、多くの場合 iss も同じで、aud が重なることもあります。署名、発行者、オーディエンスだけをチェックする API は、ID トークンをアクセストークンとして受け入れてしまうおそれがあります。RFC 8725 §3.11 は明示的な型付けを推奨しており、RFC 9068 は JWT アクセストークンについてそれを必須にしています。typ ヘッダーは at+jwt(または application/at+jwt)でなければならず、リソースサーバーはそれ以外の値を拒否しなければなりません。プロバイダーが RFC 9068 のトークンを発行しているなら、typ をチェックしてください。発行していない場合は、scope や client_id など、アクセストークンだけが持つクレームをチェックしてください。
間違い 7:検証すべきところでデコードしてしまう
どのライブラリにも、トークンを検証せずに解析する関数があります。jsonwebtoken の jwt.decode()、jose の decodeJwt()、PyJWT の jwt.decode(..., options={"verify_signature": False})、golang-jwt の ParseUnverified です。これらは、ルーティング(iss を読んで JWKS クライアントを選ぶ)とデバッグのために存在します。コードレビューでは、リクエストの処理経路でこれらが使われていたら疑ってかかるべきです。また、その出力に基づく判断は、次にどの検証処理を呼ぶかの選択を除いて、すべてバグとして扱うべきです。
間違い 8:JWKS をリクエストごとに取得する、またはまったく再取得しない
リクエストのたびに鍵セットを取得すると、すべての呼び出しにネットワークの往復が加わり、ID プロバイダーの可用性がそのまま自分の API の可用性になります。永久にキャッシュすると、最初の鍵のローテーションで、再起動するまですべてのリクエストが失敗します。正しい動作は、次の JWKS クライアントが実装しているとおり、時間ベースのキャッシュに加えて、トークンが未知の kid を指定したときに再取得し、その再取得にレート制限をかけることです。こうしておけば、ランダムな kid の値を持つトークンを次々に送りつけられても、API がプロバイダーに対する負荷生成ツールにされることはありません。
| クライアント | キャッシュの有効期間(デフォルト) | 未知の kid | 取得のタイムアウト |
|---|---|---|---|
jose createRemoteJWKSet | 10 分(cacheMaxAge) | 再取得、30 秒に最大 1 回(cooldownDuration) | 5 秒 |
| jwks-rsa 4.1.0 | 鍵ごとに 10 分、保持する鍵は 5 個 | キャッシュミスのたびに再取得。rateLimit は有効にしない限りオフ(有効時は 1 分に 10 回) | 30 秒 |
PyJWT PyJWKClient | 300 秒(lifespan) | 再取得、30 秒に最大 1 回(cooldown_duration) | 30 秒 |
keyfunc v3 NewDefault | バックグラウンドで 1 時間ごとに再取得 | 再取得、5 分に最大 1 回 | 1 分 |
値は冒頭に記したバージョンのデフォルト値で、各ライブラリのソースコードから読み取ったものです。この違いは実測で確認できます。ランダムな kid の値を持つ 20 個のトークンで、鍵セットの取得は jwks-rsa のデフォルト設定で 20 回、rateLimit: true で 10 回(残りの 10 個は JwksRateLimitError ですぐに失敗)、jose ではわずか 1 回でした。クライアントは発行者ごとに 1 回だけ作成して再利用してください。リクエストハンドラーの中で生成した JWKS クライアントは、毎回キャッシュが空の状態から始まります。発行者側から見たローテーションそのものについては、ダウンタイムなしの JWKS 鍵ローテーションで解説しています。
正しい検証コードを 4 つのライブラリで
次の 4 つの検証コードは、どれも同じことをします。ES256 のみ、鍵は発行者の JWKS URL から取得、iss と aud は完全一致、exp は必須、typ: at+jwt、許容時間は 30 秒です。それぞれ同じ 8 個のトークンに対して実行しました。
import { createRemoteJWKSet, jwtVerify } from "jose";
// Created once per issuer and reused: caches the key set, re-fetches it
// when a token names an unknown kid (at most once per cooldown).
const JWKS = createRemoteJWKSet(new URL(process.env.JWKS_URL), {
cacheMaxAge: 600_000, // re-fetch at least every 10 minutes
cooldownDuration: 30_000, // at most one kid-miss re-fetch per 30 s
});
export async function verifyAccessToken(token) {
const { payload } = await jwtVerify(token, JWKS, {
algorithms: ["ES256"], // fixed by the verifier, never taken from the token
issuer: "https://id.example.com",
audience: "https://api.example.com",
typ: "at+jwt", // RFC 9068 access token, not an ID token
requiredClaims: ["exp", "sub"], // exp is only checked when present
clockTolerance: 30, // seconds
});
return payload;
}
import jwt from "jsonwebtoken";
import jwksClient from "jwks-rsa";
const client = jwksClient({
jwksUri: process.env.JWKS_URL,
cache: true, // per-kid cache, 10 minutes by default
rateLimit: true, // off by default; caps JWKS fetches at 10 per minute
});
const getKey = (header, callback) =>
client.getSigningKey(header.kid, (err, key) => callback(err, key?.getPublicKey()));
export function verifyAccessToken(token) {
const options = {
algorithms: ["ES256"],
issuer: "https://id.example.com",
audience: "https://api.example.com",
clockTolerance: 30,
complete: true, // also return the header, to check typ
};
return new Promise((resolve, reject) =>
jwt.verify(token, getKey, options, (err, decoded) => {
if (err) return reject(err);
if (decoded.header.typ !== "at+jwt") return reject(new Error("unexpected typ"));
if (typeof decoded.payload.exp !== "number") return reject(new Error("exp is required"));
resolve(decoded.payload);
}),
);
}
import os
import jwt
# One client per issuer, reused across requests: caches the key set and
# re-fetches on an unknown kid (at most once per cooldown).
jwks = jwt.PyJWKClient(os.environ["JWKS_URL"], lifespan=600)
def verify_access_token(token: str) -> dict:
signing_key = jwks.get_signing_key_from_jwt(token)
decoded = jwt.decode_complete(
token,
signing_key,
algorithms=["ES256"], # required argument in PyJWT 2.x
issuer="https://id.example.com",
audience="https://api.example.com",
leeway=30,
options={"require": ["exp", "iss", "aud", "sub"]},
)
if decoded["header"].get("typ") != "at+jwt":
raise jwt.InvalidTokenError("unexpected typ")
return decoded["payload"]
package main
import (
"context"
"errors"
"time"
"github.com/MicahParks/keyfunc/v3"
"github.com/golang-jwt/jwt/v5"
)
type Verifier struct {
jwks keyfunc.Keyfunc
parser *jwt.Parser
}
func NewVerifier(ctx context.Context, jwksURL string) (*Verifier, error) {
// Background refresh every hour; an unknown kid triggers at most
// one extra fetch per 5 minutes (keyfunc defaults).
jwks, err := keyfunc.NewDefaultCtx(ctx, []string{jwksURL})
if err != nil {
return nil, err
}
parser := jwt.NewParser(
jwt.WithValidMethods([]string{"ES256"}),
jwt.WithIssuer("https://id.example.com"),
jwt.WithAudience("https://api.example.com"),
jwt.WithExpirationRequired(),
jwt.WithLeeway(30*time.Second),
)
return &Verifier{jwks: jwks, parser: parser}, nil
}
func (v *Verifier) VerifyAccessToken(raw string) (jwt.MapClaims, error) {
claims := jwt.MapClaims{}
token, err := v.parser.ParseWithClaims(raw, claims, v.jwks.Keyfunc)
if err != nil {
return nil, err
}
if token.Header["typ"] != "at+jwt" {
return nil, errors.New("unexpected typ")
}
return claims, nil
}
テスト用のトークンは、有効なトークン 1 個と、それぞれルールをちょうど 1 つだけ破る 7 個です。JWKS はコンテナー内から HTTP で配信し、どのライブラリもそれぞれの JWKS クライアントを通じて取得しました。
--- jose
ok ACCEPTED sub=user-42
aud for another API ERR_JWT_CLAIM_VALIDATION_FAILED: unexpected "aud" claim value
no exp ERR_JWT_CLAIM_VALIDATION_FAILED: missing required "exp" claim
expired 1 h ago ERR_JWT_EXPIRED: "exp" claim timestamp check failed
nbf in 1 h ERR_JWT_CLAIM_VALIDATION_FAILED: "nbf" claim timestamp check failed
typ JWT (an ID token) ERR_JWT_CLAIM_VALIDATION_FAILED: unexpected "typ" JWT header value
unknown kid ERR_JWKS_NO_MATCHING_KEY: no applicable key found in the JSON Web Key Set
HS256 keyed with the public PEM ERR_JOSE_ALG_NOT_ALLOWED: "alg" (Algorithm) Header Parameter value not allowed
--- jsonwebtoken
ok ACCEPTED sub=user-42
aud for another API JsonWebTokenError: jwt audience invalid. expected: https://api.example.com
no exp Error: exp is required
expired 1 h ago TokenExpiredError: jwt expired
nbf in 1 h NotBeforeError: jwt not active
typ JWT (an ID token) Error: unexpected typ
unknown kid JsonWebTokenError: error in secret or public key callback: Unable to find a signing key that matches '2026-09-z'
HS256 keyed with the public PEM JsonWebTokenError: invalid algorithm
--- PyJWT
ok ACCEPTED sub=user-42
aud for another API InvalidAudienceError: Audience doesn't match
no exp MissingRequiredClaimError: Token is missing the "exp" claim
expired 1 h ago ExpiredSignatureError: Signature has expired
nbf in 1 h ImmatureSignatureError: The token is not yet valid (nbf)
typ JWT (an ID token) InvalidTokenError: unexpected typ
unknown kid PyJWKClientError: Unable to find a signing key that matches: "2026-09-z"
HS256 keyed with the public PEM InvalidAlgorithmError: The specified alg value is not allowed
--- golang-jwt v5 + keyfunc v3
ok ACCEPTED sub=user-42
aud for another API token has invalid claims: token has invalid audience
no exp token has invalid claims: token is missing required claim: exp claim is required
expired 1 h ago token has invalid claims: token is expired
nbf in 1 h token has invalid claims: token is not valid yet
typ JWT (an ID token) unexpected typ
unknown kid token is unverifiable: error while executing keyfunc: key not found "2026-09-z"
failed keyfunc: could not read JWK from storage
HS256 keyed with the public PEM token signature is invalid: signing method HS256 is invalid
上に示したメッセージのうち、ライブラリが返すものの大半には、エラーデータベースに原因と修正方法をまとめた専用のページがあります。たとえば unexpected "aud" claim value、jwt not active、no applicable key found in the JSON Web Key Set です。
チェックリスト
- 発行者ごとに 1 つの検証設定(JWKS URL、発行者、オーディエンス、アルゴリズムのリスト)を、起動時に構築します。
- アルゴリズム:固定の許可リストで、通常は値 1 つです。トークンから導き出してはならず、
noneを含めてもいけません。 - 鍵:設定した JWKS または鍵ファイルからのみ取得します。
kidはマップの検索キーとして使い、アクセストークンではjku、x5u、埋め込まれたjwkを無視します。 - クレーム:
expを必須にし、issとaudを完全一致で比較し、nbfもチェックし、許容時間は 30〜60 秒にします。 - トークンの種類:
typをチェックします(RFC 9068 のアクセストークンならat+jwt)。または、アクセストークンと ID トークンを区別できる別のクレームをチェックします。 - JWKS のキャッシュ:クライアントは 1 つにして再利用し、時間ベースで再取得し、未知の
kidによる再取得にはレート制限をかけます。 - リクエストの処理経路では、検証を伴わない
decodeを使いません。 - テスト:有効なトークン 1 個と、上記のルールごとに 1 個ずつのトークンを用意し、後者はいずれも失敗することを確認します。この記事の 8 個のトークンを最初のセットとして使えます。
出典
- RFC 7519:JSON Web Token、§4.1(登録済みクレーム)、§7.2(検証)
- RFC 7518:JSON Web Algorithms、§3.6(
none) - RFC 8725:JSON Web Token のベストカレントプラクティス、§2.1、§3.1、§3.8〜§3.11
- draft-ietf-oauth-rfc8725bis:RFC 8725 の改訂版
- RFC 9068:OAuth 2.0 アクセストークンの JWT プロファイル、§2.1 と §4
- jose のドキュメント:
jwtVerify、createRemoteJWKSet - jsonwebtoken と jwks-rsa の README
- PyJWT のドキュメント:使い方、
PyJWKClient - golang-jwt/jwt v5 と keyfunc v3