Skip to content

使い方 — バックチャネルログアウト

そもそも「バックチャネルログアウト」とは?

ユーザは「Acme でサインイン」ボタンを介して、同じ OP に紐づく複数の RP にサインインしているのが普通です。あるアプリ(RP A)で ログアウト をクリックしても、他の RP B / RP C はそれぞれローカル cookie を保持したままなので、「アプリ A ではログアウトしたのにアプリ B ではログイン状態のまま」というズレが残ります。

バックチャネルログアウト は、このズレを OP 側から閉じる 一斉通知 機構です。各 RP は OP に対してサーバサイドのコールバック URL を事前登録しておきます。セッション終了時、OP は 署名済み logout_token を各 RP の URL に直接 POST します(ブラウザを経由しない=バックチャネル)。RP はトークンを検証してローカル cookie を破棄します。

対になる仕組みとして フロントチャネルログアウト もありますが、こちらは <iframe> とサードパーティ cookie に依存しており、現代のブラウザでは段階的に動かなくなりつつあります。バックチャネル方式が現実的な選択肢です。

このページで触れる仕様
用語の補足
  • logout_token — OP が署名して各 RP に POST する短寿命の JWT です。終了したセッションの subject (sub) を運びます。アクセストークンとは別物で、RP は検証後に自身のローカルセッションを破棄するだけです。
  • SET(Security Event Token、RFC 8417) — セキュリティイベント配送向けの JWT 形式です。events claim にイベント種別キー(ここでは http://schemas.openid.net/event/backchannel-logout)を入れることで、汎用 SET 受信側が適切なハンドラに振り分けられるよう設計されています。

ソース: examples/42-back-channel-logout

アーキテクチャ

バックチャネルログアウトのシーケンス: RP A が /end_session を起動し、OP がセッションを終了して署名済み logout token を RP B と RP C に 一斉通知 したうえで RP A へリダイレクトする。ユーザRP A起動側OPRP BRP C「ログアウト」をクリック1リダイレクト先/end_session?id_token_hint=…2セッション終了3セッション内の全 RP に 一斉通知POST backchannel_logout_uri · logout_token = 署名 JWT4520062007302 post_logout_redirect_uri8

OP は RP 毎に logout_token に署名して RP の backchannel_logout_uri に POST します。トークンの中身は次のとおりです。

Claim意味
issOP issuer
audRP の client_id
iatjti発行時刻 + replay nonce
sub終了したセッションの subject。sid は発行されません(後述)
events{"http://schemas.openid.net/event/backchannel-logout": {}}

RP は署名と aud を検証し、ローカルセッションを破棄したうえで 200 を返します。

実装

クライアント別の BackchannelLogoutURI で RP ごとに有効化します。

go
op.WithStaticClients(op.PublicClient{
  ID:                               "rp-a",
  RedirectURIs:                     []string{"https://rp-a.example.com/callback"},
  Scopes:                           []string{"openid", "profile"},
  BackchannelLogoutURI:             "https://rp-a.example.com/oidc/backchannel-logout",
  BackchannelLogoutSessionRequired: true, // logout token に "sid" claim を要求
})

BackchannelLogoutURI フィールドは op.ConfidentialClientop.PrivateKeyJWTClient にも同じ名前で存在します。いずれの型付きクライアント定義からも有効化できます。

ライブラリ全体のオプション:

go
op.New(
  /* ... */
  op.WithBackchannelLogoutHTTPClient(myHTTPClient), // mTLS / カスタム timeout
  op.WithBackchannelLogoutTimeout(5 * time.Second),
)

loopback 上に stub RP を立てるローカル demo や CI fixture では、loopback の backchannel_logout_uri に限って plain HTTP を許容できます。

go
op.WithAllowInsecureBackchannelLogoutForDev()

このオプションは、登録時の URL 検証と実行時の SSRF 判定の両方を 127.0.0.1[::1]localhost に限って緩和します。本番用の近道ではありません。public host と loopback 以外の private network は、下記の本番向け方針を別途明示する必要があります。

SSRF 防御

既定でプライベートネットワーク宛先を拒否

配送処理は、host が loopback / link-local / RFC 1918 / IPv6 ULA に解決される backchannel_logout_uri への POST を 拒否 します。これがないと、任意 URL を登録できる RP が OP の内部ネットワークへの SSRF オラクルになります。

接続先アドレスの拒否リストに加えて、登録時にも URL の形を検査します。backchannel_logout_urihttps 必須、fragment 不可、userinfo 不可、host 必須です。https://attacker:internal@rp.example.com/...https://rp.example.com/cb#anchorinvalid_client_metadata で弾かれます。backchannel_logout_session_required=true と空の URI の組み合わせも拒否します。配送先を持たないクライアントが sid 配送を有効化できないようにするためです。

jsonc
// NG: userinfo と fragment は登録時に拒否
{
  "backchannel_logout_uri": "https://attacker:internal@rp.example.com/logout#sid",
  "backchannel_logout_session_required": true
}

// NG: sid 配送を要求しているのに配送先がない
{
  "backchannel_logout_session_required": true
}

// OK: public HTTPS の配送先を明示する
{
  "backchannel_logout_uri": "https://rp.example.com/backchannel-logout",
  "backchannel_logout_session_required": true
}

RP を private DNS の内側に置くときは明示的に許可します。

go
op.WithBackchannelAllowPrivateNetwork(true)

この緩和は意図的に選び取る必要があります — オプションを明示的に存在させることで、セキュリティ上のトレードオフが設定箇所に可視化されます。

通知先はどう解決されるか(そしてなぜ上限があるか)

一斉通知の通知先は session の行ではなく grant から解決されます。coordinator は終了する session の subject を受け取り、その subject が同意している client の集合を grant ストアに問い合わせます。使うのは store.GrantClientLister.ListClientIDsBySubject で、これは ListBySubject とは別の keyset ページング付きのビューです。1 つの subject が同じ client に対して過去分を含む多数の grant 行を持ちうるため、専用のビューを置いています。一斉通知の各段階には意図的に上限が設けてあります。

上限既定値何を抑えるか
重複排除後の通知先DefaultMaxTargets(256)1 回のログアウトで通知する client 数。後段で絞るのではなく grant クエリ自体に上限を効かせる
同時配送数DefaultMaxConcurrentDeliveries(8)同時に走る outbound POST の本数

通知先ページに NextCursor が付いて返った場合、上限を超える client が該当したということです。coordinator は黙って切り捨てず、その cursor を載せた overflow の監査イベントを出します。到達できない RP が 1 つあっても、一斉通知全体は失敗せず、その RP 個別の監査イベントとして現れます。

backchannel_logout_session_supportedfalse

discovery はこれを false として広告します。これは grant ベースの解決から直接導かれる帰結です。OP 側の session 識別子が特定の RP のものであることを OP は証明できないため、sid を Logout Token に載せることはありません。RP は sub を手がかりにローカル session を破棄してください。backchannel_logout_session_required=true を登録する client は、OP が出さないものを要求していることになります。

通知先が 0 件になったとき

subject が有効な grant を 1 つも持っていない場合(すべて失効済み、あるいは client が既に存在しない古いレコードだけ)、一斉通知には通知先がありません。本ライブラリはこれを監査イベントとして表面化させます。

イベント意味
op.AuditBCLNoSessionsForSubject呼出側がセッションを指定(id_token_hint 付き /end_session または Provider.Logout)したが、一斉通知 で解決した RP が 0 件だった。

このイベントは呼出側が実際に session を指定した場合にだけ発火するので、browser session を持たない subject に対する Provider.Logout がノイズを生むことはありません。揮発 session 配置では、通知先 0 件は OIDC Back-Channel Logout 1.0 §2.7 の "best effort" の下限です。永続配置では予期せぬギャップを意味し、アラートに値します。イベント extras に設定済みの op.SessionDurabilityPostureSessionDurabilityVolatile または SessionDurabilityDurable)を載せておくことで、SOC ダッシュボードはストアアダプタの型に依存せず両者を区別できます。

フロントチャネルログアウト(別の機構)

OIDC Front-Channel Logout 1.0(ブラウザ側 iframe 一斉通知)は別仕様で、ライブラリは意図的に実装していません。Back-channel が配備可能な選択です — 第三者 cookie に依存せず、origin を跨いで動作し、一斉通知 時にユーザのブラウザが開いている必要もありません。