コンテンツにスキップ

JWT がステートレスなのにどうユーザーをログアウトさせるか

原文

Interviewer:

If JWTs are stateless… how do you "log out" a user?

要約

JWT はステートレスなため、サーバー側で単純に「削除」できない。ログアウトを実現するには「ブロックリスト(トークン無効化リスト)」「短い有効期限 + リフレッシュトークン」「Opaque Token への切り替え」等のアプローチがある。

回答

「JWT はステートレスなため、厳密な意味でサーバーからログアウトさせることはできない。しかし以下の戦略で実質的にログアウトを実現できる:

  1. ブロックリスト(JTI ベース):無効化した JTI を Redis 等に保存し、リクエスト時に確認
  2. 短い TTL(15分):アクセストークンの有効期限を短くし、実害を最小化
  3. リフレッシュトークンの失効:リフレッシュトークンを DB 管理して即時失効させる
  4. Opaque Token:ステートを持つトークンに切り替えて DB で管理する」

解説

なぜ JWT をそのまま無効化できないのか

JWT の検証はサーバーの秘密鍵(または公開鍵)だけで完結するため、DB アクセス不要でスケールしやすい。しかしこれは裏を返せば「一度発行したトークンは有効期限まで有効」を意味する。

アプローチ比較

アプローチ しくみ メリット デメリット
ブロックリスト 失効 JWT の JTI を Redis に保存 即時ログアウト可能 Redis 参照が増える(ステートが生まれる)
短い TTL access token を 15 分に設定 シンプル ログアウトしても最大 15 分は有効
リフレッシュトークン管理 refresh token を DB で管理、ログアウト時に削除 refresh token を即時失効できる access token はまだ有効
Opaque Token JWT をやめて DB 管理の不透明トークンへ 即時失効・完全なコントロール 毎リクエストで DB 参照が必要
トークンローテーション refresh token 使用のたびに新しいものを発行 漏洩を早期検知できる 実装が複雑

実践的な推奨構成

access_token:  TTL=15分(短命)
refresh_token: TTL=7日(DB管理、ログアウト時に削除)

ログアウト時:
  1. refresh_token を DB から削除
  2. access_token の JTI を Redis ブロックリストに追加(TTL=残り有効期限)
  3. クライアント側でトークンを削除

Go での実装スケッチ

// ログアウト
func Logout(ctx context.Context, jti string, exp time.Time) error {
    ttl := time.Until(exp)
    return redisClient.Set(ctx, "blocklist:"+jti, "1", ttl).Err()
}

// ミドルウェアで確認
func AuthMiddleware(c *gin.Context) {
    claims := extractClaims(c)
    blocked, _ := redisClient.Exists(ctx, "blocklist:"+claims.JTI).Result()
    if blocked > 0 {
        c.AbortWithStatus(401)
        return
    }
    c.Next()
}

→ 関連: JWT解説デバイストークンスコープ設計

リンク