使い方 — バックチャネルログアウト
そもそも「バックチャネルログアウト」とは?
ユーザは「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 に依存しており、現代のブラウザでは段階的に動かなくなりつつあります。バックチャネル方式が現実的な選択肢です。
このページで触れる仕様
- OpenID Connect Back-Channel Logout 1.0
- RFC 7519 — JWT(logout token の形式)
- RFC 8417 — Security Event Token (SET)(
eventsclaim の形式) - RFC 1918 — プライベート IPv4 範囲(後述の SSRF 防御で使用)
用語の補足
logout_token— OP が署名して各 RP に POST する短寿命の JWT です。終了したセッションの subject (sub) を運びます。アクセストークンとは別物で、RP は検証後に自身のローカルセッションを破棄するだけです。- SET(Security Event Token、RFC 8417) — セキュリティイベント配送向けの JWT 形式です。
eventsclaim にイベント種別キー(ここではhttp://schemas.openid.net/event/backchannel-logout)を入れることで、汎用 SET 受信側が適切なハンドラに振り分けられるよう設計されています。
アーキテクチャ
OP は RP 毎に logout_token に署名して RP の backchannel_logout_uri に POST します。トークンの中身は次のとおりです。
| Claim | 意味 |
|---|---|
iss | OP issuer |
aud | RP の client_id |
iat、jti | 発行時刻 + replay nonce |
sub | 終了したセッションの subject。sid は発行されません(後述) |
events | {"http://schemas.openid.net/event/backchannel-logout": {}} |
RP は署名と aud を検証し、ローカルセッションを破棄したうえで 200 を返します。
実装
クライアント別の BackchannelLogoutURI で RP ごとに有効化します。
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.ConfidentialClient と op.PrivateKeyJWTClient にも同じ名前で存在します。いずれの型付きクライアント定義からも有効化できます。
ライブラリ全体のオプション:
op.New(
/* ... */
op.WithBackchannelLogoutHTTPClient(myHTTPClient), // mTLS / カスタム timeout
op.WithBackchannelLogoutTimeout(5 * time.Second),
)loopback 上に stub RP を立てるローカル demo や CI fixture では、loopback の backchannel_logout_uri に限って plain HTTP を許容できます。
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_uri は https 必須、fragment 不可、userinfo 不可、host 必須です。https://attacker:internal@rp.example.com/... も https://rp.example.com/cb#anchor も invalid_client_metadata で弾かれます。backchannel_logout_session_required=true と空の URI の組み合わせも拒否します。配送先を持たないクライアントが sid 配送を有効化できないようにするためです。
// 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 の内側に置くときは明示的に許可します。
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_supported は false
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.SessionDurabilityPosture(SessionDurabilityVolatile または SessionDurabilityDurable)を載せておくことで、SOC ダッシュボードはストアアダプタの型に依存せず両者を区別できます。
フロントチャネルログアウト(別の機構)
OIDC Front-Channel Logout 1.0(ブラウザ側 iframe 一斉通知)は別仕様で、ライブラリは意図的に実装していません。Back-channel が配備可能な選択です — 第三者 cookie に依存せず、origin を跨いで動作し、一斉通知 時にユーザのブラウザが開いている必要もありません。