Skip to content

Surviving OIDC Back-Channel Logout

マイクロサービスのシングルサインオンを構築して、ログインが動いたとき「これで終わりだ」と思った。認可コードフローが通り、JWTのアクセストークンが各サービスに渡り、ユーザー情報が正しく取得できる。完成だ。

ログアウトボタンを実装するまでは。

ホテルのチェックアウト問題

たとえ話をしよう。大きなリゾートホテルを想像してほしい。レストラン、プール、スパ、ジム——それぞれが独立した施設として運営されている。宿泊客はチェックイン時にルームキーカードを受け取り、各施設でそれを見せればツケで利用できる。

問題はチェックアウトだ。

フロントでチェックアウトの手続きが完了しても、レストランはそれを知らない。プールの受付もスパのスタッフも知らない。元・宿泊客がルームキーカードを見せれば、施設側は「有効なカードだ」と判断して通してしまう。

これが、マイクロサービスにおけるログアウトの問題だ。OpenID Connect(OIDC)のOpenID Provider(OP)がフロント、各Relying Party(RP)がレストランやプールに当たる。ログインは一箇所で済むが、ログアウトは全施設に伝えなければならない。

OIDC / OP / RPとは

OpenID Connect(OIDC)は、OAuth 2.0の上に「このユーザーは誰か」を確認する認証レイヤーを追加した仕様。OpenID Provider(OP)はログインを受け付ける認証サーバー(Keycloak、Auth0、Entra IDなど)。Relying Party(RP)はOPに認証を委託するアプリケーション側を指す。

二種類のルームキー

ホテルのルームキーには二つの設計思想がある。

オペークトークン——フロントに電話するカード

一つ目は、カードにランダムな番号だけが書かれている方式。実物はこんな感じだ。

a]3nF8kL9mQx2vR7wP5tY1uJ4hD6gB0s

ランダムな文字列。意味のある情報は一切含まれていない。施設のスタッフはカードを見ても、それが有効かどうかわからない。毎回フロントに電話して「この番号のお客さん、まだ泊まってますか?」と確認する。これがオペークトークンとToken Introspection(RFC 7662)の関係だ。

ログアウト(チェックアウト)は簡単だ。フロントが台帳から名前を消せばいい。次に施設から電話が来たとき「もう泊まっていません」と答えるだけ。即座に、確実に、ログアウトが反映される。

代償は負荷だ。すべての施設が、すべての利用のたびにフロントに電話する。宿泊客が1000人いて、10の施設を1日に何度も利用するなら、フロントの電話は鳴り止まない。OPのIntrospectionエンドポイントがボトルネックになる。

JWT——施設側で読めるカード

二つ目は、カードに宿泊者名、部屋番号、チェックアウト日時、ホテルの署名が暗号的に刻印されている方式。実物はこうだ。

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiJndWVzdF8xMjMiLCJpc3MiOiJodHRwczovL2hvdGVsLmV4YW1wbGUuY29tIiwiYXVkIjoicmVzdGF1cmFudC1hcHAiLCJleHAiOjE3NTIyMjcyMDAsInNpZCI6InNlc3Npb25fYWJjIn0.
(署名)

Base64デコードすると、ペイロード部分にはこう書いてある。

json
{
  "sub": "guest_123",
  "iss": "https://hotel.example.com",
  "aud": "restaurant-app",
  "exp": 1752227200,
  "sid": "session_abc"
}

誰が(sub)、どこから(iss)、どのアプリ向けに(aud)、いつまで有効か(exp)。全部カードに書いてある。施設のスタッフはカードを手元で読み取り、署名を検証するだけで、フロントに電話しなくても「有効な宿泊客だ」と判断できる。これがJWTアクセストークンだ。

フロントの電話は鳴らない。各施設が自律的に検証する。スケーラビリティは抜群だ。

ただし、ログアウトが地獄になる。

宿泊客がフロントでチェックアウトしても、カードに刻印された情報は変わらない。カードには「チェックアウト日時: 明日の11時」と書いてある。物理的に回収するか、全施設に「このカードはもう無効です」と連絡しない限り、有効なままだ。

ここに、OIDCのログアウト仕様群が生まれた理由がある。

ログアウトを伝える三つの方法

OIDCは、OPからRPへログアウトを伝える方法を三つ定義している。2022年9月にすべてFinalとなり、2024年10月にはISO規格(ISO/IEC 26134〜26137:2024)にもなった。

Session Management——ブラウザが定期巡回する

RPがブラウザ上のiframeでOPのセッション状態をポーリングする方式。施設のスタッフが定期的にフロントの掲示板を覗きに行くようなものだ。

だが、この方式はサードパーティCookieに依存する。主要ブラウザがサードパーティCookieをブロックし始めた現在、実質的に機能しない。

サードパーティCookieとは

ブラウザが表示しているサイト(ファーストパーティ)ではなく、そのページに埋め込まれた別ドメイン(サードパーティ)が発行するCookie。広告トラッキングに多用されたため、プライバシー保護の観点からSafari、Firefoxはすでにデフォルトでブロックしており、Chromeも段階的に制限を進めている。OIDCのSession ManagementやFront-Channel Logoutは、OPのiframeがRP側のCookieを読み書きする必要があるため、この制限の影響を直接受ける。

Front-Channel Logout——ブラウザ経由で全施設に通知する

ユーザーがOPでログアウトすると、OPのページに各RPのfrontchannel_logout_uriを隠しiframeで埋め込む。ブラウザが各RPのURLを読み込み、RPはCookieを削除してセッションを終了する。

これもサードパーティCookieの問題を抱えている。クロスオリジンのiframeからRPのCookieにアクセスできない。仕様自体が「一部のブラウザではサードパーティコンテンツへのアクセスがデフォルトでブロックされ始めている」と認めている。

Back-Channel Logout——サーバー間で直接通知する

ブラウザを介さない。OPがサーバー間のHTTP POSTで各RPに直接通知する。ホテルで言えば、フロントが内線電話で各施設に「この方はチェックアウトしました」と直接連絡する方式だ。

サードパーティCookieに依存しない。ブラウザの状態に依存しない。クロスドメイン環境において、OIDCのログアウトで実用的に機能する唯一の方式がBack-Channel Logoutだ。

Logout Tokenの中身

OPがRPに送るLogout Tokenは、署名付きのJWTだ。

OPは各RPのbackchannel_logout_uriに対してapplication/x-www-form-urlencoded形式でHTTP POSTを送る。ボディはlogout_token=eyJhbGci...というシンプルな形式。

クレーム必須説明
issYes発行者(OPの識別子)
audYes受信者(RPのclient_id
iatYes発行時刻
expYes有効期限
jtiYes一意なトークンID(リプレイ防止)
eventsYeshttp://schemas.openid.net/event/backchannel-logout をキーとするJSONオブジェクト
sub条件付きユーザー識別子。subsidのどちらか(または両方)が必須
sid条件付きセッションID。上に同じ
nonce禁止ID Tokenとの混同を防ぐため、含めてはならない

subだけでLogout Tokenを送ると、RPはそのユーザーの全セッションを破棄しなければならない。特定のセッションだけをログアウトしたければsidが必要だ。RPはクライアント登録時にbackchannel_logout_session_required: trueを設定することで、OPにsidの送信を要求できる。

RPはLogout Tokenを受け取ると、署名検証、クレームの検証、nonceの不在確認、eventsクレームの確認——計11ステップのバリデーションを経て、ローカルセッションを破棄する。成功すればHTTP 200、失敗すれば400を返す。

JWTが作り出すパラドックス

ここで、ホテルの話に戻ろう。

JWTアクセストークンの強みは「フロントに電話しなくていい」ことだった。各施設が自律的に検証する。OPに負荷が集中しない。スケールする。

しかし、ログアウトの瞬間にこの強みが裏返る。

各施設が自律的に判断しているからこそ、「もう無効です」という情報を外から注入しなければならない。しかもJWTには有効期限が刻印されている。期限内であれば、署名は正しく、クレームは有効で、RPには拒否する理由がない。

オペークトークンなら、OPが台帳を消せば終わりだった。JWTでは、OPが能動的に全RPへ通知を送り、各RPがローカルセッションを破棄するまでログアウトは「完了」しない。

これが、Back-Channel Logoutの本質的な難しさだ。分散システムで「即座の一貫性」を実現しようとしている。

レースコンディション

OPがログアウトを処理してからRPに通知が届くまでにはタイムラグがある。

  1. T=0ms: ユーザーがRP-Aでログアウトボタンを押す
  2. T=1ms: RP-AがOPのend_session_endpointにリダイレクト
  3. T=50ms: OPがセッションを終了し、各RPへの通知を開始
  4. T=51ms: ユーザーの別タブがRP-BにJWTを送る
  5. T=200ms: OPからRP-Bへの通知が到着

T=51msの時点で、RP-Bはまだ通知を受け取っていない。JWTは署名も有効期限も正しい。リクエストは通る。ユーザーは「ログアウトした」つもりなのに。

JWTの有効期限を短くすれば(5分、あるいは30秒)、この窓は小さくなる。しかしゼロにはならない。そして金融や医療のように「ログアウト後の即時遮断」が必要な領域では、数秒の窓も許容できないことがある。

ファンアウト問題

マイクロサービス環境では、一人のユーザーのセッションに10〜50のサービスが関与することがある。OPは各RPのbackchannel_logout_uriにHTTP POSTを送らなければならない。

仕様は、OPが失敗時に「適切な時間を置いてリトライすべき(SHOULD)」と述べている。具体的なリトライ回数、バックオフ戦略、最大待ち時間は定義されていない。

さらに、RPが複数インスタンスで動いている場合。Back-Channel LogoutのPOSTはロードバランサー経由で1つのインスタンスに届く。そのインスタンスは他のインスタンスにどう伝播させるのか? 共有セッションストア(Redis)か、イベントバス(Kafka、Redis Pub/Sub)か。仕様はこの問題に一切触れていない。RPの責任だ。

仕様の狭間

ホテルの話に戻ろう。フロントから各施設への内線電話——Back-Channel Logoutの仕組み自体は理解できた。しかし、実際にこの内線電話システムを運用しようとすると、仕様書に書かれていない問題に次々とぶつかる。

Back-Channel Logout 1.0は2022年9月にFinalになった。仕様としては「完成」している。しかし実際に実装すると、仕様が踏み込んでいない空白地帯がある。

リトライセマンティクスの不在

RPのエンドポイントがダウンしていたらどうするか。仕様は「リトライすべき」としか言わない。現実には、Auth0は5秒でタイムアウトして失敗を記録する。Keycloakは同期的に処理する。ZITADELは非同期キューに入れる。各実装がバラバラだ。

確認応答プロトコルの欠如

RPはHTTP 200(成功)か400(失敗)を返す。それだけだ。「処理中」「部分的に成功」「後で処理する」を表現する手段がない。デッドレターキューもない。保証された配信もない。

subのみのLogout Tokenの危険性

Logout Tokenにsubだけが含まれsidがない場合、RPはそのユーザーの全セッションを破棄しなければならない。ユーザーが複数デバイスでログインしていた場合、一つのデバイスでログアウトしたら全デバイスが道連れになる。意図しない挙動だが、仕様上は正しい。

トークン失効との順序問題

RFC 7009のToken Revocation EndpointとBack-Channel Logoutの実行順序が未定義だ。Keycloakで報告された事例(keycloak/keycloak#34234)が象徴的で、リフレッシュトークンを失効させるとセッションが破壊され、その後のend_session_endpoint呼び出しでセッションが見つからず、Back-Channel Logoutが発火しない。Keycloakはこれを「期待される動作」として閉じた。仕様が順序を定義していない以上、どちらの解釈も正しい。

カスケードログアウト

RPが下流のOPとしても機能するフェデレーション構成では、ログアウトを下流に伝播させる必要がある。仕様は「カスケードログアウトが望ましい」と述べるだけで、伝播のプロトコル、タイムアウト、失敗ハンドリングを定めていない。

各社の温度差

仕様がFinalになって3年以上。各社の実装状況には、驚くほどの温度差がある。

プロバイダBack-Channel LogoutFront-Channel Logout備考
Keycloak最も包括的なOSS実装。OP/RP両対応
Auth0Enterpriseプラン限定。5秒タイムアウト
Okta (Workforce)ITPによるリスクベースのセッション終了で代替
Entra IDバックチャンネル未対応。対応予定も不明
AWS CognitoOIDC標準のログアウト自体が未対応
GoogleSSF/CAEPのクローズドベータに移行中
PingFederateOP/RP両対応。JWE暗号化も対応

Keycloak——先行者の苦労

Keycloakはバックチャンネルログアウトの最も成熟したオープンソース実装だ。OPとしてRPに通知を送ることも、RPとしてアップストリームのIdPから通知を受けることもできる。

しかし成熟しているがゆえに、エッジケースのバグが可視化されている。

  • Logout Tokenにexpクレームが含まれていなかった(22.0.8で修正)
  • 同一クライアントの複数セッションに対してログアウトリクエストが1つしか送られない(未解決)
  • 「全セッションのサインアウト」がバックチャンネルログアウトを発火しない——数百万セッションのイテレーションがスケールしないため、意図的に対応しない判断(Won't Fix)
  • 複数クライアントが異なる署名アルゴリズムを使っている場合に、間違ったアルゴリズムで署名される(26.3.3で修正)

Auth0——Enterpriseの壁

Auth0は2023年にバックチャンネルログアウトに対応した。ただしEnterpriseプラン限定。RPはsidクレームをログイン時に保存しておく必要がある。

Enterpriseプラン限定にした理由は推測できる。バックチャンネルログアウトはOPからRPへの能動的なHTTPコールが必要で、OP側のインフラコストが跳ね上がる。Auth0のようなマルチテナントSaaSでは、全テナントに無条件で提供するにはコストが見合わない。

OktaのIdentity Threat Protection(ITP)との連携も進んでおり、リスク検知時にセッションを一括終了する仕組みが整備されつつある。バックチャンネルログアウトの先にある、リアルタイムのリスクベース制御を目指す動きだ。

Okta Workforce——独自路線

Okta WorkforceはOIDCアプリケーションに対するバックチャンネルログアウトをネイティブにサポートしていない。Front-Channel Single Logoutは提供している(Early Access機能)。

代わりに、Identity Threat Protection(ITP)を通じて、脅威検知時にアプリケーション横断でセッションを即座に終了させる機能を提供している。2024年8月にITPが一般提供(GA)になり、Google Workspace、Slack、Salesforceなどの主要アプリに対して、リスクベースのセッション終了が可能になった。

標準のOIDC Back-Channel Logoutではなく、リスクベースのリアルタイム制御に投資している点が、Oktaの戦略的判断を示している。

Entra ID——フロントチャンネルのみ

MicrosoftのEntra ID(旧Azure AD)はバックチャンネルログアウトをサポートしていない。frontchannel_logout_uriによるフロントチャンネルログアウトのみ提供する。2025年3月時点でもMicrosoft Q&Aでバックチャンネルログアウト対応について質問が上がっており、明確な回答はない。

Microsoftのエコシステムでは、アプリケーションがEntra IDと同一ドメインまたは同一テナント内に閉じるケースが多い。その前提ではフロントチャンネルでも十分機能するため、バックチャンネルへの投資優先度が低いのだろう。クロスドメインのマイクロサービス構成では、フロントチャンネルのコールバックからAzure SignalRやAzure Web PubSubを経由してサーバーサイドに伝播させる、というワークアラウンドが取られている。

AWS Cognito——標準ログアウト自体が不在

Cognitoの状況は際立っている。OIDCのend_session_endpointをディスカバリドキュメントに含めていない。RP-Initiated Logoutすらサポートしていない。ログアウトは独自の/logoutエンドポイント(GETのみ)で、Cognitoのセッションをクリアするだけ。外部IdPからのサインアウトは行われない。SAML SLOは対応しているのに、OIDC SLOは未対応という非対称性がある。

CognitoはもともとモバイルアプリのBaaS(Backend as a Service)として生まれたサービスだ。モバイルアプリではブラウザセッションの概念が薄く、トークンの有効期限管理がログアウトの代替として機能する。OIDC SLOの優先度が低い背景にはこの出自がある。

Google——次世代に賭ける

SSF / CAEPとは

Shared Signals Framework(SSF)は、セキュリティイベントをサービス間でリアルタイムに共有するためのPub/Sub仕様。Continuous Access Evaluation Profile(CAEP)はSSF上で定義されるイベントの一つで、session-revoked(セッション失効)などのイベントタイプを持つ。Back-Channel Logoutが「ログアウト時にHTTP POSTを投げる」一回限りの通知なのに対し、SSFは継続的なイベントストリームとして設計されている。

GoogleはOIDCのバックチャンネルログアウトではなく、SSFとCAEPに向かっている。Google WorkspaceのSSF統合はクローズドベータで提供されており、CAEPのsession-revokedイベントがバックチャンネルログアウトに相当する機能を担う。

SSFはPub/Subモデルで、Back-Channel Logoutのfire-and-forget HTTP POSTよりも堅牢な配信メカニズムを持つ。Apple、Google、IBM、Oktaが参加しており、長期的にはBack-Channel Logoutを置き換える可能性がある。2025年9月にFinal仕様が公開された。

現実的な対処

完璧なソリューションはない。あるのはトレードオフの組み合わせだ。

JWTの有効期限を短くする。5分、あるいは30秒。有効期限が短いほどレースコンディションの窓は小さくなる。ただし、リフレッシュの頻度が上がり、OPへの負荷が増える。オペークトークンの負荷パターンに近づいていく。

機密性の高い操作だけIntrospectionを挟む。通常のAPIコールはJWTのローカル検証で通す。決済やデータエクスポートのような高リスク操作だけ、OPのIntrospectionエンドポイントに問い合わせる。二段構えだ。

共有セッションストアを置く。Redisやデータベースにセッション状態を集約し、バックチャンネルログアウト通知が来たらストアを更新する。全RPインスタンスが同じストアを見るので、伝播の問題は緩和される。ただしストアがSPOFになる。

イベントバスで伝播させる。KafkaやRedis Pub/Subでログアウトイベントを全インスタンスに配信する。共有ストアより疎結合だが、eventually consistentになる。

戦略ログアウトの即時性OPへの負荷実装の複雑さ
JWT短期限 + リフレッシュ数分以内
機密操作だけIntrospection操作単位で即時低〜中
共有セッションストアほぼ即時
イベントバス秒単位
オペークトークン全面採用即時

トレードオフの先

OIDCのバックチャンネルログアウトは、分散システムにおける古典的な問題——「全ノードに即座に状態変更を伝播させる」——の、認証という文脈での表出だ。

仕様はFinalだが、万能ではない。リトライポリシーは未定義、確認応答は貧弱、カスケード伝播は「望ましい」止まり。各クラウドプロバイダの対応状況はバラバラで、CognitoはOIDC標準のログアウト自体を持たず、Entra IDはフロントチャンネルのみ、GoogleはSSF/CAEPという別の未来に賭けている。

JWTの「RP側で検証が完結する」という分散の強みと、「ログアウトを即座に全体に反映させたい」という集中の要求。この二つは本質的に相容れない。どちらかを選ぶのではなく、自分のシステムがどの程度の「ログアウトの遅延」を許容できるかを見極めて、トレードオフを配置するしかない。

正解は、まだない。