🆔

OpenID Connect のリクエストオブジェクト(Request Object)を調べてみた

2021/08/03に公開

背景

Slack が提供する "Sign in with Slack" がアップデートされて OpenID Connect 互換になりました。

自分は、OpenID Connect に対応したサービスを見るとまず OpenID Configuration を確認するようにしています。結構面白いです。

ちなみに、公開されているものを見つけたら以下の記事にまとめています。
https://zenn.dev/nhosoya/articles/c5a897b9b1974ae4ada6

さて、Slack の OpenID Configuration は https://slack.com/.well-known/openid-configuration に公開されています。

今回特に気になったのが以下の部分です。

{
  "request_parameter_supported": false,
  "request_uri_parameter_supported": true
}

これまで見た各 OP の OpenID Configuration では見かけたことがありませんでしたので、割とめずらしいなーと思い、この記事にまとめてみました。
基本的には https://openid.net/specs/openid-connect-core-1_0.html#JWTRequests に書かれているものを、自分なりに解釈したものです。

簡潔にいうと

request_parameter_supportedrequest_uri_parameter_supported は、Authentication Request におけるリクエストパラメータの受け渡し方において、リクエストオブジェクトを使った方式をサポートしているかどうかを示しています。

リクエストオブジェクト(Request Object)とは

一般的には(?)OpenID Connect の Authorization Code Flow における Authentication Request のパラメータは、署名や暗号化はされずに、ユーザー(User-Agent)から直接 OP に対してクエリパラメータとして送られます。つまり、改ざんすることが可能です。(TLS で保護されているので盗聴もできると書いていいのか微妙)

そこで、OpenID Connect では、Authentication Request のパラメータに署名をつけたり暗号化することも仕様として定められています。具体的にはパラメータの内容を JWT にして OP に渡すことができます(もちろん OP がサポートしていればですが)。この Authentication Request のパラメータを JWT にしたものをリクエストオブジェクト(Request Object)と呼びます。


まずは、普段 Authentication Request で送っているパラメータを JSON 化します。

{
  "response_type": "code id_token",
  "client_id": "s6BhdRkqt3",
  "redirect_uri": "https://client.example.org/cb",
  "scope": "openid",
  "state": "af0ifjsldkj",
  "nonce": "n-0S6_WzA2Mj"
}

これを JWT に変換するのですが、JWT に署名をつけたい場合には、パラメータを受け取った OP が署名検証するために発行者(iss)と受信者(aud)の情報を追加します。

{
  "iss": "s6BhdRkqt3",
  "aud": "https://server.example.com",
  "response_type": "code id_token",
  "client_id": "s6BhdRkqt3",
  "redirect_uri": "https://client.example.org/cb",
  "scope": "openid",
  "state": "af0ifjsldkj",
  "nonce": "n-0S6_WzA2Mj"
}

そして JWT に変換します。

eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJzNkJoZFJrcXQzIiwiYXVkIjoiaHR0cHM6Ly9zZXJ2ZXIuZXhhbXBsZS5jb20iLCJyZXNwb25zZV90eXBlIjoiY29kZSBpZF90b2tlbiIsImNsaWVudF9pZCI6InM2QmhkUmtxdDMiLCJyZWRpcmVjdF91cmkiOiJodHRwczovL2NsaWVudC5leGFtcGxlLm9yZy9jYiIsInNjb3BlIjoib3BlbmlkIiwic3RhdGUiOiJhZjBpZmpzbGRraiIsIm5vbmNlIjoibi0wUzZfV3pBMk1qIn0.xJlut1ecyeQ8qBZrRVb9pCKiIh-QLEZTtYRCiYPLLttTp6vx_g9nJ0Am9kZj4T7UKM-hqFiyADfoWLGyT49rufTQl9AnlFsXlA-znKD1Z3PazBHCgk2bAvWypqjEihoooWsoJoWiZ6d8x70vefVkffAX1sIXiWxXoJnbyVM3hRBp2FOf6zt7yY-h28rYh-Sl65i2FSGlY7pr2w9EYvR2-G_5imAZHzRnb6ow5dU0ke3cILPls_SHWCtrgdHgk6qRoDQWuIbt6wkGznSlW7Rv9dFZcp9rN39bQDdh_3LcJBawPzreW0HMvS_vDRgzk4mAhqEkN1OOegrdTSPhTlGknQ

これがリクエストオブジェクトです。
暗号化したい場合は JWE を使って暗号化することもできます。


リクエストオブジェクトの渡し方

では、このリクエストオブジェクトをどうやって OP に渡すのか。
以下の 2 つの方法があります。

  1. クエリパラメータで直接渡す(Request using the "request" Request Parameter)
  2. リクエストオブジェクトの参照を渡す(Passing a Request Object by Reference)

1. クエリパラメータで直接渡す(Request using the "request" Request Parameter)

OP がこの方式をサポートしているかどうかを示すのが先の request_parameter_supported です。
(Slack はこの方式はサポートしていません。)

とてもシンプルです。リクエストオブジェクトを request というパラメータで Authentication Request に送るだけです。

https://server.example.com/authorize?
  client_id=s6BhdRkqt3
  &response_type=code%20id_token
  &scope=openid
  &request=eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJzNkJoZFJrcXQzIiwiYXVkIjoiaHR0cHM6Ly9zZXJ2ZXIuZXhhbXBsZS5jb20iLCJyZXNwb25zZV90eXBlIjoiY29kZSBpZF90b2tlbiIsImNsaWVudF9pZCI6InM2QmhkUmtxdDMiLCJyZWRpcmVjdF91cmkiOiJodHRwczovL2NsaWVudC5leGFtcGxlLm9yZy9jYiIsInNjb3BlIjoib3BlbmlkIiwic3RhdGUiOiJhZjBpZmpzbGRraiIsIm5vbmNlIjoibi0wUzZfV3pBMk1qIn0.WseHWNXmoD_a7GG9yLBOrYV09jJ5RpjPaEMZ6pPB4MA

このとき client_id response_type はリクエストオブジェクトの claim に含まれているにも関わらず、クエリパラメータとしても渡す必要があります。これは OAuth 2.0 との互換性のために必要です。
また scope もクエリパラメータとして openid を含む値を渡す必要があります。これは OpenID Connect のリクエストであることを示すために必要です。

フローは以下のようになります。
シンプルな認可コードフローに 3 の処理(渡されたリクエストオブジェクトの署名検証をする)追加された形です。

Request using the "request" Request Parameter を使ったフロー

2. リクエストオブジェクトの参照を渡す(Passing a Request Object by Reference)

リクエストオブジェクトを参照できる URL を request_uri というパラメータで送ります。
また、RP はその request_uri から OP がリクエストオブジェクト取得できるようにエンドポイント用意する必要があります。

OP がこの方式をサポートしているかどうかを示すのが先の request_uri_parameter_supported です。Slack はこの方式はサポートしているようです。

https://server.example.com/authorize?
  client_id=s6BhdRkqt3
  &response_type=code%20id_token
  &scope=openid
  &request_uri=https://client.example.org/request.jwt#GkurKxf5T0Y-mnPFCHqWOMiZi4VS138cQO_V7PZHAdM

client_id response_type scope がクエリパラメータとして必要な理由は前述のものと同じです。

フローは以下のようになります。
シンプルな認可コードフローに 3 の処理( request_uri からリクエストオブジェクトを取得する)と 4 の処理(取得したリクエストオブジェクトの署名検証をする)が追加された形です。

Passing a Request Object by Reference を使ったフロー

シンプルな認可コードフローに 3 と 4 が追加された形ですね。

requestrequest_uri の比較

一見すると request を使ったフローがとてもシンプルで、 request_uri を使うフローは RP 側も OP 側も実装が面倒に見えますね。
request_uri を使いたいのはどういうときだろう、と疑問に感じます。

これに対しての request_uri を使うべき理由も spec に書いてあります。 https://openid.net/specs/openid-connect-core-1_0.html#RequestUriRationale

  • パラメータが多くなるとリクエストオブジェクトのサイズが大きくなり、ブラウザの制限を超えてしまう。 request_uri なら問題ない。
  • request_uri なら Authentication Request のレイテンシーを小さくすることができる。
  • リクエストオブジェクトに含まれる claim の内容はほとんど固定である。そのため、リクエストのたびにリクエストオブジェクトおよび request_uri を作成する必要はなく、事前に作成しておくことができる。
  • リクエストパラメータの内容を事前登録しておくことで、OP は事前に検証しキャッシュしておくことができる。そのため、リクエストのたびに fetch する必要はない
  • リクエストパラメータの内容を事前登録しておくことで、OP は事前にリクエストの内容を消費者保護の観点で精査することができる

雑な意訳ですが、こんなことが書かれていると思います。

どうやら基本的にリクエストオブジェクトには固定のもののみを含めるようにして、リクエストごとに変わるようなもの( statenonce)は含めないことを想定しているみたいです。そうすることで RP はリクエストオブジェクトを事前に作成しておくことができるため手間はかからないし、それを OP に事前登録しておくことで、OP としても事前に検証、キャッシュ、精査することができるということで、一見複雑に見えてしまう request_uri を使ったフローにも使うべき理由があると説明しているように思われます。

ここの解釈は自信がないです。

まとめ

spec 読もう。
https://openid.net/specs/openid-connect-core-1_0.html#JWTRequests

最後に

Slack はどうして request_uri_parameter_supportedtrue にしたのだろう。
request_parameter_supportedfalse にしたのだろう。
中の人の話を聞いてみたいですね。

GitHubで編集を提案

Discussion