STORES Product Blog

こだわりを持ったお商売を支える「STORES」のテクノロジー部門のメンバーによるブログです。

OpenID Connect Session Management について

概要

nannany です。

この記事ではOpenID Connect Session Managementについての説明と、実際にサンプルを実装して得た気づきを記述しました。

Authorization Code Grantがどのようなフローで行われるかなど、OIDCのベースとなる説明は割愛しています。

OpenID Connect Session Management とは?

OpenID Connect Session Management は OpenID Foundation が定めた仕様の1つです。

openid.net

仕様書のうち、その内容を端的に表している箇所が下記になります。

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_stateUser 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_stateUser 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からの値の読み取りができません。

参考