TrendVidStreamは、認証スタック全体を、盗まれたトークンが再利用された瞬間にそれを検知する「ローテーション・リフレッシュ・トークン・システム」へと移行しました。この変更により、かつて攻撃者が自由に動き回ることを許していた30日間有効なJWTは、即座にロックアウトを発生させる短命な認証情報へと生まれ変わりました。
この切り替えを余儀なくされたのは、ある単一の侵害事件が原因でした。パートナーのSDKが30日間有効なJWTをプレーンテキストでキャッシュしており、攻撃者がそれを抽出して別の国からリプレイ(再送)したのです。唯一の救済策は署名鍵をローテーションすることでしたが、その操作によって全ユーザーがログアウトされてしまいました。この出来事を機に、同社のトークンセキュリティは再構築され、現在その仕組みがビデオストリーミングサービスの基盤となっています。
なぜ旧モデルは失敗したのか
JWT(JSON Web Token)は、サーバーがデータベースへの照会なしにリクエストを検証できる、自己完結型の署名済みデータ(blob)です。この利便性の裏には代償があります。もしトークンの有効期限が数週間もある場合、それを盗まれた攻撃者は数週間にわたってアクセス権を保持できてしまいます。今回の侵害では、盗まれたトークンを早期に無効化する方法がなかったため、30日間の有効期限が切れるまで有効なままとなっていました。
署名鍵をローテーションすることは、すべてのトークンを無効化する唯一のグローバルな方法ですが、これを行うとすべてのユーザーに再ログインを強いることになり、サービスの停止と信頼の低下を招きます。欠陥はJWT自体にあったのではなく、単一の長寿命な認証情報に依存していたことにありました。
新設計の概要
TrendVidStreamは現在、2種類のトークンを発行しています。
- Access tokens(アクセストークン) – 有効期限は15分。各APIコールに必要な権限を保持します。
- Refresh tokens(リフレッシュ・トークン) – 一回限りのトークン。短命なアクセストークンを新しいペアと交換するために使用されます。
クライアントがリフレッシュ・トークンを提示すると、サーバーは以下の処理を行います。
- トークンの署名とクレーム(claims)を検証する。
- そのトークンがすでに使用されていないかを確認する。
- チェックを通過した場合、新しいリフレッシュ・トークンと、新しい15分間のアクセストークンを発行する。
- 古いリフレッシュ・トークンを使用済みとしてマークする。
もしステップ2で失敗した場合(つまり、同じトークンが2回目に現れた場合)、サーバーはそれを盗難のシグナルとみなし、そのログインセッションに関連付けられたトークンの「ファミリー」全体を無効化します。これにより、被害者と攻撃者の両方が再ログインを強制され、侵害の被害を最小限に抑えることができます。
トークン・ファミリーの追跡
システムは各トークンを独立したレコードとして扱うのではなく、ユーザーがログインしたときに開始される「ファミリー」としてグループ化します。ローテーションが行われるたびに、そのファミリーの新しいメンバーが作成されます。本番環境で使用されているSQLiteのスキーマには、以下の情報が保存されます。
- family_id – セッション全体を識別する安定したID。
- token_id – 各リフレッシュ・トークンの固有ID。
- generation – デバッグに役立つカウンター。
- used_at – トークンが使用されたタイムスタンプ。
- revoked – これがセットされると、ファミリー全体が無効化されるフラグ。
再利用イベントが検出されると、その family_id の revoked フラグがセットされ、侵害されたセッションに属するすべてのトークンが即座に無効化されます。これにより、静かな乗っ取りが、ログに記録される明確なシグナルを持つアラートへと変わります。
システムの信頼性を維持するための3つの実装ルール
まず署名を検証する もしサーバーが署名の検証よりも先に「使用済みか?」というチェックを行うと、攻撃者がトークンIDを推測して大規模な無効化を引き起こす可能性があります。まず真正性を確認することで、不必要なDoS(サービス拒否)攻撃を防ぐことができます。
グレイス・ウィンドウ(猶予期間)を設ける モバイルアプリでは、トークンの期限が切れた際に、短時間に2つのリフレッシュ・リクエストを連続して送ることがよくあります。サーバーの判定が厳格すぎると、2番目のリクエストが再利用とみなされ、正当なユーザーがログアウトされてしまいます。数秒間のバッファを設けることで、「使用済み」のトークンであっても新しいペアを返せるようにし、レースコンディション(競合状態)を回避できます。
フロー全体を行ロックを伴うトランザクションで囲む 原子性(atomicity)がなければ、2つの同時実行リクエストがどちらも「自分が最初の使用者である」と判断してしまい、重複したリフレッシュ・トークンを発行して「一回限り」の保証を破ってしまう可能性があります。トークンの行をロックするデータベース・トランザクションを使用することで、一つのリクエストのみが成功することを保証できます。
まとめ: リフレッシュ・トークンを使い捨てにし、その再利用を監視することで、システムは盗まれた認証情報をすべてアラームへと変え、プラットフォーム全体のログアウトを強いることなくユーザーを保護できるようになります。
