DEV Community

myougaTheAxo
myougaTheAxo

Posted on • Originally published at zenn.dev

Claude CodeでJWTキーローテーションを設計する:JWK・kid管理・ゼロダウンタイム更新

JWTのキーローテーションを正しく実装できていますか?

「秘密鍵を変えたらトークンが全部無効になった」——これ、ゼロダウンタイムが必要な本番環境では致命的です。

Claude Codeに以下のルールをCLAUDE.mdに記述することで、JWK・kid管理・ゼロダウンタイムローテーションの設計を一気に実装できます。


CLAUDE.mdルール定義

まずClaude Codeへの指示をまとめます。

# JWT Key Rotation Rules
- 常時2つの鍵を保持する: current(署名用)と previous(検証用)
- 各鍵にはkidを付与し、JWTヘッダーのkidフィールドで使用鍵を識別する
- ローテーション後30分経過したら previous を破棄する
- JWKS(/.well-known/jwks.json)エンドポイントを公開し、公開鍵を配布する
Enter fullscreen mode Exit fullscreen mode

これを読み込んだClaude Codeは、設計ルールに従ったコードを生成します。


JWKKeyManagerクラスの実装

import { SignJWT, importJWK, exportJWK, generateKeyPair } from 'jose';
import { v4 as uuidv4 } from 'uuid';

interface KeyEntry {
  kid: string;
  privateKey: CryptoKey;
  publicKey: CryptoKey;
  createdAt: Date;
  role: 'current' | 'previous';
}

export class JWKKeyManager {
  private keys: Map<string, KeyEntry> = new Map();

  async generateNewKey(): Promise<void> {
    // currentをpreviousに降格
    for (const [kid, entry] of this.keys) {
      if (entry.role === 'current') {
        entry.role = 'previous';
        // 30分後に自動削除
        setTimeout(() => {
          this.keys.delete(kid);
          console.log(`[KeyRotation] Removed expired key: ${kid}`);
        }, 30 * 60 * 1000);
      }
    }

    // 新しいcurrentを生成
    const { privateKey, publicKey } = await generateKeyPair('RS256');
    const kid = uuidv4();

    this.keys.set(kid, {
      kid,
      privateKey,
      publicKey,
      createdAt: new Date(),
      role: 'current',
    });

    console.log(`[KeyRotation] New current key generated: ${kid}`);
  }

  getCurrentKey(): KeyEntry | undefined {
    for (const entry of this.keys.values()) {
      if (entry.role === 'current') return entry;
    }
    return undefined;
  }

  getKeyById(kid: string): KeyEntry | undefined {
    return this.keys.get(kid);
  }
}
Enter fullscreen mode Exit fullscreen mode

ポイント:

  • generateNewKey()でcurrent→previousへ降格
  • previousは30分後に自動削除(setTimeout管理)
  • kidはUUIDで一意性を保証

signToken():kidをヘッダーに埋め込む

export async function signToken(
  keyManager: JWKKeyManager,
  payload: Record<string, unknown>
): Promise<string> {
  const currentKey = keyManager.getCurrentKey();
  if (!currentKey) throw new Error('No current signing key available');

  const token = await new SignJWT(payload)
    .setProtectedHeader({ alg: 'RS256', kid: currentKey.kid })
    .setIssuedAt()
    .setExpirationTime('1h')
    .sign(currentKey.privateKey);

  return token;
}
Enter fullscreen mode Exit fullscreen mode

setProtectedHeaderkidフィールドに現在の鍵IDを埋め込むことで、検証時にどの鍵を使うべきか特定できます。


verifyToken():kidで鍵を引き当てる

import { jwtVerify, decodeProtectedHeader } from 'jose';

export async function verifyToken(
  keyManager: JWKKeyManager,
  token: string
): Promise<Record<string, unknown>> {
  // ヘッダーからkidを取得
  const header = decodeProtectedHeader(token);
  if (!header.kid) throw new Error('Token missing kid in header');

  const keyEntry = keyManager.getKeyById(header.kid);
  if (!keyEntry) throw new Error(`Unknown kid: ${header.kid}`);

  const { payload } = await jwtVerify(token, keyEntry.publicKey);
  return payload as Record<string, unknown>;
}
Enter fullscreen mode Exit fullscreen mode

なぜこの設計が安全か:

  • ローテーション直後でもprevious鍵が30分間残るため、既存トークンは検証可能
  • kidが存在しない場合はエラーで弾く(不正トークンの早期検出)

/.well-known/jwks.jsonエンドポイント

クライアントが公開鍵を取得できるよう、JWKSエンドポイントを公開します。

import express from 'express';

const app = express();

app.get('/.well-known/jwks.json', async (req, res) => {
  const jwks = [];

  for (const entry of keyManager.getAllKeys()) {
    const jwk = await exportJWK(entry.publicKey);
    jwks.push({ ...jwk, kid: entry.kid, use: 'sig', alg: 'RS256' });
  }

  res
    .setHeader('Cache-Control', 'public, max-age=3600')
    .json({ keys: jwks });
});
Enter fullscreen mode Exit fullscreen mode

Cache-Control: max-age=3600で1時間キャッシュを許可しつつ、ローテーション後は古いキャッシュが切れるまでprevious鍵で検証継続できます。


まとめ

Claude Codeにルールを与えるだけで、以下の4点を満たした実装が生成されます:

  1. kidヘッダー管理 — JWTのどの鍵で署名したかを明示
  2. JWKSエンドポイント — 公開鍵を標準形式で配布
  3. ゼロダウンタイムローテーション — previousを30分間保持して既存トークンを継続検証
  4. 自動クリーンアップ — 古い鍵はタイマーで自動削除

キーローテーションを「後回し」にすると、鍵漏洩時に全ユーザーのセッションを強制失効させる選択肢しか残りません。最初から設計に組み込んでおきましょう。


セキュリティ設計をもっと強化したい方へ

JWT以外のOWASP Top 10対策・APIセキュリティ設計を網羅したSecurity PackをPromptWorksで販売中です。

  • /security-check — コードのセキュリティ診断プロンプト
  • 価格: ¥1,480
  • 販売ページ: prompt-works.jp

Claude Codeと組み合わせて、セキュアな実装を最速で仕上げてください。

Top comments (0)