OpenID Connect Session Management 1.0 でフロントチャネルからOPのログイン状況をRPに伝える

2019.11.29

※ 本記事ではわかりやすさを優先するため仕様で規定されていない部分についても具体的な実装方法について記載しています。正確な仕様については OpenID Connect Session Management 1.0 - draft 28を参照ください。

本記事は認証認可技術 Advent Calendar 2019の5日目の記事です。

私は現在 Barista という認証サーバーの実装に携わっています。

少し前に OpenID Connect の関連仕様である OpenID Connect Session Management 1.0 - draft 28 の一部を Barista に実装したので、本記事では仕様と実装についてまとめさせていただきます。

OpenID Connect Session Management とは

本仕様 では大きく2つの機能が定義されています。

Session Status Change Notification

Session Status Change Notification では、RP から OP のエンドポイントを iframe でロードし、iframe に対して JavaScript でメッセージを送ることで、OP のログイン状況を RP に同期することができます。

OP のログイン状況を取得するためには、他に prompt=none で認可リクエストを送信し認可レスポンスを確認する方法が考えられますが、Session Status Change Notification を利用する場合、

  • OP へのアクセスを抑えられる(場合によってはアクセス不要)こと
  • ブラウザ上で処理が(概ね)完結すること

がポイントかと思います。

どうやるか?

ものすごく大雑把にいうと、

  1. 認可レスポンスとして「ログイン状況が変わると変化する値(session state)」を OP から RP に発行
  2. OP は session state の生成元になる値(opbs)を、ログイン状態が変わるタイミング(ログイン時やログアウト時)でブラウザの Cookie に登録
  3. RP は iframe で OP の特定のページをロードし、session state を iframe に送信
  4. OP の iframe は、Cookie に登録済みの opbs から生成した session state と RP が送信してきた session state を比較
  5. 値が同じならログインしたままで、変わっていればログアウト

といった感じです。ただし、上記の実装では一旦ログアウトしてからログインした場合も opbs の値が変わるため、正確には5は「値が同じならログインしたままで、変わっていればログアウトした可能性がある」となります。

以下の図にあわせて、もう少し具体的な各コンポーネントの実装について説明します。

OP の実装

認可コードフローでOPが返す値の追加(session_state)

OP は認可レスポンスで、session_state を返すようにします。(1) session_state の具体的な値は仕様では規定されていませんが、4.2. OP iframe では以下のようなサンプルコードが紹介されています。

var ss = CryptoJS.SHA256(client_id + ' ' + e.origin + ' ' +
      opbs + ' ' + salt) + "." + s

client_idorigin は文字通り、salt はいわゆる salt なので乱数で良いと思います。 opbs がポイントで、要するに「ログイン状態が変わったら変わる値」を生成する必要があります。いわゆる「ログインしたりログアウトしたりするとセッションIDが変わるシステム」であれば、セッションIDそのままでも挙動としては問題ないですが、RP や JS から参照可能な場所にセッションIDを露出するのもアレなので、セッションIDのハッシュ値か何かを opbs とするのが無難かと思います。

opbs (op browser state)をブラウザに保存

発行した session state は、最終的にブラウザ上で「OP から参照可能な領域に保存されたログイン状態を表す値(opbs)」との比較に利用します。この値をユーザーが OP にアクセスした際にブラウザに保存してあげる必要があります。具体的な保存箇所は未定義ですが、仕様では

in a cookie or in HTML5 storage

とあります。保存のタイミングについては、基本的にログイン状態が変わった際のレスポンスのみで問題ないはずですが、「ログアウトは完了したけどブラウザがレスポンスを処理できなかった」ようなケースを考えると、ログイン状態の同期の精度を上げるにはすべてのリクエストに対して opbs をレスポンスしてしまうのが良いかと思います。(A)

OP 側の iframe の提供

RP のページから読み込み、ログイン状態を RP に通知するための iframe 用のエンドポイントを提供します。(3) 実装は仕様で示されているサンプルの通りで問題無いと思います。opbs は前述の処理で保存した場所から取得します。

この JavaScript では、RP が postMessage で送信してきた session state に対して、ブラウザに保存している opbs から session state を再計算し、RP が送信してきた値と比較することでログイン状況を判定し、RP に返却しています。

また RP の実装にもよりますがこのエンドポイントは大量にアクセスがあることが想定されます。例えば、RP がすべての画面で常にログイン状態を取得したい場合、RP のあらゆる画面へのアクセス時にこの iframe 用エンドポイントへリクエストがくるため、キャッシュやCDN経由での配信の検討が実装のポイントになるかと思います。

RP の実装

RP 側の iframe の提供

認可レスポンスとして OP が session_state を送信するので、この値と Client ID を RP の iframe に渡します。 これらの値を OP の iframe に送信するスクリプトを実装した iframe 用のエンドポイントを RP から提供します。(1)

以下は実装のサンプルです。postMessage で OP に送信している各値は、実際の値を送信する必要があります。ここでは、1秒ごとにログイン状態を取得しています。

<script>

    function check_session()
    {
        var win = window.parent.document.getElementById("op").contentWindow;
        // clientid、session state、opのoriginはそれぞれ値を置換
        win.postMessage("clientid" + " " + "session state", "https://oporigin.example.com");
    }

    function setTimer()
    {
        check_session();
        timerID = setInterval("check_session()", 1000);
    }

    setTimer()

    window.addEventListener("message", receiveMessage, false);

    function receiveMessage(e)
    {
        if (e.origin !== getOpOrigin() ) {return;}
        var state = e.data // 取得したログイン状態を使ってよしなに
    }

</script>

RP でログイン状態を取得したいページで、各 iframe を読み込む

RPはページ内に、iframe(OP)とiframe(RP)を埋め込みます。(2)(3)(4) 結果、iframe(RP)は、iframe(OP)に対して定期的にpostMessageで (1) で取得したsession_state を送ります。このときサーバへのリクエストは行いません。(5)

これに対して、iframe(OP)はiframe(RP)にchanged or unchanged のレスポンスを返します。(6)

このとき、以下の結果に対して RP は後続の処理を実装します。

  • unchanged の場合は、ログアウトしていないと判断してよい。
  • changed の場合は、ログアウトしている可能性がある。
    • 確実な判定をするためには、prompt=noneで認可リクエストを送り、エラーが返ってきたらログアウトと判断できる。

RP-Initiated Logout

本仕様で定義されているもう一つの機能が、RP-Initiated Logout です。 これはざっくりいってしまえば、RP が OP に対してユーザーのログアウトを依頼する機能です。

フロー的には、

  1. RP は OP から RP-Initiated Logout 用に提供された URI に、ユーザーをアクセスさせる。このとき、ログアウト後の遷移先となる RP のURI (post_logout_redirect_uri)をクエリパラメーターに指定してもよい。
  2. OP はユーザーに「ログアウトしてもよいか」尋ねて、OK ならログアウトする。
  3. post_logout_redirect_uri にリダイレクトする。

といった感じです。要するにログアウト後 RP にリダイレクトして戻すための仕様だと思います。(それ以外は OP にいわゆるログアウト機能が実装されていればだいたい達成できるので)

具体的に1では

  • id_token_hint
    • 事前に RP に発行されたIDトークン
  • post_logout_redirect_uri
    • ログアウト完了後のリダイレクト先
  • state
    • ログアウトを依頼した RP とリダイレクトで戻ってきた時に RP が検証する値
    • 認可リクエストの state と同じようなものだと思う

の値をそれぞれ OP に渡します。

post_logout_redirect_uri の値は RP が事前に OP に登録しておく必要があります id_token_hint の具体的な用途は仕様では定義されていませんが、こちらの実装を参考にさせていただいたところ、

  • IDトークンの検証
  • ログアウトを要求しているクライアントの判定
  • そのクライアントに対して post_logout_redirect_uri が有効かどうかの判定

を行っていました。確かにどの RP がログアウトの要求をしているのか?はここからしか判断できない気がします。

参考資料

まとめ

OpenID Connect Session Managementは、

の2つの機能を定義しています。

前者の Session Status Change Notification では、フロントチャネルから、OP へ大きな負担をかけずに RP から OP へのログイン状況を同期可能です。 後者の RP-Initiated Logout では、ログアウト後に RP の ページへリダイレクトさせることができます。

明日の認証認可技術 Advent Calendar 2019は@82pさんの記事です。

クラスメソッド の事業開発部ではソフトウェアエンジニアを募集しています

現在私は事業開発部で prismatix というサービスの開発に携わっています。(prismatix では、前述の Barista を利用しています。) 具体的には認証認可のためのマイクロサービスの開発を行なっています。 OAuth 2.0 および OIDC の仕様を読みながらコードを書く仕事です。

そして、事業開発部ではソフトウェアエンジニアを募集しています。

もし興味のある方がいましたら、こちらのページを見ていただけますと幸いです。

私からは以上です。