2024年1月13日土曜日

OpenID Providerを作る)response_typeを実装する

こんにちは、富士榮です。

前回までで一通り認可コードフロー(code flow)の流れを実装しました。



ただ、簡易的にプロトコルの流れを知る目的だったこともあり、かなりの実装を飛ばしてきました。今回はその中の一つのresponse_typeパラメータについてです。

このパラメータはこれまでみてきた認可コードフローに代表されるOpenID Connectのフロー(トークンの払い出しを行うための処理の流れ)を指定するための利用します。
OpenID Connect core 1.0で定義されているフローには、以下の種類が存在します。
  • 認可コードフロー(Authorization code flow)
  • インプリシットフロー(Implicit flow)
  • ハイブリッドフロー(Hybrid flow)

一番よく使われているのは認可コードフローだと思いますが、特定の条件下においてはインプリシットやハイブリッドフローを用いられることがあります。
例えば、インプリシットフローはトークンエンドポイントに対してRelying Partyからバックエンド通信が発生しないので、OpenID Providerが社内などファイアーウォールの内側に配置されていてRelying Partyがクラウド上に配置されているなど、Relying PartyからOpenID Providerへ直接の通信が行えない環境において利用されることがありますし、認可コードとアクセストークンとIDトークンが同一のトランザクションで発行されていることを確認したいような高いセキュリティレベルが求められるようなケースではハイブリッドフローが用いられることがあります。

認可エンドポイントの実装を拡張する

実装をする上では認可エンドポイントでresponse_typeパラメータに指定された値をみて振る舞いを変えていくことになります。
実際にパラメータとして指定される可能性があるのは、以下のパターンです。尚、複数値が指定される場合のデリミタはスペース(%20)です。
  • 認可コードフロー
    • response_type=code
  • インプリシットフロー
    • response_type=token
    • response_type=id_token
    • response_type-token id_token
  • ハイブリッドフロー
    • response_type=code token
    • response_type=code id_token
    • response_type=code token id_token

実装方法は色々とあると思いますが、まずはクエリパラメータからresponse_typeを取得して配列に入れておこうと思います。
// response_typeの判断
const response_types = req.query.response_type.split(" ");
尚、本来は値のサニタイズなどちゃんと実装してください。

あとはどの値が含まれるかによって処理を変えていきます。
  • codeが含まれる場合
    • 認可コードを生成する
  • id_tokenが含まれる場合
    • id_tokenを生成する
  • tokenが含まれる場合
    • access_tokenを生成する
またid_token、tokenが含まれる場合はインプリシットもしくはハイブリッドフローなので、redirect_uriへのリダイレクト時のパラメータはクエリ文字列(?)ではなくフラグメント(#)で返却する必要があります。

そこで、返却パターンを判別するためのフラグを用意しました。
// implicitもしくはHybridを判定するフラグ(フラグメントでレスポンスを返すかどうかの判定)
let isImplcitOrHybrid = false;

また、返却するパラメータを入れておくバッファとして利用する空の配列も定義しておきます。
// レスポンスを保存する配列
let responseArr = [];

これまで解説したとおり、認可コード、IDトークン、アクセストークンの中身はバックエンドサーバもないので共通のものを使っています。
// ペイロード(暫定なので固定値。有効期限関係だけ個別に含める)
let payload = {
iss: baseUrl,
aud: req.query.client_id,
sub: "test",
email: "test@example.jp",
name: "taro test",
given_name: "taro",
family_name: "test",
nonce: req.query.nonce
};

これで下準備は済んだので実際にresponse_typeの値を判別して処理をしていきましょう。
まずはcodeです。
この場合は認可コードを生成するので、前回までに実装したコードをベースにJWEを生成します。生成したら返却するパラメータに追加しておきます。なお、有効期限(exp)は短めです。
if(response_types.includes("code")){
// code flow
// 認可コードの作成
payload.exp = Math.floor((date.getTime() + (1000 * 30)) / 1000); // 有効期限は30秒
const code = await utils.generateJWE(payload);
responseArr.push("code=" + code);
}

次はid_tokenです。
こちらはiat(発行した時刻)に現在時刻を設定し、有効期限(exp)は10分にしておきます。また、インプリシットかハイブリッドであることが確定するのでisImplicitOrHybridのフラグをONにしておきます。最後に認可コードと同じように返却するパラメータに値を追加しておきます。
if(response_types.includes("id_token")){
// implicit/hybrid
// id_tokenの生成
payload.exp = Math.floor((date.getTime() + (1000 * 60 * 10)) / 1000);
payload.iat = Math.floor(date.getTime() / 1000);
const id_token = await utils.generateJWS(payload);
responseArr.push("id_token=" + id_token);
isImplcitOrHybrid = true;
}

最後にtokenです。
こちらも基本はid_tokenと同じことをやりますが、有効期限(exp)は60分にしておきます。
if(response_types.includes("token")){
// implicit/hybrid
// access_tokenの生成
payload.exp = Math.floor((date.getTime() + (1000 * 60 * 60)) / 1000);
payload.iat = Math.floor(date.getTime() / 1000);
const access_token = await utils.generateJWS(payload);
responseArr.push("access_token=" + access_token);
isImplcitOrHybrid = true;
}

response_typeによる振り分けが終わったら、Relying Partyから指定されたstateの値を返却するパラメータに追加しておきます。Relying Party側でのCSRF対策用ですね。
// stateをレスポンスに含める
responseArr.push("state=" + req.query.state);

そして返却するパラメータを&で繋ぎ合わせます。
const responseParam = responseArr.join("&");

最後にインプリシットもしくはハイブリッドフローならフラグメントに、認可コードフローならクエリ文字列に返却パラメータを入れてredirect_uriへリダイレクトします。
if(isImplcitOrHybrid){
// implicit/Hybrid flowなのでフラグメントでレスポンスを返却する
res.redirect(req.query.redirect_uri + "#" + responseParam);
} else {
// code flowなのでクエリでレスポンスを返却する
res.redirect(req.query.redirect_uri + "?" + responseParam);
}

これで認可エンドポイントの拡張はおしまいです。

ディスカバリエンドポイントの拡張

上記実装によりこのOpenID Providerがサポートするフローが増えていますので、Discoveryエンドポイントで提供するメタデータにも情報を追記しましょう。
response_types_supportedが返却する値に対応するフローの値を追加しておきます。
const response_types = ["code", "code token", "code id_token", "code token id_token", "token", "id_token"];

これで全て完了です。

動作確認

ちょうとMicrosoftが提供しているJWTデコードツール(https://jwt.ms)はインプリシット・ハイブリッドフローで提供されるIDトークンのデコードに対応しています。

http://localhost:3000/oauth2/authorize?
   response_type=id_token&
   client_id=111&
   redirect_uri=https://jwt.ms&
   state=hoge

という形でjwt.msをredirect_uriにセットし、response_type=id_tokenでリクエストしてみます。
https://jwt.ms/#id_token=eyJ0eXAiOiJqd3QiLCJhbGciOiJSUzI1NiIsImtpZCI6ImpURkZodFR4ZVVsUHFEVlNEQ0dTNVBCOF9HNkpTSjdIS0IxSGFyQU1GMUkifQ.eyJpc3MiOiJodHRwczovL2xvY2FsaG9zdDozMDAwIiwiYXVkIjoiMTExIiwic3ViIjoidGVzdCIsImVtYWlsIjoidGVzdEBleGFtcGxlLmpwIiwibmFtZSI6InRhcm8gdGVzdCIsImdpdmVuX25hbWUiOiJ0YXJvIiwiZmFtaWx5X25hbWUiOiJ0ZXN0IiwiZXhwIjoxNzA1MTM0NjAyLCJpYXQiOjE3MDUxMzQwMDJ9.bSgUeKQBf8cYv2wveFvq5IFTWDpYUu7_s2iVci3dZKyp8tkful3u1Jj9oN4xzS-mn8bz-Ww9a4jIS6KyTKBgeT6HHMlhF-YiMtwMn8_FKqpOY_O94GPFkgUMgwB3Qu1QGTxx5O4fWCzQoZpUFgFF5JW339CAAH4TRtDRcjSJnBR5z7aMU8Ig-UQXIuhLtAAuHDZ01fc3xYyXiOWi4C9Rio6yFDxrO-kVxgAlFWfP7k8qvUEWrX3IlZYIVQQ0GgNtKQSAxGVQZxb9TgShZi2sQvl2EeE-I9DKVSw9W_OI6UOTIdQlc4KNQ5qqLHsuoE74iE1SVf6Oqr_Z67NZYm3GMw&state=hoge
という形でフラグメントにIDトークンが返却され、jwt.msのサイトでデコードされた情報が表示されます。

ということで今回はresponse_typeについて実装してみました。
今回の拡張部分についてもこちらのレポジトリに反映してありますので、全体を確認したい方はご覧ください。

引き続き実装を拡張していきます。

0 件のコメント: