リフレッシュトークン運用の正しい設計と失敗パターン
JWT
セキュリティ
認証
失敗しやすい原因
リフレッシュトークン(RT)運用で強制ログアウトや不安定化を招く典型原因は3つ。
- 並行リクエストの競合(最頻出): 画面表示時に複数 API が同時に期限切れを検知し、同時に refresh する。RT をローテーション(使い捨て)していると最初の1本だけ成功し、残りが「古い RT」として弾かれる。
- Set-Cookie とリダイレクトの連鎖: middleware/edge 層で refresh して新 Cookie をセットしつつリダイレクトすると、Cookie が焼き付く前に次のリクエストが旧 Cookie で飛ぶ。
- 失効できないステートレス RT: RT をサーバー側に保存していないと、漏洩した RT を無効化できず、再利用検知もできない。
トークン2本立てと保存
- アクセストークン(AT): 短命(15〜30分)、httpOnly Cookie、
Path=/。ステートレス JWT でよい。 - リフレッシュトークン(RT): 長命(14〜30日)、httpOnly Cookie、
Pathを refresh エンドポイントに限定する。これで通常の API リクエストに RT が一切送信されず漏洩面が減る。 - RT はサーバー側 DB にハッシュで保存しステートフル化(user_id, token_hash, family_id, expires_at, rotated_at, revoked_at)。AT はステートレスのままでよい。
リフレッシュのタイミング
先回りで期限を見て更新するより、API が 401 を返したら refresh して元リクエストを1回だけリトライする方式が堅牢。トークン更新はクライアントの API クライアント層(401 インターセプタ)に集約し、edge/middleware 層では行わない(原因2を回避)。
並行競合のシングルフライト化(最重要)
401 が同時多発しても refresh は1回だけ走らせ、他は同じ Promise を待たせる。例: モジュール変数に refresh 中の Promise を保持し、完了で null に戻す。サーバー側ローテーションをするならこれがないとほぼ確実に破綻する。
ローテーションと再利用検知
- refresh のたびに RT を新規に差し替え、旧 RT を失効する。
- 失効済み RT が再使用されたら盗難と判断し、そのユーザーの RT ファミリーを全失効する(トークンファミリー方式)。
- ただしネットワーク再送で正規ユーザーが旧 RT を再送することがあるため、直前ローテーションした旧 RT は数十秒のグレース期間だけ許容して誤ログアウトを減らす。
検証方法
複数タブ・並行リクエストで同時に AT 期限切れを起こしても強制ログアウトしないこと、失効済み RT を意図的に再送すると全セッションが失効すること、RT が refresh エンドポイント以外のリクエストに送られていないこと(Cookie の Path)を確認する。