OpenID Connectの仕様を改めて読み返してみる

はじめに

この記事は、「Digital Identity技術勉強会 #iddance Advent Calendar 2023」10日目の記事となります。
https://qiita.com/advent-calendar/2023/iddance

OpenID Connect ムズカシイ…

なんとなく、OpenID ConnectやOAuth2.0の仕様を勉強して、わからないなりに手を動かし、ググりまくり、実際にRP・OP側の実装経験らしきものを積んでくると
お、これで自分も基本的な所は理解できてきたかな? などと軽い気持ちで思い出した頃に、実装ミスや答えに窮する質問を受けて落ち込むもの。
自戒の意味も込めて、一般に言われていることが仕様上はどう記載されているかやTipsも含めて残していきたいと思います。

今回読んでいくのは「OpenID Connect Core 1.0」です。

個人的にそうだったんだ~と思った個所を雑多に集めて関連性は全然なくなっちゃいましたが。。

1. httpスキーマのURLをredirect_uriに指定できるか

なかなか本運用をしているRPがhttpスキーマで動作していることは少ないと思いますが、実情、開発環境ではRP側の制約などで指定したい、と思われるケースも。
Tokenエンドポイントへのリクエストは置いておいて、
Authorization Code Flowを定義した「3.1.2.1. Authentication Request」のredirect_uriパラメータの説明は下記のとおりです。

When using this flow, the Redirection URI SHOULD use the https scheme; however, it MAY use the http scheme, provided that the Client Type is confidential, as defined in Section 2.1 of OAuth 2.0, and provided the OP allows the use of http Redirection URIs in this case. The Redirection URI MAY use an alternate scheme, such as one that is intended to identify a callback into a native application.

「Client Typeがconfidential(=client sercretを安全に管理できる)であり、かつOPがhttpの利用を容認する場合に限り、httpスキーマを利用してもよい」ということなので、条件付きで利用が可能という記載にはなっています。
一方Implicit Flowを定義した「3.2.2.1. Authentication Request」では下記のような記載です。

When using this flow, the Redirection URI MUST NOT use the http scheme unless the Client is a native application, in which case it MAY use the http: scheme with localhost as the hostname.

「このフローを使用する場合、Clientがネイティブアプリケーションでない限り、redirect_uriにhttpスキーマを利用してはならない(ネイティブアプリケーションの場合はホスト名を「localhost」とした上でhttpスキーマを利用してもよい)」
ということなので、フロントを経由してリダイレクト先にトークンが渡るImplicit Flowの場合はhttpスキーマは利用しないように、という記述になっています。

結果的に、OPでこの辺りのRP側の細かな条件が判断できないという背景もあると思いますが、OP製品側でhttpスキーマはクライアント設定時のリダイレクトURLに無条件で登録できない、という仕様になっているケースもあるので、Authentication Requestでも "httpスキーマは基本利用しない" という前提の方が齟齬が起きにくいと思います。開発環境であっても。。

2. フローごとのnonce定義

こちらも改めてですが、nonceはIDトークンのリプレイアタック対策として利用されるパラメータです。
つまり、正規のRPに渡したはずのIDトークンが何らかの方法で窃取された場合に、IDトークン内にセッション内で発行したnonce設定しておき、RP側でトークン内の値とセッション内の値の一致確認をすることで、セッション外で窃取されたトークンであるかどうかを確認します。

nonceもAuthorization Code Flowを定義した「3.1.2.1. Authentication Request」では 「OPTIONAL」 として記載されています。

一方、Implicit Flowの「3.2.2.1. Authentication Request」ではというと下記の通り。

REQUIRED. String value used to associate a Client session with an ID Token, and to mitigate replay attacks. The value is passed through unmodified from the Authentication Request to the ID Token. Sufficient entropy MUST be present in the nonce values used to prevent attackers from guessing values. For implementation notes, see Section 15.5.2.

Implicit Flowの場合は 「REQUIRED」 で利用が必須となっています。
redirect_uriと同様、リダイレクト先にフラグメントでIDトークンが付与される=フロントエンドの通信を経由してトークンが受け渡されるImplicit Flowではバックエンド通信が前提のAuthorization Codeフローより窃取のリスクが高まるため、nonceの検証でカバーする必要があります。
Implicit Flowの「3.2.2.1. Authentication Request」では下記のような形で、基本的にはAuthorization Codeフローの定義と同一であるとしたうえで

Authentication Requests are made as defined in Section 3.1.2.1, except that these Authentication Request parameters are used as follows:

  • response_mode(処理フローを決定)
  • redirect_uri
  • nonce

の3項目は異なる定義として記載がされています。
Authorization CodeフローとImplicitフローはシーケンスの差としては理解した気になっていつつ、
個々のパラメータ差異までは深く理解できていなかったな…と読み返して思いました。

3. 「alg : none」問題

JWTの署名検証時には、ヘッダーに記載されているalgパラメータ通りのアルゴリズムで正しい署名がされているか検証し、トークン発行元の正当性を確認する必要があります。
ただ、本当にこの通りに検証してしまった際に発生するのが 「none」 の問題。

とした際に、「トークンに記載されているalgの値を元に署名検証方法を選択したのち検証を実施する」となると、
書き換えられたnoneが優先されてしまい、そもそも署名検証が飛ばされてしまう(本来はヘッダーを書き換えた時点で署名検証は失敗するはずなのに)
という問題が発生する可能性があります。

対策としてはOPの署名アルゴリズムを確認したうえでRPが検証を実施する、ということになりますが、 Authorization Code Flowを定義した「3.1.2.1. Authentication Request」では下記のような記載。

ID Tokens MUST NOT use none as the alg value unless the Response Type used returns no ID Token from the Authorization Endpoint (such as when using the Authorization Code Flow) and the Client explicitly requested the use of none at Registration time.

仕様上はnoneの利用は禁止されているんだ!と発見しつつ、
トークンがバックエンドで取得されるAuthorization Codedフロー等の利用勝つClientが明示的にnoneの利用を要求した場合は例外とも書かれています(大分レアケースな気がしますが)
署名検証にはライブラリを利用することがほとんどだと思いますが、RPが利用しているライブラリによっては通ってしまう、ということもあるとのことでRP側でも確認が必要なポイントです。

4. subにメールアドレスが利用されるケース

色々な所で言われる「識別子にメールアドレスを使う」問題。
もちろんメールアドレスはUniqueではありますが

  • 変化しうる(別のメールアドレスに変更される可能性がある)
  • 再利用されうる(ユーザを作り直した際に、同じメールアドレスが利用される可能性がある)

という点で、本来非常に扱いづらい属性です。
企業内だと自分のメールアドレスが変わることはあまりないかもしれませんが、コンシューマの文脈だとそれ相応に発生することが考えられますし、
OpenID ConnectでSSOを利用していると余計に 「統合ID側でメールアドレスを変更するときにはOP側のメールアドレスも必須で変更をかけなければならない」 という事態にもなりかねません(到達性確認していなかったりするともっと悲しい)。

このあたりも仕様では「5.7. Claim Stability and Uniqueness」に下記の通り記載されています。

All other Claims carry no such guarantees across different issuers in terms of stability over time or uniqueness across users, and Issuers are permitted to apply local restrictions and policies. For instance, an Issuer MAY re-use an email Claim Value across different End-Users at different points in time, and the claimed email address for a given End-User MAY change over time. Therefore, other Claims such as email, phone_number, and preferred_username and MUST NOT be used as unique identifiers for the End-User.

例としてメールアドレス、電話番号、ニックネームのような時間の経過により変更の可能性がある属性は一意性を保証できず、識別子としては利用してはいけない、とのことですが、
とはいえ・・・このあたりはRP側の仕様に拠る部分が大きく、まだまだメールアドレスが識別子となっているRPは多いため、実情は対応が難しい部分であると思います。
使わざるを得ないなら変更時のケアも考慮する という所も踏まえて、長期的なシステムへの負荷等含めて検討が必要な個所になってくると思います。
この辺りも仕様に書いてたこと自体が気づけていなくて、なるほど…になりました。

5. リフレッシュトークンとpromptパラメータ

最後はリフレッシュトークン関係の話題。
scopeに「offline_access」を指定することでOAuth2.0のリフレッシュトークン発行を要求することができます。
仕様では発行に際してpromptパラメータの利用方法について言及があります。
「11. Offline Access」での記載は下記の通り。

When offline access is requested, a prompt parameter value of consent MUST be used unless other conditions for processing the request permitting offline access to the requested resources are in place. The OP MUST always obtain consent to returning a Refresh Token that enables offline access to the requested resources. A previously saved user \consent is not always sufficient to grant offline access.

offline_accessを要求する際は

  • リクエストするリソースに対する「offline_access」の許可に必要な何らかの他の条件を満たさない限り、promptパラメータに「consent」を指定しなければならない
  • OPはリフレッシュトークンを発行する際は必ず明示的な同意を得なければならない
  • ユーザーの事前同意は offline access の許可には常に十分とはならない

ということで、リフレッシュトークンは一般的に有効期限が長く、一度発行した後は明示的に取消されるまでトークン更新に利用ができるため、発行時に明確にエンドユーザに同意を取ることが必要、と記載がされています。

ここで「prompt」についても言及がされていますが、
promptについては「3.1.2.1. Authentication Request」で次のように記載されています。

OPTIONAL. Space delimited, case sensitive list of ASCII string values that specifies whether the Authorization Server prompts the End-User for reauthentication and consent.

エンドユーザへの「再認証・同意要求」をコントロールするパラメータとのことで、以下の値が定義されています。

概要
none いかなる認証・同意画面も表示してはいけない。エンドユーザが未認証の場合はAuthorization Serverはエラーを返却する
login エンドユーザに再認証を求める(SHOULD)
consent エンドユーザに再同意を求める(SHOULD)
select_account 現在のセッションに複数のエンドユーザアカウントが紐づいている前提で、エンドユーザに利用アカウントの選択を促す(SHOULD)

ちなみにスペース区切りで複数選択が可能(ただしnoneは対象外)です。

リフレッシュトークン発行時等エンドユーザに同意を取りたい、となった際にRP側はpromptパラメータを活用することで同意表示を要求することができるようになっています。
とはいえ、OP側がこの仕様に対応していなければ意味がないので、それぞれ実機側での確認は必要。 個人的には再認証要求時に使うイメージが強かったので、今回読むまでリフレッシュトークンとの関係性に関してはすっかり頭にはなかったのですが、
全体の要件を見ながらシーンごとに設定内容を整理していく必要がありそうです。

ということで

OP・RPの製品固有仕様でこうなっているのかなと勝手に思っていたことが実はOIDCの仕様だったり、改めて読み返してみるとあの課題には仕様側から説明すればよかったのか!と思うことがあったり、
とはいえ色々な事情で仕様通りにはいかないケースもあったりと、奥深さを痛感しつつ、 そう言えばそんな仕様の記載もあったね~と思っていただければ幸いです!