"日刊 OpenID Providerを作る" を見守る会
始まりました。
OpenID Connectってシンプルなプロトコルだと思いますが、やっぱりOpenID Providerの気持ちにならないと本当のところはわからないよね、ということで「OpenID Providerを作る」シリーズ(デ⚫︎ゴ⚫︎ティーニ風)でもやってみようかと思います。
なお、そもそも論となりますがOpenID Connectの究極の目標は「Relying PartyがIDトークンを取得すること」です。その過程においてユーザ認証を行なったり、APIアクセスするためのアクセストークンを取得することもありますが、まずは周辺の仕様を削ぎ落としてど真ん中だけを考えることが理解を進めるためには必要だと思います。
これは大事なことですね。
ドヤ顔で「OIDCはOAuth 2.0を拡張したものでぇ〜」とか書いている記事がありますが、
- 認証イベントの情報、対象ユーザーの属性情報をIDTokenとして提供するのがいちばんの目的
- IDTokenに含まれた対象ユーザーの属性情報はあくまでその時点のもの。ユーザーアクションを伴わないオフラインアクセスとして最新の情報を取得するために、OAuth 2.0のリソースアクセスの仕組みを利用するUserInfo Endpointの仕様を定義した
というところまで抑えておきたいですね。
ディスカバリとは何をするフェーズなのか?ですが、簡単にいうとOpenID Provider自身に関する情報をRelying Partyに伝えることが目標となります。
- ユーザの識別子からOpenID ProviderのURLを取得する
- OpenID Providerの構成情報を取得する
この辺に関しては自分のポストで少し補足しました。Discoveryって実は奥が深いんです。
認可エンドポイントの話です。
認可コードフローに限定して話すと認可エンドポイントの目標は「認可コード」を発行することです。クライアント(Relying Party)はこの「認可コード」を後日紹介する「トークンエンドポイント」でIDトークンやアクセストークンと交換しますので、認可エンドポイントが正しく認可コードを発行することはOpenID Connectのフロー全体の安全性を担保する上で非常に重要な第一歩となります。
ちょっと内容から逸脱しますが、普通にOIDCを学んだ場合、認可コードフローはこう!って覚えていくわけです。一方、各エンドポイントがやることベースで整理すると
- OAuth/OIDCでトークンを発行するエンドポイントは Token Endpoint
- Token Endpointに対して "どのユーザーに?" みたいなところを grant と呼ばれるもので指定してやる
- 認可コード
- リフレッシュトークン
- 別サービスから受け取ったトークンなど
- Authorization Endpointについては、現在OP上でログインしていてRPからの要求に同意したユーザー のトークンをToken Endpointから受け取るために認可コードを発行するためのエンドポイント
となります。
Implicit Grantはこの省略形としてAuthorization/Authentication Responseとして各種トークンを返すものだと考えても良いですが、OAuth 2.0ではImplicitは非推奨であるしOIDCの場合はフロントエンドのみで完結するケースやAuthorization ResponseをIDTokenを使って保護するみたいなケースなので特殊なものなんだと認識してもらえたら良いかと思います。
前回、認可エンドポイントの目標はトークンエンドポイントでトークンと交換するための認可コードを取得することである、という説明をしました。今回はトークンエンドポイントなので認可エンドポイントで発行された認可コードを受け取りトークン(IDトークン、アクセストークン、リフレッシュトークン)をクライアントへ渡すことが目標となります。
3.1.3.1. Token Requestによるとリクエストをする際は、クライアントの認証をあらかじめ定めた方法で行った上でgrant_typeにauthorization_codeを指定し、取得した認可コードをPOSTする必要がある様です。
前回のコメントの通り、トークンエンドポイントは
- クライアント認証
- grant_typeに対応するパラメータから各種トークンを生成する
と言う処理となります。
grant_type=authorization_code なので code パラメータで指定された認可コードから各種トークンを発行します。
OAuth 2.0ではRFC6749で
- client_credentials
- password
- refresh_token
と言うgrant_typeが定義されていますし、他の拡張仕様では次のようなネームスペースが使われていたりします。
- urn:ietf:params:oauth:grant-type:token-exchange (RFC8693)
言い換えると、なんらかのデータからトークンを発行する仕組みはこのトークンエンドポイントに集約してくることになると言うことです。想像するだけでやばい。
もし気が⚪︎って自前でトークンエンドポイントを作ろうと思ったら、この辺りを頭に入れておくと良いでしょう。
前回までの処理でOpenID Connectの最大の目標であるIDトークンをクライアント(Relying Party)に発行する、という処理は終わっていますので、今回のUserInfoエンドポイントについては必ずしも実装する必要はありません。
UserInfoエンドポイントはユーザの属性情報を提供するための標準化されたAPIで、OpenID Provider(のベースとなるOAuth2.0の認可サーバ)の保護対象リソースとなります。そのためOpenID ProviderからIDトークンに加えてアクセストークンの発行を受ける必要があり、クライアントはアクセストークンをAuthorizationヘッダに付加した状態でUserInfoエンドポイントへアクセスすることでユーザの属性情報を取得します。
なお、OpenID Connectではユーザ情報をIDトークンの中に含めることも可能なので、UserInfoエンドポイントとの使い分けをどうするのか?はID基盤全体としての設計上のポイントとなります。
IDトークンはユーザ認証のイベントに連動して発行される一方でUserInfoは認証イベントとは非同期で(様するに後からでも)ユーザ情報を取得することができるのでクライアントの利用シーンによって使い分けることが重要です。
例えばモバイルアプリケーションなどは毎回ログインするわけではないのでユーザの属性情報を取得するのにIDトークンの利用はできませんが、リフレッシュトークンを使ってバックエンドでアクセストークンを更新しつつUserInfoエンドポイントへアクセスすることでログインとは連動せずに最新のユーザ情報を取得することが可能となります。
ここでは "IDTokenとUserInfoそれぞれ属性情報を取得するタイミング、情報の精度違うので使い所を考えましょう" という書き方ですが、もう一つ別件で重要なのが「RPの認証処理にどの属性情報を使うんや?」というところです。
RPの認証処理にはIDTokenのsubの値を使いましょう。
UserInfoの結果を使って認証処理に使うのはOIDCではなくちょっと標準化されたOAuth認証です。
私からは以上です。
私たちがふじえさんの記事を見守っているように、ふじえさんもここを見守っています。
今回はその中の一つのresponse_typeパラメータについてです。
このパラメータはこれまでみてきた認可コードフローに代表されるOpenID Connectのフロー(トークンの払い出しを行うための処理の流れ)を指定するための利用します。
ここはニュアンスを説明するのが難しいですね。「OpenID Connectのフローに対してresponse_typeの値の組み合わせが決められています。」ぐらいの方が適切かもしれません。
response_typeは認可エンドポイントへの認証リクエスト/レスポンスというやりとりにおいてOIDCでもらえる各種トークンのどれが欲しいのかを指定するパラメータです。認可コードもトークンとして認識しておきましょう。
そして、そのトークンの種類によって認証レスポンスへの指定方法が変わることを説明いただいています。
OIDCの理解としてはここまで覚えましょう。
こんなの知ってるという方は、response_typeの複数指定についてはもう一つドキュメントがあるので時間がある方は読んでみましょう。
こちらの仕様には "response_mode" という用語があります。
ふじえさんの前の記事でも一瞬見かけたこのパラメータですが、これはresponse_typeで指定されたトークンをどのように認証レスポンスに指定するかを指定する(ややこしい)パラメータです。
- code: query
- token, id_token: fragment
というのはresponse_modeのデフォルト値と言えます。利用している環境毎にデータ漏えいなどのリスクを見極め、適切なresponse_modeを指定することで複雑なハンドリングを避けられること"も"あります。この辺りは別途説明していただけそうな気がするのでこの辺りに押さえておきましょう。
この実装を行うためにデジタル署名されたIDトークンの中に認可コードやアクセストークンのハッシュの値がc_hash(認可コードのハッシュ)やat_hash(アクセストークンのハッシュとして埋め込みます。
あれっ、もしかして…
先走ってしまいましたすみませんすみません。
OAuthにおける認可フローというのはアクセストークンをどう発行するかを定義したものに対してOIDCはIDTokenをどう発行するかを定義していると言えます。
認証レスポンスにて複数のトークンを発行するHybridフローにもその違いが関係してきます。
単純にパラメータをクエリで渡すよりもJWT形式で渡すことで改竄されていないことを検証できます。これはわかりやすいですよね。
さらに、一緒に送られるパラメータのハッシュ値をPayloadに含むJWTを一緒に送っても同じような検証ができます。response_typeにIDTokenを含んだときの仕様はその仕組みを認証レスポンスに適用したものです。
JWSで改竄検知ができたり、JWEで暗号化したりすることで認証リクエストやレスポンスを保護できます。他には先にバックエンドでパラメータを送っておいて、その参照先のURLをパラメータで送ったりするやり方もありますね。
OIDCの仕様自体にこのような保護のためのパラメータがたくさん定義されていたり、拡張仕様になっていたりします。これまでのOAuth/OIDCのベーシックな機能に加えて、様々な方法でセキュアにする仕組み、そしてそのような保護を必須とするFAPIのようなプロファイルというように理解できると、仕様を読むこともだんだん楽しくなってきますよ。疲れますけど。
気になった方はこの辺の投稿も参考にしてください。
今後も触れるかもしれませんが今回はPairwise識別子と実装についてみていこうと思います。
PPIDについての説明です。
なお、識別子をpairwiseにしてもメールアドレスなど他の属性で識別可能になってしまう実装は世の中にたくさんあります。この辺りはユーザに対して提供する利便性とのバランスを含め考えていく必要があると思います
これにも気をつけなければなりませんね。
他には3rd Party Cookieやブラウザのフィンガープリントのようなものを用いた名寄せなんかもあります。
記事では仕様に記載されている払い出しの方法が説明されています。ハッシュなんかはデータストアなしで実装できそうなので魅力的に見えるものの、実際にPPIDを実装しようと思うと ProviderもしくはResource Serverが "PPID"からローカルアカウントIDを参照する機能 が必要となることに留意しましょう。
だったらEncrypt?となったり、結局データストア必要なのか...となるわけですが、エンドユーザーのリソースが増えたりRPの数が増えるとデータが増えたり思ったより参照される回数が多いものになったりします。
識別子の話はそれだけでご飯が3杯くらい食べられるくらいのおかずなので、
まさにこれですね。
最後にめんどくさい話をして終わりましょう。
- あるID基盤がPPIDを実装済みだとします。そしてそれをいくつかのRPが利用しています。
- 認証機能やユーザー単位の属性情報以外にも基盤の価値を上げるために、RPのソーシャルグラフをリソースとして活用したい。まぁ会社名のついてるアカウントと会社内の独立したサービスみたいなのを想像してくれたら良いです。
- RPにはそれぞれローカルのユーザー識別子で表現されたグラフがあり、それを別のRPで利用してほげほげみたいなこともやりたい。
こういう時にPPIDをどう絡めていくか?みたいなのを考えるとちょっと嫌ですよね。
ユーザー識別子の設計は奥が深いのです。
OpenID Providerを作っていくとメインの機能ではないけど必要な属性情報を扱う必要性に気がついてきます。
例えば、最終ログイン日付とか利用しているOSやブラウザの種別などの環境要素なども代表的なものの一つだと思います。
これらの情報をどうやってユーザデータベースに保存するか、そしてこれらの情報をどのタイミングで読み書きするのか、がID基盤の性能やセキュリティ、スケーラビリティなどのいわゆる非機能系の設計を行う上では重要な要素になりそうです。今回はそれらの属性の取り扱いを含むログイン処理の実装について考えてみたいと思います。
環境要素とかは、OIDCと無関係とも言えない話がありますね。OpenID Summit Tokyo 2024でTom Satoさんからお話しいただいたSSFというものがあります。
OIDFシェアードシグナルフレームワーク(ID2)を利用してリアルタイムでセキュリティシグナルを共有するための最新情報
Tom Sato — VeriClouds BoD (Seattle, US)
その中でMSが積極的に採用しているCAEP(Continuous Access Evaluation Profile)ではアクセス元環境が変更された際にイベントが送られるみたいなのがあります。
一般に認証(ログイン)処理は出来うる限りシンプルにしていくことが望ましいと考えられます。これは可用性や性能の観点から複雑な処理を入れるとログイン処理が遅くなったり同時に多数のユーザがアクセスする場合にサーバーリソースを多く消費してしまうことが考えられるためです。
この辺は難しいですね。色々やろうと思うと重くなってしまい、利用者が増えたときに詰まるみたいなことがあってもいけません。認証ならまだしも認可の文脈だとさらに大変だったりします。
ログ相当のデータだし書き込みは非同期でええやろと簡単にやってしまって後から別の用途で使うことになり...みたいな経験もあります。
また、加えてログイン時の条件や認証結果によってログイン処理にかかる時間があまりに異なると内部処理の推測をされてしまうなど攻撃者によって大きなヒントを与えてしまうことにもなりえます。(例えば、削除済みユーザ、ロック済みユーザとパスワードを単に間違えたユーザで認証試行に対するレスポンス時間が極端に異なると、ブルートフォースアタックをされた時にユーザが存在する可能性がわかってしまう、などのリスクに繋がります)
これは!大好物のやつ!
OPを作るには色々と考慮が必要ということですね。
OIDCでRPがOPにエンドユーザーの属性情報を要求する際、指定されたscopeに対して属性情報のマッピング、簡易的な実装例を説明していただいています。
そして、フォーマットについての説明が次の投稿で説明されています。
仕様を読んで実装だ!となった時に一番気をつけないといけないのがフォーマットですね。
特に電話番号あたり...気をつけましょう!
クライアントの情報に関連する記事が出ていました。
まずはトークンエンドポイントのクライアント認証です。
本来はclient_idと紐付けて管理されるべき情報としてredirect_uriや同意画面を出そうと思うとクライアント名やクライアントに関する説明やロゴ画像、利用規約などの情報も必要になりますし、登録状態の管理も行おうと思うと登録日や更新日、有効・無効などのステータス、管理者の連絡先などの情報も必要になってくると思います。
といってもそこまで必要になるのはもう少し先の話なので、まずは最低限+αということで名称、ID、シークレット、redirect_uriをファイルに保存しておきます。
あえて、先読みしていきましょう。
OIDCのRP情報でどのような情報を管理すべきか、というところの理解を深めるためには、こちらの仕様を参照するのが良いでしょう。
どの値が複数(配列で管理)なのか、デフォルト値をOP側が決めることでどれを省略できるか、などを考えてみるのも頭の体操になると思います。
エラーレスポンスについての詳細は、RFC 6749 やClient認証が定義されている仕様に従いましょう。
次に認可エンドポイントでのredirect_uri検証です。
認証リクエストでエラーが起こった場合、redirect_uriが有効ならそっちにエラーを返す、そうじゃなかったら返さないみたいな仕様の説明がありますが、これ戻しても正しい認証リクエストにしなおしてくれるんかぁ?と思っています。Certificationなどを意識しているなら頑張るべきかもしれませんが、個人的には安全に倒してOP側でエラーを表示でもいいと思いますね。
それではふじえさんの新作に期待しましょう。
ではまた!
ついに、データストアに手を出しましたね。
- JSONbin.ioでユーザー情報を作る
- ユーザーが入力する(想定)のemailの値を使ってデータを引く(ここはemailで引いてもいいんやで)
- その後の処理も直す
というお話です。
ユーザ情報を取得する、と言ってもまだユーザ認証画面などを作るところまでは手を出しませんので、
まだ ってことはやる気だよこの人は。
そりゃあまだ31/366ですしね。気長にいきましょう。