JWKS エンドポイント
/jwks は、OP の署名鍵の公開鍵を JWKS(JSON Web Key Set、RFC 7517)として公開するエンドポイントです。JWKS は公開鍵を kid ごとに並べた JSON 文書で、RP はこれを取得して、ID トークン、JWT アクセストークン、JARM、userinfo JWT を検証します。本ページは OP と RP キャッシュの間の運用契約です。
何を返すか
GET /jwks
Accept: application/jwk-set+jsonレスポンス:
{
"keys": [
{
"kty": "EC",
"crv": "P-256",
"x": "...",
"y": "...",
"kid": "sig-2026-05",
"alg": "ES256",
"use": "sig"
}
]
}署名鍵のエントリは use: "sig" と、その鍵のアルゴリズムを持ちます。既定では alg: "ES256"、kty: "EC"、crv: "P-256"、OP が RS256 で署名する場合は alg: "RS256"、kty: "RSA" です。2 つの間を切り替える期間中は、両方の種類の鍵が載ります。WithEncryptionKeyset を設定している場合、OP は暗号化鍵の公開鍵(use: "enc")を署名鍵のあとに追加で告知します。両者が別々にローテーションする仕組みは 鍵ローテーション を参照してください。
Keyset の署名アルゴリズムが 1 つである理由
JOSE の許可リスト(RS256、PS256、ES256、EdDSA)は OP が 検証するときの集合です(RP の private_key_jwt 署名や JAR request object 署名で意味があります)。OP が 署名する側の鍵は ES256 か RS256 の 1 種類で、もう一方の種類の鍵は切り替え期間中に検証専用のエントリとして載るだけです。必須オプション § WithKeyset を参照。
HTTP ヘッダ
| Header | 値 | 補足 |
|---|---|---|
Content-Type | application/jwk-set+json | RFC 7517 |
Cache-Control | public, max-age=3600, stale-while-revalidate=3600 | 1 時間キャッシュ、1 時間 SWR |
ETag | "<sha256-hex>" | シリアライズ後のボディ全体に対する strong validator |
Allow | GET, HEAD | 他のメソッドは 405 |
HEAD はボディなしで同じヘッダ(ETag、Cache-Control を含む)を返します。RP のキャッシュは通常、HEAD ではなく次の条件付き GET で再検証します。
If-None-Match
ハンドラは If-None-Match を尊重します:
- ETag が完全一致 →
304 Not Modified、ボディなし。304 にもETagとCache-Controlは付きます。 *ワイルドカード →304 Not Modified。- 同じ opaque-tag の weak validator(
W/"...") → weak comparison により一致扱い。 - カンマ区切りの一覧 → いずれか 1 つが一致すれば一致扱い。
ETag は シリアライズ後の JWKS 全体 に対するハッシュなので、kid の変更や鍵集合のメンバー変更が起きると自動で値が変わります。手動でキャッシュを破棄させる手順は不要です。
ローテーション中のキャッシュ制御
ローテーションのオーバーラップ期間中は、短いキャッシュを出せます:
Cache-Control: public, max-age=300, must-revalidateop.WithJWKSRotationActive(predicate) で Provider に述語を渡します。述語はリクエストのホットパスで評価されるので、軽量で並行安全にしてください。オプションを省略するか nil を渡すと、すべてのレスポンスで長いキャッシュが返ります。
典型的なパターン:
var rotationUntil atomic.Value // time.Time
rotationUntil.Store(time.Time{})
// ローテーション開始時:
rotationUntil.Store(time.Now().Add(24 * time.Hour))
isRotating := func() bool {
until, _ := rotationUntil.Load().(time.Time)
return time.Now().Before(until)
}
provider, _ := op.New(
/* 必須オプション */
op.WithJWKSRotationActive(isRotating),
)期間が過ぎると述語が false を返し、長いキャッシュに戻ります。op.WithJWKSRotationActive を複数回呼ぶと最後の呼び出しが勝つので、管理プロセスが以前のオプションリストを組み立て直さずに述語を差し替えられます。
RP のキャッシュ挙動
適切に実装された RP は次のように動きます:
- 起動時(または初回検証時)に
/jwksを 1 回取得します。 Cache-Controlに従ってキャッシュします。max-ageを超えたらIf-None-Matchで再検証します。- JWS 検証時に未知の
kidを見たら、キャッシュをバイパスして/jwksを再取得します。これがローテーション時に期待される経路です。新しいkidが届き、検証をやり直して成功します。 kidが一致しないときに、キャッシュ済みの鍵にフォールバック しないでください。フォールバックはローテーションの監査を壊します。
OP 自身も、/userinfo と /introspect で自分が発行した JWT アクセストークンを検証するときに同じ規則を使います。設定済みの keyset から kid を探し、未知の kid や NotAfter を過ぎた kid は拒否します。「未知の kid なら現行鍵を信頼する」という特例はありません。
JWKS に複数の鍵を出す
設定された op.Keyset のすべてのエントリが告知されます:
op.WithKeyset(op.Keyset{
{KeyID: "sig-2026-05", Signer: newPriv}, // 現行 — 署名はこの鍵
{KeyID: "sig-2026-02", Signer: prevPriv}, // 退役 — 検証専用
})両方の kid が /jwks に並びます。OP は先頭エントリで新しいトークンを署名し、それ以降のエントリは、ローテーション前に発行されたトークンを RP が検証できるよう JWKS に残ります。サイクルは 鍵ローテーション を参照してください。
エントリは、Keyset から外して作り直すまで /jwks に残ります。退役側のエントリに SigningKey.NotAfter を設定すると、期限になった時点から OP 自身がその kid を信頼しなくなり、その kid が提示されると key.retired_kid_presented も記録します。ただし公開鍵は公開されたままなので、RP のキャッシュは自然に入れ替わります。暗号化鍵は扱いが違い、NotAfter を過ぎた EncryptionKey は /jwks からも取り除かれます。
HSM / KMS 連携
op.SigningKey.Signer の型は crypto.Signer です。Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) を満たす実装なら何でも入ります。よくあるパターン:
- AWS KMS:
kms.NewFromConfig(cfg)+ KMS 鍵に対するSignを呼ぶcrypto.Signerアダプタ。 - Azure Key Vault:
azkeys.Client+ アダプタ。 - HashiCorp Vault Transit: HTTP signer アダプタ。
- Google Cloud KMS:
kms.NewKeyManagementClient+ アダプタ。 - PKCS#11 / ハードウェア HSM:
crypto11.Contextがそのままcrypto.Signerを返します。
OP が要求するのは:
- active な signer の公開鍵が署名アルゴリズムに合っていること。
ES256(既定)ならelliptic.P256()上の*ecdsa.PublicKey、RS256なら 2048 bit 以上の RSA 鍵です。retiring のエントリはNotAfterを設定すれば、もう一方の型でも構いません。それ以外の鍵だとop.Newが失敗します。 Signが並行安全であること。薄いアダプタを書くときは、クライアントが並行安全でなければアダプタ側で保証してください。
KMS をバックエンドにした署名器は、トークン発行の待ち時間を増やします(典型的には 1 署名あたり 10 〜 50 ms)。容量見積もりに織り込んでください。
Discovery 上の jwks_uri
discovery 文書は、設定された JWKS パスを、issuer に mount prefix(既定は /oidc)を加えた配下の絶対 URL として jwks_uri に出します。RP は discovery を 1 回取って jwks_uri を辿るので、OP 側でパスをハードコードする必要はありません。
ルータと衝突するなら WithEndpoints(op.Endpoints{JWKS: "/keys"}) でパスを上書きできます。discovery 文書も追従します。
契約の検証
# ヘッダ — Cache-Control と ETag を確認。
curl -I https://op.example.com/jwks
# ボディ — 現行 + 退役の各 kid に 1 エントリずつ並ぶか。
curl -s https://op.example.com/jwks | jq '.keys[] | {kid, alg, use}'
# 再検証 — 一致する ETag で 304。
ETAG=$(curl -sI https://op.example.com/jwks | awk '/[Ee][Tt]ag/ {print $2}' | tr -d '\r')
curl -I -H "If-None-Match: $ETAG" https://op.example.com/jwks
# → HTTP/1.1 304 Not Modifiedキャッシュが温まった RP の通常レスポンスは 304 です。