JWT 指南

我如何做 JWT Key Rotation 而不讓驗證突然失敗

我會按照 new public key 先 publish、再切換 signing、保留 old/new overlap、處理 JWKS cache 與 unknown kid,並把 planned rollover 與 emergency revocation 分開。

作者:JWTDecoder.tools最後確認 2026-09-13

編輯語氣說明:第一人稱敘述代表 JWTDecoder.tools 已文件化的驗證工作流程;發布者與組織作者皆為 JWTDecoder.tools。

KEY A → A + B → KEY B
KEY A → A + B → KEY B · TTL · cache · exp

如果 key rotation 後只有新 JWT 開始出現 unknown kid 或 invalid signature,我會先畫出時間順序:new public key 什麼時候進 JWKS?issuer 又是什麼時候開始用 new private key 簽 token? 很多 rotation outage 都發生在這兩件事順序反了。

我使用的 planned rotation 順序

1. 產生 new key pair
2. 先把 new public JWK publish 到 JWKS
3. 等待合理的 cache / propagation
4. issuer 開始用 new private key 簽 token
5. JWKS 同時保留 old + new public JWK
6. 等 old token 最大 lifetime 結束
7. 移除 old public key 並退休 old private key

核心原則:publish first, sign later。

為什麼 new key 要先 publish

如果 issuer 已經發出:

{ "kid": "new-key" }

但 verifier cache 只有:

JWKS = [old-key]

那新 token 會立刻失敗。這不是 signature algorithm 的問題,而是 key distribution 時序錯誤。

為什麼 old public key 不能立刻刪

rotation 前最後一秒簽出的 old token 可能還有 30 分鐘或 1 小時 lifetime。只要這些 token 仍應合法,verifier 就仍需要 old public key。

我通常考慮:

old key overlap >= max old-token lifetime
                + cache propagation
                + operational margin

kid 讓 old/new key 可以共存

JWKS:

{
  "keys": [
    { "kid": "old-key", "kty": "RSA", "...": "..." },
    { "kid": "new-key", "kty": "RSA", "...": "..." }
  ]
}

新 token header 使用 new-key,舊 token仍使用 old-key。詳細 key selection 原則見 JWT kid。

Unknown kid 時我允許受控 refresh

我不希望 verifier 每一個 request 都抓 JWKS,但也不希望 cache TTL 還有 30 分鐘時新 key 永遠失敗。

我偏好的流程:

cached JWKS 找 kid
→ 找不到
→ rate-limited/coalesced refresh
→ 再找一次
→ 還是沒有就 fail closed

random kid 可以被攻擊者大量製造,所以 refresh 不能無限制。

我會檢查多層 cache

除了 application cache,還可能有:

  • CDN;
  • reverse proxy;
  • identity SDK 內部 cache;
  • 多個 API instance 各自 cache。

如果「只有部分機器失敗」,很可能各 instance 看到不同版本 JWKS。

Planned rotation 與 emergency revocation 不一樣

Planned rollover

目標是讓合法舊 token 平順存活到自然過期,因此需要 old/new overlap。

Emergency revocation

如果 private key 可能外洩,我可能刻意讓 old token 提前失效。這時安全 containment 比 session continuity 更重要。

所以「永遠保留 old key 到所有 token 過期」並不是 incident response 的絕對規則。

我不重複使用舊 kid

如果換了 key material 卻沿用相同 kid,stale cache 可能繼續用 old key,造成難以解釋的 invalid signature。

我傾向 new key material 使用 new kid。JWK Inspector 的 RFC 7638 thumbprint 可以幫助比較實際 public key 是否相同。

Rotation 後 invalid signature 的排查順序

  1. failing token 的 kid;
  2. live JWKS 是否包含它;
  3. failing verifier 的 cached JWKS 是否包含它;
  4. JWK kty/alg/use/key_ops 是否相容;
  5. 實際 signature 是否通過;
  6. issuer 是否真的用對應 private key 簽發。

我會用 JWKS Validator 把 token + 當前 JWKS 組合重現。

Token lifetime 直接影響 rotation 成本

lifetime 很長,old public key 就必須保留更久;lifetime 很短,rotation 可以更快結束,但 refresh 頻率與使用者體驗會受到影響。

所以我不會把 token lifetime 與 key rotation 當成完全獨立的兩個設定。

我在 production 前測試的案例

old token + old/new JWKS → pass
new token + old/new JWKS → pass
new token + stale old-only JWKS → refresh 後 pass
random unknown kid → 受控 refresh 後 fail
old token + new-only JWKS → 只在 retirement 後 fail

我避免的 rotation 方式

  • new JWK 還沒 publish 就先用 new key 簽;
  • 換 key material 卻重用 kid;
  • unknown kid 觸發無限 JWKS fetch;
  • 不看 token lifetime 就移除 old key;
  • compromised key 還照 planned rollover 長時間 overlap;
  • 讓 token 的 jku 自己指定 rotation key source。

我的 rotation 清單

  1. new key 使用 new kid。
  2. new public JWK 先 publish。
  3. 確認 JWKS/CDN/verifier cache propagation。
  4. 再切 signing key。
  5. old/new key 依 token lifetime overlap。
  6. unknown kid refresh 有 rate limit。
  7. old token drain 後再退休 old key。
  8. emergency revocation 有獨立程序。
  9. old/new/stale-cache scenario 自動測試。

對我來說,rotation 完成不是「新 key 已經上線」,而是 old/new token 在預期時段都能正確驗證,cache refresh 與 key retirement 也完全符合 lifecycle policy。overlap 時段必須涵蓋仍可能有效的 token exp,才能安全退休舊 key。

來源與參考資料

相關指南