メインコンテンツまでスキップ

サーバーサイドトークンストアと分散セッション(Redis・リフレッシュロック)

概要

BFF パターンでは、アクセストークン・リフレッシュトークン・ID トークンを ブラウザに渡さずサーバー側に保管します。本ドキュメントは、その「サーバー側トークンストア」の設計、 すなわち In-Memory と Redis の選択TTL 設計複数 Pod での同時リフレッシュ競合への対処 (分散ロック)、fail-closed なキー解決を解説します。

なぜサーバー側に保管するのか

  • クッキーサイズ制限の回避:access + refresh + id token をクッキーに入れると容易に 4 KB を超過する。 クッキーには不透明な session_id のみを入れ、トークン本体はストアに置く。
  • トークン露出の排除:トークンがブラウザに存在しないため、XSS による窃取が成立しない。
  • 集中管理:失効・ローテーション・期限管理をサーバー側で一元的に制御できる。
__Host-bff クッキー(暗号化, 中身は session_id のみ)


トークンストア(キー: session_id)
└── { access_token, refresh_token, id_token, expires_at }
クッキーの暗号化

クッキー(session_id を含む)は ASP.NET Core の Data Protection で暗号化・署名されます。複数 Pod で クッキーを相互に復号するには鍵リングの共有が必要です。詳細は .NET Data Protection と認証クッキーのセキュリティ を参照してください。

In-Memory と Redis の選択

レプリカ数バッキングストア説明
1(初期)DistributedMemoryCache(In-Memory)外部依存なし。開発・低トラフィック向け。Pod 再起動でセッション消失
2 以上Redis(分散キャッシュ)レプリカ間でセッション共有が必須。共有しないと別 Pod 振り分けでセッション断

共通のストア抽象(例 IUserTokenStore)を介すことで、フェーズ間の移行はバッキングストアの差し替え (NuGet 追加+設定変更)のみで済み、アプリのコード構造を変えずに In-Memory → Redis へ移行できます。

TTL 設計(最重要)

ストアのエントリにはリフレッシュトークンが含まれ、これはアクセストークンより長命です。したがって、

TTL はアクセストークン期限に合わせてはいけない
  • TTL = セッション/リフレッシュトークン寿命にする。
  • ❌ アクセストークン期限(数十分)を TTL にすると、アクセストークン失効と同時にリフレッシュトークンごと 消滅し、更新不能になる。ユーザーは数十分ごとに強制ログアウトされる。

fail-closed なキー解決

session_id をキーにストアを引く際、session_id が欠落していたら**例外で拒否(fail-closed)**します。

var sessionId = user.FindFirst("session_id")?.Value;
if (string.IsNullOrWhiteSpace(sessionId))
throw new InvalidOperationException("session_id is missing; cannot resolve token store key.");
return $"tokens:{sessionId}";
空キーの衝突

session_id が空のとき tokens:(空文字キー)を返してしまうと、クレームを持たない複数ユーザーが同一 エントリを共有し、他人のトークンを読めてしまう重大な情報漏洩につながります。必ず fail-closed にします。

同時リフレッシュ競合と分散ロック

トークン管理ライブラリは通常、単一プロセス内の同時リフレッシュは同期します。しかし BFF が複数 Pod の 場合、別 Pod 上の同時リクエストが同時にリフレッシュし得て、リフレッシュトークンのローテーションと 相まって更新競合(トークン上書き・リフレッシュトークン無効化)が起きます。

これを防ぐのが分散リフレッシュロックです。

キー構造:
tokens:{session_id} # トークン本体(TTL = セッション/リフレッシュ寿命)
lock:refresh:{session_id} # 分散ロック(TTL = 10〜30 秒)
リフレッシュ手順(concurrency control 内):
1. lock:refresh:{session_id} を SET NX PX で取得
2. 取得できたら → ストアを「再読込」(別 Pod が直前に更新済みかもしれない)
まだ期限切れなら → IdP でリフレッシュ → 保存
ロックを compare-and-delete で解放(他人のロックを消さない)
3. 取得できなければ → 短く待ってストアを再読込し、新しいトークンを返す(再リフレッシュしない)
「distributed lock」と「Redlock」は別物

ここでの分散ロックは 単一の論理 Redis(例: マネージド Redis のプライマリ)に対する SET NX PX + compare-and-delete です。複数の独立した Redis マスターを前提とする Redlock アルゴリズムとは異なります。独立マスターを運用しない限り Redlock は不要で、用語も区別すべきです。

取得後の「再読込(re-read)」が肝心です。待機中にピア Pod がトークンをローテーション済みの場合に、 二重リフレッシュを防ぎます。

ストアのハードニング

トークンは保存時に平文相当のため、Redis を使う場合は次を必須とします。

項目内容
転送時 TLSssl=true で接続(TLS 1.2+)
ネットワーク隔離公開エンドポイントを無効化し、Private Endpoint / VNet 経由のみ
永続化の無効化RDB/AOF を無効化し、トークンがディスクに書き出されないようにする(揮発キャッシュ運用)
認証アクセスキーより Managed Identity(Microsoft Entra 認証) を優先
任意の多層防御保存前にトークン値を暗号化し、メモリダンプでの平文露出を防ぐ

まとめ

  • BFF はトークンをサーバー側ストアに保管し、クッキーには不透明な session_id のみを入れる (4 KB 制限の回避・トークン露出の排除)。
  • レプリカ 1 は In-Memory、2 以上は Redis(共有必須)。共通抽象で移行コストを最小化。
  • TTL はリフレッシュトークン寿命に合わせる(アクセストークン期限にしない)。
  • キー解決は fail-closed。複数 Pod の同時更新は分散リフレッシュロック + 再読込で直列化する。
  • Redis は TLS・Private Endpoint・RDB/AOF 無効化・Managed Identity でハードニングする。

関連ドキュメント

参考リンク