🙏

OIDCでRPが必須な属性情報を要求する方法とOP側の実装案

2021/12/26に公開

ritouです。

なんだか今年はいまいちあれだった気もしないでもないですが、Digital Identity技術勉強会 #iddance Advent Calendar 2021の最終日の記事です。

https://qiita.com/advent-calendar/2021/iddance

まだ記事が書かれてない日もあるみたいなのでこのまま放置されそうだったら年内に何か書いて埋めときます。
でも最近寒すぎて無理かもしれない。雪がひどい。

何の話か

今回は、OIDCのOP/RP間でのこんなやり取りを実現する方法について書きます。

  • OP : この時代にパスワード認証なんて使わん!SMS/Email経由のOTPやソーシャルログインに対応してるんや!どんどん使ってくれや!
  • RP : 今までSMS経由のOTPでやってましたが大人の事情でOPさんとのID連携することになりました。 そちらにはEmailのみのユーザーがいるようですがSMS番号(細かい話はめんどくさいので一旦電話番号でいい)を必ずIDTokenの中に入れて返していただくことは可能でしょうか?もしよろしければ一度Zoomなどで開発メンバー含めたお打ち合わせを...

みたいな時の話です。

仕様にはどう書いてあるか

OpenID Connect Core 1.0 incorporating errata set 1 の仕様を見てみます。

https://openid.net/specs/openid-connect-core-1_0.html

scopeで要求した属性情報の扱い

そもそもOIDCでscopeを指定した場合のそれに紐づくclaim(s)って、どんな扱いだっけ?ってところを確認します。

5.4. Requesting Claims using Scope Values
For OpenID Connect, scopes can be used to request that specific sets of information be made available as Claim Values.
Claims requested by the following scopes are treated by Authorization Servers as Voluntary Claims.

この "Voluntary Claim" ってのは何かというと

1.2. Terminology
Voluntary Claim
Claim specified by the Client as being useful but not Essential for the specific task requested by the End-User.

で、 "Essential" ってのは

Essential Claim
Claim specified by the Client as being necessary to ensure a smooth authorization experience for the specific task requested by the End-User.

ということで、scopeで指定した場合は必須ではないclaim(s)として扱われます。

必須なclaim(s)を指定するには

scopeとは別で "claims" というパラメータにて指定します。

5.5.1. Individual Claims Requests
essential
OPTIONAL. Indicates whether the Claim being requested is an Essential Claim. If the value is true, this indicates that the Claim is an Essential Claim

"auth_time": {"essential": true}

By requesting Claims as Essential Claims, the RP indicates to the End-User that releasing these Claims will ensure a smooth authorization for the specific task requested by the End-User.

なるほど、これでRPはEndUserに対してこれらが必要なんだっていう要求ができると。
気をつけないといけないのが次の一文ですね。

Note that even if the Claims are not available because the End-User did not authorize their release or they are not present, the Authorization Server MUST NOT generate an error when Claims are not returned, whether they are Essential or Voluntary, unless otherwise specified in the description of the specific claim.

(別途決まりがある場合を除いて)Essential/Voluntary に関わらず、エラーは返すなと。
ということは、どうしたって要求したclaimが含まれない場合があるってことになります。

RP側の実装

RPとしては

  1. OIDCの認証リクエストのclaimsパラメータで電話番号を必須でくれと要求する
  2. IDTokenに電話番号が含まれているかを検証、含まれていなかったらエラーにする

という実装が必要です。

claims パラメータでの要求

claimsパラメータでは、IDToken/Userinfo Endpointのレスポンスで欲しいclaimを指定する形式になっています。
細かく指定できるぶんだけ、OP側でこのclaimsパラメータのハンドリングは結構大変な印象がありますね。

なるべくシンプルな実装にしたいというOP視点のバイアスをかけつつ整理していくと、例えばRPのログインや新規登録で利用する場合はIDTokenの方の指定は必要になるでしょう。
scopeで要求したclaimsを普通にIDToken/Userinfo両方に突っ込んでくれるOPであれば、Userinfoの方を明示的に指定しなくてもIDTokenで返してくれるclaimsがUserinfoから"も"返ってくる、という挙動が考えられます。そのような場合はIDTokenでの必須claim指定がUserinfoにも反映されそうなのでシンプルになりそうです。(とかなんとか書いてますが、こういう細かい部分はOPが考えて決めないといけない部分があるということだけ覚えておきましょう。)

ということで、IDTokenの中に電話番号を必須で欲しいと要求する場合は

{
  "id_token":
    {
     "phone_number": {"essential": true}
    }
}

というのをエンコードした

{\"id_token\":{\"phone_number\":{\"essential\":true}}}

という文字列を claims パラメータとして指定する必要があります。

IDTokenの検証

検証自体は一般的なので問題ないでしょう。

  • phone_number
  • phone_number_verified

あたりのパラメータの有無を確認します。

ここで含まれない場合はどんなケースでしょうか。

  • OPが渡す気なかった
  • OPは渡す気があったけど値がなかった
  • ユーザーが個別に拒否した
  • ユーザーが認証リクエストをいじってclaimsパラメータを取り払った

とか色々考えられます。

また、RP-OPの関係によって

  • OPは必ずもらえる用に、別途取り決めがある -> 含まれないのはおかしいのでエラーにしても良さそう
  • OPからもらえない可能性がある -> RP側でリカバリーできるならする、できないならエラーでも仕方ないか

といった対応が考えられますね。

OP側の実装

OPがこのような機能を提供しようとした場合どうするか、認証リクエストの解釈は上述のとおりですが、画面遷移のあたりはOIDCの仕様の対象外です。

一般的には

  • 認証リクエストを検証する
  • EndUserがログイン状態でなかったらログインさせる
  • 認証リクエストの内容に同意を求める
  • 認証レスポンスを返す(返せないclaimは省略する)

といった流れでしょうが、単に空で値を省略することなく、

  1. ユーザーに許可を求める
  2. 許可した場合は入力させた上で値を返す

というフローを実現したいならば

  • 認証リクエストを検証する
  • EndUserがログイン状態でなかったらログインさせる
  • 認証リクエストの内容に同意を求める
  • 不足している属性情報を入力させる
  • 認証レスポンスを返す

となるのが自然な気がします。
シーケンスにするとこんな感じでしょうか。

必須な属性が複数ある場合とかOP内でどう実装するとか細かい話はまだまだありそうですが、大枠はこんなもんでしょう。
うん、ではこれで実装してみます(何を!?)

ではまた。

Discussion