Skip to content

セッションとログアウト

セッション とは、「このブラウザは時刻 T、保証レベル A で、ユーザ X として認証された」という OP 側の状態です。小さな暗号化 cookie が、ブラウザをその状態に紐付けます。ログアウト はその状態を破棄することを意味します — そして任意で、その状態に依存していた RP に「ユーザは去った」ことを通知します。

このページで触れる仕様

30 秒のメンタルモデル

  • セッションは OP 側 に生きていて、cookie の中ではありません。
  • cookie はセッション行を指す暗号化されたポインタにすぎません。
  • 「ログアウト」とはその行を消すこと。任意で RP に通知を fan-out できます。
  • cookie だけ消した場合は OP がユーザを忘れます。行だけ消した場合は次のリクエストで cookie が空のセッションを指し、ユーザは再認証を強制されます。

本ライブラリでのセッション表現

OP は store.SessionStore に次の行を保持します。

フィールド内容
ID不透明な OP ブラウザセッション識別子。cookie / セッション検索と監査相関に使う値ですが、現在の sub-only Back-Channel Logout Token では RP 固有の sid claim として発行されません。
SubjectOP 内部の安定したユーザ ID。ID トークンの sub になります。
AuthTimeユーザがいつ認証したか。ID トークンの auth_time になります。
ACRAuthentication Context Class Reference — セッションが満たす保証レベル。
AMRAuthentication Methods References(RFC 8176) — pwdotpmfahwk、…
ChooserGroupIDマルチアカウントチューザのグループ識別子。同じブラウザに属する複数セッションが共有します。
ExpiresAtCreatedAtUpdatedAtライフサイクルのタイムスタンプ。

この行を指す cookie が __Host-oidc_session です(internal/cookie/profile.go で定義)。本ライブラリは合計 4 つの cookie を使います。

Cookie用途仕様
__Host-oidc_session永続セッションへのポインタ。不透明な payload を AES-256-GCM で AEAD 暗号化。__Host- 接頭辞で同一 origin に強制。SameSite=Lax
__Host-oidc_interaction進行中のインタラクション(ログインフォーム、MFA チャレンジ)の状態。同じ AEAD。1 時間 TTL。
__Host-oidc_csrfインタラクションフォームの double-submit CSRF トークン。HMAC のみ(AEAD なし)。SameSite=Strict
__Host-oidc_localeインタラクション画面をまたいでユーザが選んだ UI ロケールを覚えておく。平文(暗号化なし)。1 年 TTL。SameSite=Lax
__Host- 接頭辞とは

RFC 6265bis により、cookie 名が __Host- で始まるとき、ブラウザは SecurePath=/、そして Domain 属性 なし を同時に満たさないと cookie を受理しません — つまり、OP の origin に厳密に縛られます。サブドメイン侵害でも偽造できず、兄弟ドメインからも読めません。本ライブラリが平文 HTTP では起動しないのは、まさに __Host- 接頭辞がそれでは成立しないからです。

セッション行は store.SessionStore を経由します — 組み込み側がライブラリの不変条件を破ることなく、揮発性のバックエンド(Redis、Memcached)に振り向けてもよい substore です。トレードオフは 設計判断 #10 を参照。

ログアウトの分類

ログアウトに関わる OIDC 仕様は 3 つあります。本ライブラリは 2 つを実装します。

RP-Initiated Logout 1.0

RP はブラウザを次の URL にリダイレクトします。

GET /end_session?id_token_hint=<id_token>&post_logout_redirect_uri=<uri>&state=<opaque>

OP は次のことを行います。

  1. id_token_hint を検証(自分が発行したセッションに紐付くか確認)。
  2. 任意で「本当にログアウトしますか?」の確認画面を挟む(UX として推奨)。
  3. logout_scope を解決し、選択されたセッション行を削除。
  4. post_logout_redirect_uri がそのクライアントに登録されていれば、state を反射させてブラウザをリダイレクト。

デフォルトの scope は、アクティブな chooser group 全体です。ブラウザの group にあるすべてのセッションを破棄します。単一値のクエリパラメータ logout_scope=current を指定すると、アクティブなセッションだけを破棄し、兄弟セッションが残れば cookie をそのセッションへ再バインドします。空値、未知の値、logout_scope の重複は、セッション状態を変更する前に HTTP 400 で拒否します。

セッション cookie の subject と一致しない id_token_hint では確認画面を省略できません。セッションストアの transport 障害がセッションの解決または破棄中に起きた場合、/end_session は静的な HTTP 503 ページを返します。post_logout_redirect_uri へはリダイレクトせず、cookie とセッション行をそのまま残します。セッションが存在しない、または期限切れの場合は別の安全に継続できるケースです。

/end_session が返す確認画面とブラウザ向けエラー HTML には、X-Frame-Options: DENYframe-ancestors 'none' を含む Content-Security-Policy の両方が付くため、どちらの画面も埋め込めません。

native / public client では、redirect_uri と同じ RFC 8252 の loopback any-port ルールを post_logout_redirect_uri にも適用します。固定の loopback URI を登録しておき、logout 時には別の ephemeral port で戻る形を許容します。変動できるのは port だけで、host / scheme / path は登録形状と一致している必要があります。

主眼は このブラウザの OP セッションを終わらせる ことです。リダイレクトを起点にした RP は当然ログアウトを把握していますが、他の RP は — OP が Back-Channel Logout も同時に動かしていない限り — 把握できません。

Back-Channel Logout 1.0

/end_session がセッションを終了すると、OP はその subject の grant から導出した対象 RP それぞれに、署名された logout_token JWT を POST します。RP は JWT を検証し、自身のローカルセッションを無効化します。

これは server-to-server で動きます。ブラウザは関与しないので、ユーザがタブを閉じていても、別のブラウザに移っていても動作します。公開されているセッションログアウトの入口は /end_session です。

外向き HTTP リクエストは、JWKS / sector_identifier_uri 取得と同じ SSRF 拒否リストでガードされます — 組み込み側が明示的にオプトインしない限り、private network には到達しません。失敗は op.AuditLogoutBackChannelFailed、成功は op.AuditLogoutBackChannelDelivered で記録されます。session-bearing logout notice の grant 由来 RP audience が空の場合は op.AuditBCLNoSessionsForSubject が出ます — 「RP の対象を解決できなかった」と「配送に失敗した」を区別するのに有用です。

detached fan-out には独立した 2 つの時間制限があります。op.WithBackchannelLogoutTimeout は 1 RP への HTTP 配送(デフォルト 5 秒)を制限し、op.WithBackchannelFanOutBudget は detached fan-out 全体(デフォルト 30 秒)を制限します。後者は RP ごとの timeout ではなく、イベント全体の予算です。実行中の fan-out は Provider.Shutdown(ctx) でも drain されます。

揮発セッション下では Back-Channel Logout は best-effort

揮発な store.SessionStore/end_session の解決と snapshot 取得より前に行を追い出すと、ブラウザからの logout trigger を失い detached fan-out は起動しません。snapshot を取得できた場合、RP audience は SessionStore の行を辿らず grant store から解決されます。session-bearing notice で grant 由来 RP 対象が 0 件の場合だけ、対象 0 件として監査されます。op.SessionDurabilityPosture ノブは trigger / snapshot の喪失と対象 0 件をダッシュボードで解釈する文脈を提供します。詳細は 設計判断 #10 を参照。

Front-Channel Logout 1.0 — 非実装

Front-Channel Logout は、OP が RP ごとの frontchannel_logout_uri をひとつずつ <iframe> で並べた HTML を返し、各 iframe が third-party context で自身の cookie を読みに行って消す、という仕組みです。この仕組みは「third-party iframe が embedded context から自身の cookie を読める」という前提に依存しており、主要ブラウザは段階的にこの能力を取り去ってきました。

  • Safari ITP(2017 年〜)
  • Firefox ETP(2019 年〜)
  • Chrome の SameSite=Lax デフォルト化(2020 年〜)
  • third-party cookie の段階的廃止(2024〜2025 年)

本ライブラリは Front-Channel Logout を 実装しません。discovery 文書でも frontchannel_logout_supported は広告しません。fan-out 型のログアウトが必要な組み込み側は、ブラウザの cookie ポリシーから独立した server-to-server の Back-Channel Logout 1.0 を使います。完全な背景は 設計判断 #5 を参照。

End-session カスケード

/end_session は単に「cookie を消す」だけではありません。組み込み側が GrantsAccessTokens の substore を組み込んでいる場合、本ライブラリは subject が保持するすべての grant を歩き、grant ごとのアクセストークン shadow row を失効させます。JWT アクセストークンは OP が応答するエンドポイント(/userinfo/introspect)で inactive になります。opaque アクセストークンは introspection を経由するすべての RS で inactive になります。

セッション行ひとつと、終了がどこまで届くか
上段はセッションモデル。OP オリジンに紐づく 4 つの cookie のうち 1 つが SessionStore の行 ID を運び、OP 側の状態はその行が持ちます。下段は end-session のカスケード。行を削除し、grant を走査して失効させ、アクセストークンを失効させ、登録済みの back-channel logout URI すべてに logout_token を POST します。セッションモデルブラウザの cookie__Host-oidc_session__Host-oidc_interaction__Host-oidc_csrf__Host-oidc_localeOP オリジンに紐づき、RP には渡らない行 ID を運ぶSessionStoreセッションごとに 1 行 — OP 側の状態はすべてここID · Subject · AuthTime · ACRAMR · ChooserGroupID · timestampscookie が持つのは行 ID だけend-session カスケード/end_sessionRP 起点のログアウトSessionStore行を削除するGrants走査して revoked に切り替えるAccessTokensOP が直接失効させるBack-Channel LogoutPOST logout_token → 各 RP実際にどこまで届くかJWT アクセストークンは OP が/userinfo と /introspect で拒否するopaque なトークンは各 RS が introspectして初めて inactive になる到達範囲は OP ではなく各 RS 次第
左の列は同期的で、すべて OP の制御下にあります。下 2 行はそうではありません。すでに配られた JWT は有効期限まで形式上は正当なままですし、バックチャネル通知が効くのは URI を登録して応答する RP に限られます。

cookie は単なるポインタで、セッションの実体は SessionStore の行です。ブラウザ側で cookie を消しても行は残るため、OP が行そのものを削除する必要があります。

組み込まれた storeカスケード挙動
Grants + AccessTokens(同梱アダプタを使えばデフォルト)カスケード発火。AT が revoked に切り替わる。JWT AT は /userinfo で拒否、opaque AT は全 RS で拒否。
いずれかが nilカスケードは静かに短絡。AT は自然に exp まで生存。

理由と JWT / opaque のカスケード到達範囲の非対称性は 設計判断 #17 を参照。

揮発ストアと永続ストアの選択

セッションの substore は、トランザクショナルストア(認可コード、リフレッシュトークン、client)から意図的に分離されています。組み込み側は典型的には次のいずれかを選びます。

方針SessionStore のバックエンドトレードオフ
Hot/cold 分離(高トラフィック向けに推奨)Redis(揮発)セッション書き込みのレイテンシが低い。追い出しとログアウトが競合すると BCL は best-effort。
すべて永続トランザクショナルストアと同じ SQL クラスタBCL 配送は完全配送が前提。セッション書き込みはトークン書き込みとレイテンシを共有。

op.WithSessionDurabilityPosture(...) で組み込み側のスタンスを宣言することで、監査トレイル(op.AuditBCLNoSessionsForSubject)を正しく解釈できます。完全な実装例は Hot / Cold 分離の使い方 を参照。

graceful shutdown では、まず HTTP トラフィックの受付を止め、その後 provider の detached 処理を drain します。

go
srv.Shutdown(ctx)      // 受付を停止
provider.Shutdown(ctx) // detached fan-out を drain

セッションライフサイクルの監査イベント

イベント発火タイミング
op.AuditSessionCreated新規セッション発行。
op.AuditSessionDestroyedセッション行が削除された(ログアウト、追い出し、GC)。
op.AuditSessionAlreadyAbsentログアウト対象のセッションがすでに存在しない、または期限切れだった。
op.AuditSessionDestroyFailedセッション削除がストア障害になった。リクエストは静的 503 となり、状態は保持される。
op.AuditLogoutClientLookupFailedlogout client の解決中に client registry への lookup が障害になった。通信路上は一律の拒否で、正常な「client が存在しない」結果では発火しない。
op.AuditLogoutRPInitiated予約語 — OP は RP が要求したことだけを示すイベントを発火しない。
op.AuditLogoutTokenRevokeFailedlogout token またはログアウトカスケードの失効処理がストア障害になった。
op.AuditLogoutBackChannelDeliveredRP が logout_token の POST に 2xx を返した。
op.AuditLogoutBackChannelFailedRP が non-2xx を返した、ネットワークエラー、または拒否リストで URL が遮断された。
op.AuditLogoutBackChannelResolveFailedgrant ベースの配送先解決に失敗した。
op.AuditLogoutBackChannelOverflow上限付き配送先解決が overflow した。
op.AuditBCLNoSessionsForSubjectsession-bearing /end_session notice で grant 由来の RP 対象が 0 件だった。

op.AuditLogoutRPInitiated は予約されたカタログ語彙です。OP は個別の「RP が要求した」イベントではなく、結果としてのセッション/配送イベントを記録します。

次に読む

  • 使い方: Back-Channel Logout — RP の backchannel_logout_uri の設定、署名鍵の選択、logout_token の payload。
  • 設計判断 — #5、#10、#17 がログアウト方針の背後にある明示的な解釈を扱っています。
  • 運用: マルチインスタンス — 揮発ストアでセッションを共有しつつロードバランサ越しに OP を動かす場合の話。