はじめてのOkta Workforce Identity Cloud (WIC) [第9回] OpenID Connectってなんだ?
目次
本ブログ記事では、Okta Workforce Identity Cloud (以降、Okta WIC) とSaaSアプリ間のフェデレーションで利用できるOpenID Connect (以降、OIDC) について触れてみたいと思います。
認証に関わっていると、「OAuth 2.0は認可のプロトコルだから、認証に使っちゃダメ!認証にはOIDCを使いなさい!」という話を聞くことが多いと思います。
「OAuth 2.0だって認証するじゃん。なんで認可だけなの?」っていう疑問を持って悶々としたままの日々を過ごしていませんか?
「正直、OAuth 2.0とOIDCってどう違うのか、よく分からない。。。」なんて悩みも。
本ブログ記事では、そんな疑問/お悩みを解消できるよう、「こんな感じで説明すれば、分かりやすいんじゃないだろうか?」という思いで、解説をしてみたいと思います。
以降の解説は、以下のブログ記事の内容を実施済み (理解済み) の前提で進めていきます。
・はじめてのOkta Workforce Identity Cloud (WIC) トライアル環境の構築
・はじめてのOkta Workforce Identity Cloud (WIC) [第1回] ユーザーと認証器の関係を紐解く
・はじめてのOkta Workforce Identity Cloud (WIC) [第2回] 多要素認証を紐解く
・はじめてのOkta Workforce Identity Cloud (WIC) [第6回] APIってどうやって使うの? (トークン編)
・はじめてのOkta Workforce Identity Cloud (WIC) [第7回] APIってどうやって使うの? (OAuth 2.0認可コード編)
・はじめてのOkta Workforce Identity Cloud (WIC) [第8回] APIってどうやって使うの? (OAuth 2.0 Client Credentials編)
なぜ、OAuth 2.0は「認可のプロトコル」と言われるのか?
「OAuth 2.0は認証のプロトコルじゃない。認可のプロトコルだ。」と言われます。
でも、[第7回] のOAuth 2.0 認可コードグラントでは、リソースオーナーは認証していますよね。
なのに、なぜ「OAuth 2.0は認可のプロトコル」だと言われるのでしょうか?
この混乱の原因は、主役がちゃんと説明されていないことにあるような気がします。
「認証」だの「認可」だのといっているその主役は、「リソースオーナー」ではなく「クライアント」なのです。
以降、その意味を紐解いていきましょう。
OAuth 2.0の4つの役割
以下は、今回のOAuth 2.0の解説に登場する4役です。
A. 「リソースオーナー」は、リソースの所有者 =「人 (+Webブラウザ) 」です。[email protected]です。
B. 「クライアント」は、「インターネット上のWebアプリケーション」です。今回の主役です。https://goodweb.shopです。
C. 「認可サーバ」は、「認証と認可を行ってアクセストークンを発行するサーバ」です。https://auth.siteです。
D. 「リソースサーバ」は、リソースオーナーが所有するリソースを預かる場所です。今回の例では「写真やファイルを保存するサーバ」です。https://storager.xyzです。
認可サーバを運営する会社とは異なる会社がリソースサーバを運営している、というイメージです。(これは、OAuthでは一般的な形態です。)
クライアントは、OAuth 2.0ではリソースオーナーの認証ができない
「主役はクライアント (goodweb.shop) だ」という観点で、なぜOAuth 2.0では認証が成立しないのか、その理由を、認可コードグラントの例を使って解説します。
[第7回] でご紹介したOAuth 2.0の認可コードグラントを簡単におさらいしますと、下図の (1)〜(8) の流れで、クライアントにアクセストークンが払い出され、クライアントはそのアクセストークンを使って、リソースサーバからリソースオーナーの情報 (リソース) を取得する (下図 (9) と (10) ) というものでした。
「認証」とは、「クライアント (goodweb.shop) が、リソースオーナーが誰なのかを知ること」と考えて、上記の図を眺めてみてください。
(a) クライアント (goodweb.shop) は、(1)〜(7) の中ではリソースオーナーが誰なのかを知ることができるステップがありません。
上図の (2) と (3) で認証が行われていますが、これは「リソースオーナー」と「認可サーバ」との間で認証しているのであって、クライアント (goodweb.shop) は関与していません。
でも、クライアント (goodweb.shop) は、上図の (8) で認可サーバからアクセストークンを受け取っていますので、「じゃぁ、このアクセストークンの中にユーザー名があれば、認証になるんじゃないの?」と思いませんか?
その着眼点は正しいのですが、ここがポイントで、「OAuth 2.0の仕様が、そうなっていない」のです。
(b) アクセストークンが持ついくつかの情報のうち、sub (subject) という変数の値にリソースオーナーのユーザー名 ([email protected]) が存在することがあります。
(これも100%ではなく、一般的には「認可サーバ (図中のauth.site) 自身が内部で管理するためのユーザーの識別子」であることの方が多いです。)
(c) 仮にsubの値にユーザー名が存在していたとしても、アクセストークンが認証に使えない理由は、aud (オーディエンス) という変数の値が要因です。
このaud変数の値には「アクセストークンを受け取るのは誰なのか」が記載されており、その値がクライアント (goodweb.shop) ではなく、リソースサーバ (storager.xyz) になっています。
そのため、OAuth 2.0の仕様では、クライアント (goodweb.shop) は、「(自分宛じゃないので) アクセストークンの内容や正当性をチェックしない」ようになっており、「アクセストークンをリソースサーバ (storager.xyz) に渡すだけ」なのです。
(d) その後、上図 (9), (10) のように、クライアント (goodweb.shop) がアクセストークンを使って、リソースオーナー ([email protected]) が所有する写真を、リソースサーバ (storager.xyz) から取得したとしましょう。
(e) リソースサーバ (storager.xyz) は、アクセストークンのaudの値が自分自身宛なので、中身と正当性をチェックし、問題がなければ、要求されたリソースオーナーのリソース (この例では、写真) を返送します。
上図 (10) でリソースサーバから取得したその写真の属性情報に、リソースオーナーのユーザー名 ([email protected]) が存在しなかったら、クライアント (goodweb.shop) は未だ尚、リソースオーナーが誰なのかを知ることができません。
この後も、リソースサーバから写真やファイルをいくら取得したとしても、それらの属性情報にリソースオーナーのユーザー名が存在していなければ、クライアントはずっとユーザー名を知らないままです。
===== << Tips >> =====
上記の解説では、まるで「写真の属性情報にユーザー名さえあればOK」みたいな書き方をしてしまっていますが、それはそれで課題があります。
写真の属性情報のユーザー名がリソースオーナーと一致することが100%保証されているのならよいかもしれませんが、もし、zsaku.zkawaが、たまたま他の人 (例えば、ptaro.pmoto) の写真を保存していて、その写真のユーザー名を示す属性情報が[email protected]だったとしたら、クライアント (goodweb.shop) は間違った認証を成立させてしまうことになります。
そもそも論として、仮にリソースサーバから得たリソースのユーザー名属性が、リソースオーナーのユーザー名と見事に一致したとしても、リソースオーナーを認証したのは「https://auth.site」なのに、認証に関わっていない「https://storager.xyz」から得られた情報で認証OKにするって、違和感ありますよね?
===================
このようにOAuth 2.0の仕様では、クライアントは、リソースオーナーのリソースへのアクセス許可 = 「認可」 はもらっているので、リソースの取得はできるけれど、リソースオーナーの「認証」はできないのです。
これが、「OAuth 2.0は認証のプロトコルじゃない、認可のプロトコルだ。」と言われる所以です。
アクセストークンの中身を見てみる
では、OAuth 2.0のアクセストークンがどのような情報を持っているのか、中身を見てみましょう。
[第7回] で設定した「OAuth 2.0 認可コードグラント+PKCEの設定」をそのまま使います。
(「OAuth 2.0 認可コードグラントの設定 (PKCEなし) 」でもよいです。)
Postmanが取得したアクセストークンをコピーしてください。
[第8回]でJSON Web Token (JWT) について少し触れましたが、実はアクセストークンもJWTで構成されているので、下記のサイトにアクセストークンをペーストすることで中身を確認できます。
下図のように、アクセストークンのペイロード (PAYLOAD) には、複数の「変数 : 値」が存在していることが分かります。
これら一つ一つを「クレーム (claim)」と呼びます。
ここではaudクレームの値とsubクレームの値に注目してください。
audクレームの値は、リソースサーバであるOkta WICのドメイン (xxxxx.okta.com) になっています。
そのため、クライアントはアクセストークンをクライアント自身のものとしては受け取らず、リソースサーバへ渡すだけなのです。
subクレームの値はユーザー名 ([email protected]) になっていますが、audクレームの値がクライアントを指していないので、クライアントはアクセストークンを参照しませんから、クライアントがこのsubクレームの値を知ることもないのです。
===== << Tips >> =====
JWTのaudクレームの扱いについては、RFC7519 「4.1.3. "aud" (Audience) Claim」に記載されています。
以下、抜粋です。
「The "aud" (audience) claim identifies the recipients that the JWT is intended for. Each principal intended to process the JWT MUST identify itself with a value in the audience claim. If the principal processing the claim does not identify itself with a value in the "aud" claim when this claim is present, then the JWT MUST be rejected. 」
〜↓機械翻訳〜
「aud」 (オーディエンス) クレームは、JWTが意図する受信者を特定する。 JWTを処理することを意図された各プリンシパルは、audience claimの値で自身を識別しなければならない [MUST] 。 このクレームが存在するときに、そのクレームを処理するプリンシパルが「aud」クレームの値で自分自身を識別しない場合、そのJWTは拒否されなければならない [MUST] 。
OIDCは「認証のプロトコル」である
上記の解説を見ると、OAuth 2.0に欠陥があるように感じるかもしれませんが、OAuth 2.0は全く悪くありません。
OAuth 2.0は、「クライアントが、リソースオーナーの認可を得て、リソースサーバから情報を取得するためのプロトコル」として誕生したので、上記は期待された通りの動作です。
その動作でよいアプリも存在するのです。
例えばクライアントとして、Postmanをイメージしてみてください。
Postmanは、リソースオーナーが誰であろうが知る必要はなく、リソースオーナーから指示されたリソースサーバのAPIへアクセストークンを送って、リソースを得るだけなので、OAuth 2.0でよいのです。
一方で、上図のgoodweb.shopのようなアプリの場合、それを運営する会社は、「OAuth 2.0は便利なんだけど、誰がリソースサーバから写真を取得しているのかは把握したいよね。」と思うはずです。
クライアント (goodweb.shop) がユーザー名を知ることで、リソースサーバからの情報をリソースオーナーに提供 (中継) するだけでなく、他のデータベースから、[email protected] に関係する情報を提供したり、アクセス履歴から興味どころを探ったり、といった具合に、サービスを色々と拡張できますし。
そこで、OpenID Connect (OIDC) の登場です。
OIDCは、OAuth 2.0の機能はそのまま使えるようにしつつ、認証もできるようにOAuth 2.0を拡張したプロトコルです。
OIDCの3つの役割
以下は、OIDCの解説に登場する3役です。
OIDCでは、OAuth 2.0で使われていた用語が変化します。(これもOAuth 2.0とOIDCを分かり難くしている一つの要因のような気がしますが、決まり事なので慣れるしかありません。)
A. 「End-User」= OAuth 2.0でいうところの「リソースオーナー」です。
B. 「Relying Party」= OAuth 2.0でいうところの「クライアント」です。「RP」と略されることも多いです。
C. 「OpenID Provider」= OAuth 2.0でいうところの「認可サーバ」です。「OP」と略されることも多いです。
OIDCの認証の処理には、「リソースサーバ」は関与しません。
Relying Party (クライアント) は、OIDCであれば End-User (リソースオーナー) の認証ができる
OIDCはOAuth 2.0を拡張したプロトコルですから、基本的な流れはOAuth 2.0と変わりませんので、差分だけ解説していきます。
[第7回] と上記で長々と解説したOAuth 2.0認可コードグラントの流れが概ね理解できていると、OIDCはとてもシンプルに感じると思います。
(a) 上図 (1) の認可リクエストのスコープに「openid」と「profile」が追加されています。
(b) 上記 (a) のスコープ値によって、OpenID Provider は「IDトークン」を生成し、上図 (8) でアクセストークンと共に Relying Party に送ります。
(c) IDトークンのaud (オーディエンス) = 受け取り手は「クライアントID」= Relying Party = goodweb.shop なので、Relying Party (goodweb.shop) は、IDトークンの内容や正当性をチェックします。
このことで、Relying Party (goodweb.shop) は、「[email protected] は、OpenID Providerで認証が正しく行われたんだな」ということがわかります =Relying Party における「認証」が成立しました。
OIDCにおいても、OAuth 2.0の機能はそのまま並行して利用できます。
上図 (d) および (e) については、この後の動作確認にて触れます。
OIDCの動作確認
では、OIDCの動作を確認してみましょう。
再び、[第7回] のPostmanとOkta WICによる「OAuth 2.0 認可コードグラント+PKCEの設定」をそのまま使います。
===== << Tips >> =====
[第7回] のブログ記事で、Okta WICのアプリケーションを設定する際に「OIDC - OpenID Connect」を選択しました。
実はOkta WICは、[第7回]の時点から既にOIDCとして動作する設定になっています。
よって、Okta WICに対する追加の設定はありません。
===================
Postmanの設定で、まずスコープに「openid」だけを追加してください。(区切り文字は、空白です。)
上記が設定できたら、「Get New Access Token」をクリックしてください。
スコープに「openid」が追加されたことで、Postmanは、アクセストークンだけでなく、IDトークンも受け取っていますね。
IDトークンをコピーしてください。
IDトークンもJWTで構成されているので、下記のサイトにIDトークンをペーストすることで中身を確認できます。
下図のように、IDトークンのペイロード (PAYLOAD) にも、アクセストークンと同様に複数のクレームが存在していますが、ここでもaudクレームの値とsubクレームの値に注目してください。
IDトークンの場合は、audクレームの値が「クライアントID」になっているので、クライアント (Postman) が受け取るものとして発行されていることが分かります。
ただ、このIDトークンには、どこにもユーザー名が存在していません。
subクレームの値は、ユーザー名ではなく、Okta WIC内でユーザーをユニークに管理するための、ユーザー識別子となっています。
Okta WIC内でのユーザー識別子は、Admin Consoleの「ディレクトリ」→「ユーザー」で該当ユーザー (本例では[email protected]) を選択した際のURLに表示されます。
「OpenID Connect Core 1.0 incorporating errata set 2」の「2. ID Token」の章を見てみると、subクレームについては、以下のように書かれています。
「A locally unique and never reassigned identifier within the Issuer for the End-User」
ちょっと意訳→「OIDCでいうところのEnd-User (OAuth 2.0でいうところのリソースオーナー) の発行者の内部でユニークになっていて、ずっと再割り当てされることのない識別子」
「発行者」とは、Okta WICのことです。
Okta WICのユーザー名は、ユニークであることは保証されるものの、変更可能な値であり、他の人に割り当てることもできるため、この仕様を満たすとなると、ユーザー名はsubクレームの値に使えません。
Okta WICの内部で、End-User (リソースオーナー) をユニークに扱える & 再割り当てされることのない識別子となると、ユーザー識別子 (上記のAdmin Console画面の値) になることは頷けます。
sub以外のクレームで、「OpenID Connect Core 1.0 incorporating errata set 2」の「2. ID Token」の中で「REQUIRED」 (=「必須」) という文字が存在するのは、iss、aud、iat、exp、(条件付きで) auth_time ですが、それらの必須クレームの値を見ても、ユーザー名を示すものは存在していません。
よって、Relying Party (= Postman) から要求するスコープに「openid」だけ加えても、Relying Party はIDトークンからユーザー名を得ることができないのですが、それを得るためのオプションが用意されています。
「OpenID Connect Core 1.0 incorporating errata set 2」の「5.4 Requesting Claims using Scope Values」を見ると、いくつかのオプションの中に「profile」があります。これを追加してみましょう。
Postmanの設定で、スコープに「openid」に加え「profile」も追加してください。(区切り文字は、空白です。)
上記が設定できたら、「Get New Access Token」をクリックしてください。
もう一度、下記のサイトに再取得したIDトークンをペーストして、中身を確認してみましょう。
今度は、下図のように、IDトークンのペイロード (PAYLOAD) に、「name」クレームと「preferred_username」クレームが加わっていると思います。
これで、Relying Partyが期待する値 =「Okta WICに設定されたEnd-Userのユーザー名」が得られるようになりました。
OIDCの仕様上では「profile」はオプション扱いですが、Relying PartyがEnd-Userのユーザー名を知るには、要求するクレームには「openid」だけでなく「profile」も加えるのが良さそうですね。
userinfoエンドポイントについて
Relying Partyを運営する会社としては、「ユーザーの情報って、ユーザー名だけじゃなく、メールアドレスや住所とかも欲しいよね。」という要求もあると思います。
ここでは、それらを得る方法をお伝えします。
前提として、End-User (リソースオーナー) である[email protected]は、属性情報としてOkta WIC内で以下のような値を持っているとします。
では今度は、「OpenID Connect Core 1.0 incorporating errata set 2」の「5.4 Requesting Claims using Scope Values」にある、「Profile」以外のオプション (=email、address、phone) も、Relying Party (Postman) から要求するスコープに追加して、IDトークンを再取得してください。
もう一度、https://jwt.io でIDトークンの中身をみてみましょう。
「あれ?emailクレームは加わったけど、それ以外は、スコープにProfileオプションだけを加えた時と何も変わってないぞ?」ってなると思います。
実は、アクセストークンとIDトークンの両方が同時に認可サーバから発行される、認可コードグラント方式のような場合、「Thin ID Token」といって、IDトークンは必要最小限の情報しか持たない仕様になっているのです。
===== << Tips >> =====
「じゃぁ、Thinじゃなくて、Fatもあるの?」との疑問が沸くと思います。
はい、あります。
OpenID ProviderがIDトークンだけ返答する場合、そのIDトークンは、多くの情報を持つ「Fat ID Token」になります。
この挙動については、「OpenID Connect Core 1.0 incorporating errata set 2」の「5.4. Requesting Claims using Scope Values」の、以下の説明が該当します。(「Thin」や「Fat」とは明確に書かれていませんし、機械翻訳してみても、やや難解ですが。)
The Claims requested by the profile, email, address, and phone scope values are returned from the UserInfo Endpoint, as described in Section 5.3.2, when a response_type value is used that results in an Access Token being issued. However, when no Access Token is issued (which is the case for the response_type value id_token), the resulting claims are returned in the ID Token.
〜↓機械翻訳〜
profile、email、 address、phoneのスコープ値によって要求されたクレームは、セクション5.3.2で説明されているように、レスポンスタイプ値が使用され、その結果アクセストークンが発行される場合、UserInfoエンドポイントから返されます。しかし、アクセストークンが発行されない場合(レスポンスタイプ値がid_tokenの場合)、結果として得られるクレームは、IDトークンで返されます。
〜〜〜〜〜〜〜
具体的には、OAuth/OIDCには「インプリシットグラント (Implicit grant)」という方式もあり、その方式を使った場合で、且つクライアントからの要求に「レスポンスタイプ=id_token」と指定した場合には、認可サーバはIDトークンだけ返答しますので、その場合は「Fat ID Token」になります。
[参考]: API Access Management with Okta
ただ、インプリシットグラント方式は、認可サーバが発行したトークンをWebブラウザ経由でRelying Party (クライアント) に渡す方式なので、Webブラウザにトークンが到達した時点で、悪意のある第三者によってトークンの置き換えが割と簡単にできてしまうため、安全性に問題があると言われています。
よって、Oktaでは「認可コードグラント+PKCE」の利用を推奨していることもあって、「Fat ID Token」が取得できる場面は少ないように思います。
[参考]: Implement authorization by grant type
===================
「Thin ID Tokenでは得ることができないユーザー情報 (属性) を得るにはどうすればよいのか」の解決策として、「userinfoエンドポイント」が用意されています。
macOSやLinuxなどのターミナルから、下記のcurlコマンドを実行してみてください。
※1)「xxxxx.okta.com」は、ご自身のOkta WICのドメイン名に置き換えてください。
※2)「Authorization: Bearer」に指定するのは「アクセストークン」です。(「IDトークン」ではありませんのでご注意ください。)
上記は画像ですが、コピー&編集して利用できるように、以下にテキストでも貼り付けておきます。
curl -X GET "https://xxxxx.okta.com/oauth2/v1/userinfo" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \<
-H "Authorization: Bearer eyJra〜〜〜wgYPJw" | jq
上記のcurlコマンドで Userinfoエンドポイントにアクセスすることで、以下のように、IDトークン ( = Thin ID Token) では得られなかったユーザー情報 (属性) が得られます。
userinfoエンドポイントから得られるユーザー情報 (属性) をカスタマイズしたい場合には、下記Linkの内容に従って設定を行ってください。
Add Custom Profile Attributes as Claims in an ID Token or /userinfo
まとめ
本ブログ記事では、OpenID Connect (OIDC) がどういうものなのかについて解説してみました。
「OIDCをインターネットで検索して記事を読み漁っても、なかなか理解できない。。。なんて難しいんだ、OIDC。。。」と思われていた方も多いと思います (長い間、私がそうでした) が、ここ数回のOAuth 2.0に言及したブログ記事と合わせて理解することで、大枠は掴んで頂けたのではないかなと思うのですが、いかがでしょうか?
(より詳細を追求したい方は、「OpenID Connect Core 1.0」や「RFC7519: JSON Web Token (JWT)」あたりを深掘りして頂ければと思います。)
アイデンティティ管理者として、早い段階でOAuth 2.0/OIDCがどういうものなのかの概略だけでも理解しておくと、後々が楽になると思いますので、本記事がその手助けになっていれば幸いに思います。
==========
Okta WICでは、認証の強化だけに留まらず、管理者が行うユーザーの登録/変更/削除に関わる業務の自動化や、人事管理システムや既設ディレクトリなどとの柔軟な連携も可能です。
Oktaでは、これらの機能を体感頂くことができる「Okta Essentials」トレーニングをご用意しています。(日本語でのトレーニングもご用意しています。)
このトレーニングは全体像を体系的にご理解頂ける内容となっていますので、是非ともご活用ください。