概要
nannany です。
この記事ではOpenID Connect Session Managementについての説明と、実際にサンプルを実装して得た気づきを記述しました。
Authorization Code Grantがどのようなフローで行われるかなど、OIDCのベースとなる説明は割愛しています。
OpenID Connect Session Management とは?
OpenID Connect Session Management は OpenID Foundation が定めた仕様の1つです。
仕様書のうち、その内容を端的に表している箇所が下記になります。
This specification complements the OpenID Connect Core 1.0 [OpenID.Core] specification by defining how to monitor the End-User's login status at the OpenID Provider on an ongoing basis so that the Relying Party can log out an End-User who has logged out of the OpenID Provider.
この仕様は、OpenID Connect Core 1.0 [OpenID.Core]を補完するものであり、 OpenID Providerにおけるエンドユーザのログイン状態を継続的に監視する方法を定義することで、 Relying PartyがOpenID Providerからログアウトしたエンドユーザをログアウトできるようにする。
つまり、OpenID Connect Session Management は、OpenID Provider(以下、OP)でのログイン状態を継続的に監視することで、OPからログアウトしたユーザーをRelying Party(以下、RP)にてログアウトできるようにする仕様と言えます。
OpenID Connect Session Management 概観
OpenID Connect Session Management に登場する概念を図示すると下記のようになります。
RP のフロントエンドに RP iframe と OP iframe を埋め込み、Window.postMessage()を利用してiframe間の情報交換を行います。
postMessage
を使っているのは、RP iframe と OP iframe の origin が異なるためです。
RPのクライアントサイドストレージには、session_state
を保持します。session_state
はエンドユーザーのOPにおけるログイン状態を表現する値ですが、RP側からみたら不透明(opaque)な値です。また、このsession_state
はOpenID Connect Session Management をサポートしている場合は、Authentication Response の中に含まれています。
OPのクライアントサイドストレージには、User Agent State
を保持します。User Agent State
はOP側でsession_state
に変換することができます。例えば、 session_state = SHA256(client_id + ' ' + RPのorigin + ' ' + User Agent State + ' ' + salt) + "." + salt
のような形でsession_state
を算出することができます。このsession_state
の算出式は仕様で定められているものではなく、実装に依存します。
処理フロー
RP iframeは任意のタイミングでOP iframeにエンドユーザーのセッション状態をpostMessage
を使って問い合わせます
postMessage
にはsession_state
の値を含めます。
OP iframeはRPがpostMessageで送ってきたイベントを受け取ったら、RPから受け取ったsession_state
と、User Agent State
から算出したsession_state
が等しいか計算します。
RPから受け取った session_state == User Agent Stateから算出した session_state
等しい場合、OPのエンドユーザーのログイン状態は変化がないので unchanged
を、等しくない場合は changed
をRPにpostMessageで通知します。
RP iframeは changed
を受け取った場合は、promtp=none
でOIDCの認証をし、エンドユーザーのOPでのログイン状態と同期するようにします。
graph TD A[RP:処理発火] --> B(RP:OPにログイン状態問い合わせ) B --> C{OP:ログイン状態に変化ある?} C -->|changed| D[RP:prompt=noneを実行] C -->|unchanged| E[RP:何もしない] D --> F[RP:session_stateを更新]
OpenID Connect Session Management の良いところは?
この仕様の良さは、OPとRPのログイン状態同期において、極力フロントエンド、バックエンド間の通信をしないところだと考えています。
RPからページを遷移するたびにprompt=noneを実行して、ログイン状態の同期を行うにこともできますが、その場合はかなりの数のフロントエンド、バックエンド間の通信が走ってしまいます。(このことについては仕様の本文にも言及がある)
RP iframe の実装例
公式文章に RP iframe の実装例が掲載されています。
var stat = "unchanged"; var mes = client_id + " " + session_state; var targetOrigin = "https://server.example.com"; // Validates origin var opFrameId = "op"; var timerID; function check_session() { var win = window.parent.frames[opFrameId].contentWindow win.postMessage(mes, targetOrigin); } function setTimer() { check_session(); timerID = setInterval(check_session, 5 * 1000); } window.addEventListener("message", receiveMessage, false); function receiveMessage(e) { if (e.origin !== targetOrigin) { return; } stat = e.data; if (stat === "changed") { clearInterval(timerID); // then take the actions below... } } setTimer();
この例は5秒に1回 OP iframe に問い合わせて、changed
が返ってきたら何かアクションを取る、というものになっています。
OP iframe の実装例
こちらも公式文章に実装例が掲載されています。
window.addEventListener("message", receiveMessage, false); function receiveMessage(e){ // e.data has client_id and session_state var client_id = e.data.substr(0, e.data.lastIndexOf(' ')); var session_state = e.data.substr(e.data.lastIndexOf(' ') + 1); var salt = session_state.split('.')[1]; // if message is syntactically invalid // postMessage('error', e.origin) and return // if message comes an unexpected origin // postMessage('error', e.origin) and return // get_op_user_agent_state() is an OP defined function // that returns the User Agent's login status at the OP. // How it is done is entirely up to the OP. var opuas = get_op_user_agent_state(); // Here, the session_state is calculated in this particular way, // but it is entirely up to the OP how to do it under the // requirements defined in this specification. var ss = CryptoJS.SHA256(client_id + ' ' + e.origin + ' ' + opuas + ' ' + salt) + "." + salt; var stat = ''; if (session_state === ss) { stat = 'unchanged'; } else { stat = 'changed'; } e.source.postMessage(stat, e.origin); };
この例は、RPからきたイベントに含まれるsession_state
と、
CryptoJS.SHA256(client_id + ' ' + e.origin + ' ' + opuas + ' ' + salt) + "." + salt;
で算出した値が等しいかを計算し、等しいならunchanged
を、等しくないならchanged
を返しています。
session_state の更新タイミング
RPで保持するsession_state
は、OIDCのAuthentication Responseに含まれるsession_state
に同期します。
(下記、Authentication Responseの例)
HTTP/1.1 302 Found Location: https://rp.example.com/cb? code=SplxlOBeZQQYbYS6WxSbIA &state=af0ifjsldkj&session_state=aaaaaaaaaaaa.aaaaa
つまり、session_state
の更新タイミングは、OIDCのAuthentication Responseを受け付ける時になります。
User Agent State の更新タイミング
OPで保持するUser Agent State
は、エンドユーザーのログイン状態が変化したタイミングで更新されます。
つまり、エンドユーザーのログイン、ログアウト実行がUser Agent State
の更新タイミングです。
実装して迷ったこと・気付いたこと
こちらに OpenID Connect Session Management のコンセプト部分を実装しました。 最後に、実装する際に迷ったこと・気付いたことを列挙してこの記事の締めといたします。
User Agent State はどのような値を持たせるのが良いか?
OPで保持するUser Agent State
の扱いに関する情報はあまりありません。
数少ない情報源として、デジタル庁の資料におけるsession_state
はUUID v4であり、User Agent State
に値するものも同じUUIDであると推察されます。この場合、OP iframeで行う比較は、RPから受け取った session_state
と User Agent State
の(状態算出処理が無い)単純な比較になります。
ただ、User Agent State
を再計算なしにそのまま session_state
として扱うことは下記のMust要件に背きます。
The OP iframe has access to User Agent state at the OP (in a cookie or in HTML5 storage) that it uses to calculate and compare with the OP session state that was passed by the RP. The OP iframe MUST recalculate it from the previously obtained Client ID, the source origin URL (from the postMessage), and the current OP User Agent state.
OP iframe は、RP から渡された OP セッション状態を計算し、OP セッション状態と比較するために使用する、OP のユーザーエージェント状態(Cookie 内または HTML5 ストレージ内)にアクセスすることができます。 OP iframeは、取得したClient ID、(postMessageの)送信元のURL、および現在のOP User Agent State から、セッション状態を再計算しなければならない(MUST)。
OP iframeでのsession_state
再計算の仕様上の意図としては、インプットにClient ID
と送信元のURL
を入れることによって、これらの検証も兼ねようとしているのでは無いかと推察しています。
ただ、それらは別の箇所で検証できるので、個人的にはこのsession_state
とUser Agent State
を同じUUIDとしていく案もありだと考えています。
RP iframeを用意する必要はあるのか? (OP iframeのみを埋め込み、RPフロントエンドとOP iframe間でpostMessageでやり取りするのは駄目なのか?)
RP iframeの必要性はstack overflowで議論されていますが、明確な回答はありませんでした。
個人的にはRP iframeは不要で、RPのフロントエンドコードからOP iframeを呼べば良いと考えています。
OP iframeにてUser Agent StateをCookieから取得するための注意点
OP iframeはRPのフロントエンドコードの中で動作するため、仮にUser Agent State
をCookieに設定する場合には注意が必要です。
JavaScriptで値を取る必要があるので、HttpOnly
属性はつけられません。また、RPから見ると別ドメインのクッキーに属するので、SameSite=none; Secure;
にしないとCookieからの値の読み取りができません。