OAuth2.0のImplicit GrantとOIDCのImplicit Flowは何が違うの?
こちらの記事を読んでいて、以下の記述がよくわからなかったので再勉強してみました。
Implicitって言っても、OAuth 2.0 の response_type=token は使わん方がいいけど OIDC の response_type=id_token は使っていい
OAuth2.0のImplicit Grantとは?
implicitの意味は「暗黙的」という意味ですが、なにが暗黙的なのでしょうか?まずはOAuth2.0の仕様で確認してみます。
Implicit Grant の「暗黙」について
RFC6749 には以下の記述があります。
The grant type is implicit, as no intermediate credentials (such as an authorization code) are issued (and later used to obtain an access token).
ref. https://datatracker.ietf.org/doc/html/rfc6749#section-1.3.2
認可コードと呼ばれるトークン発行のための中間認証情報を省略できることが暗黙的だと書かれています。認可コードはリソースオーナーがクライアントへの権限委譲を認可した証明ですので、それを暗黙的に認可したこととし、アクセストークンを発行する、という意味だと思われます。
ではまず認可コードを付与するAuthorization Code Grant
の流れを再確認してみます。
Authorization Code Grant
では クライアントからのリダイレクトによって、リソースオーナーが認可エンドポイント(/authorize
)にリクエストを投げ、そのレスポンスとして認可コードを受け取ります。クライアントはその認可コードを受け取り、トークンエンドポイント(/token
)に認可コードをつけてリクエストを投げることでアクセストークンを受け取ります。
一方 Implicit Grant
では認可エンドポイントにリクエストを投げると、そのレスポンスからアクセストークンを受け取ります。
アクセストークンはフラグメントを通じて渡されます。フラグメント(#xxx=yyy
)に渡すことでリクエストを受け取ったサーバーはフラグメントの値を受け取ることができず、ブラウザ上で動くjavascriptが値を取得することができます。以下はRFC6749で紹介されたリクエストです
HTTP/1.1 302 Found
Location: http://example.com/cb#access_token=2YotnFZFEjr1zCsicMWpAA
&state=xyz&token_type=example&expires_in=3600
このリクエストは先ほどのフロー図の「クライアントへリダイレクト(フラグメントにアクセストークン)」に当たります。クライアントサーバー(example.com)はリクエストを受け取るとhtmlを返却します。フラグメントの値はサーバーに送られないため、サーバーはaccess_tokenフラグメントの値を知ることはできません。返却されたhtmlに埋め込まれたjavascriptがフラグメントの値を読み取り、リソースサーバーにリクエストを投げるという仕組みです。
こちらの記事でわかりやすく説明されています。
ユースケース
Implicit Grantは、クライアントシークレットを安全に管理できない Public Client 向けの認証方式です。クライアントIDとクライアントシークレットを送信するクライアント認証をスキップし、シークレット漏洩のリスクを回避します。またフラグメントを使うことでクライアントにアクセストークンの情報が記録されなくなります。
OAuth2.0 のImplicit Grantはなぜ非推奨?
公式のセキュリティベストプラクティスでImplicit Grantは非推奨になっています。このサイトで紹介されている脆弱性についてみていきます。
Insufficient Redirection URI Validation
認可サーバーに登録するクライアントのredirect_uriがパターンマッチングでの登録になるケースは避けた方がいいというものです。
認可サーバーで以下のようにredirect_uriが保存されていたとします。
https://*.example.xyz/*
攻撃者は以下のような認可リクエストをユーザーにさせることで、アクセストークン付きの認可レスポンスを攻撃者が設定したURLにすることが可能になります
https://auth.example.xyz/authorize?
response_type=token
&client_id=xxx
&redirect_uri=https://evil.example.xyz/callback // 攻撃者のuri
そのため、redirect_uriは完全一致することが推奨されています。Authorization Code Grantでも同様の対策が推奨されます。
Credential Leakage via Referer Headers
認可レスポンスで遷移するredirect_uriにアクセスした際に、redirect_uriのページで外部ページへのリンクをユーザーがクリックしたり、ページ自体が外部のページへリクエストを飛ばしているとリファラーにクエリパラメータが含まれてしまいます。
ブラウザの仕様ではフラグメントはリファラーに含まれないと記載されていますが、ブラウザのバグでリファラーに記載されてしまうケースがあったため、使わない方が無難でしょう。
Credential Leakage via Browser History
アクセストークンがフラグメントで渡されることで、ブラウザ履歴に残る可能性があります。
攻撃者がブラウザにアクセスできる場合は、履歴を辿ってアクセストークンを取得可能です。フラグメントだけではなく、アクセストークンはクエリパラメータに含むべきではなく、Authorizationヘッダーに含めることが推奨されています。(RFC6750)
Access Token Injection
これはアクセストークンが悪意あるクライアントによって盗まれるケースで、これがImplicit Grantで一番リスクとなっている部分です。
被害者のリソースオーナーが悪意のあるクライアントを使って認可リクエストを送ります。悪意のあるクライアントは取得したアクセストークンを保持します。
Evil Clientはそのままブラウザを経由してEvil Resource Ownerとして別のClientへ認可リクエストを行い、認可レスポンスで受け取ったアクセストークンを盗んだアクセストークンに置き換えて Valid Clientにリクエストを送信することができます。
これで攻撃者は盗んだユーザーのリソースにアクセスすることが可能です。
ではもしクライアントがアクセストークンに紐付いたユーザー情報を使って認証をしていた場合(いわゆるOAuth認証)、上記の例だとEvil Resource Ownerは盗んだアクセストークンのユーザーとしてログインすることが可能ということです。
この脆弱性は以下の2点によるものです。
- クライアントがアクセストークンの置き換えに気付けない
- リソースサーバーがアクセストークンの所有者をチェックしない
1つ目の「クライアントがアクセストークンの置き換えに気付けない」というのは、クライアントがトークンの中身を検証するという仕様がOAuthに存在しないためです。RFC6749では以下のように記述されています
The string is usually opaque to the client.
ref. https://datatracker.ietf.org/doc/html/rfc6749#section-1.4
日本語訳だと「アクセストークンは通常クラアントから見てランダムな文字列になっている」と書かれている通り、クライアントはアクセストークンの内容を具体的には理解できないと書かれています。
2つ目の「リソースサーバーがアクセストークンの所有者をチェックしない」というのは、一般的にアクセストークンとして使用されるBearerトークンの性質によるものです。RFC6750にもBearerトークンについて記載があります。
セキュリティトークン. トークンを所有する任意のパーティ (持参人 = bearer) は, 「トークンを所有している」という条件を満たしさえすればそのトークンを利用することができる. 署名無しトークンを利用する際, 持参人は, 暗号鍵の所持を証明 (proof-of-posession) するよう要求されない.
ref. https://openid-foundation-japan.github.io/rfc6750.ja.html#anchor3
つまりBearerトークンを持っている = 有効なトークンを利用できるクライアントな訳で、トークンエンドポイントから受け取ったアクセストークンじゃなくてもBearerトークンとして送信すればリソースサーバーは有効であると判断するということです(別途リソースサーバーはscopeやトークンの有効期限などを確認する必要があります)。そのため正式なトークンの所持者というのは仕様上だとわからないのです。
またOAuth2.0では誰がトークンの所有者であるか検証する方法を仕様で定義していないと明記されています。
この仕様は, 提示されたアクセストークンが認可サーバーから提示元のクライアントに発行されたことを確認するいかなる方法もリソースサーバーに提供しない.
https://openid-foundation-japan.github.io/rfc6749.ja.html#AccessTokenSecurity
そのため、所有者を確認するには独自拡張が必要です。例えばgoogleではtokeninfoエンドポイントを提供してアクセストークンの中身を検証することができます。
curl "https://oauth2.googleapis.com/tokeninfo?access_token=ACCESS_TOKEN"
{
"azp": "32553540559.apps.googleusercontent.com",
"aud": "32553540559.apps.googleusercontent.com",
"sub": "111260650121245072906",
"scope": "openid https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/cloud-platform https://www.googleapis.com/auth/accounts.reauth",
"exp": "1650056632",
"expires_in": "3488",
"email": "user@example.com",
"email_verified": "true"
}
各フィールドの説明はドキュメントに記載されているので省きますが、トークンの所有者を表すのはazp
で「トークンをリクエストしたアプリケーションのプロジェクト、メール、またはサービス アカウント ID」と書かれています。(おそらくsubはトークンを発行されたクライアントIDを示してると思われます)。リソースサーバーはこの情報から正しいアクセストークンなのか検証することが可能です。
しかし、全く同じクライアントで別のユーザーの有効なアクセストークンを使われると不正を検知することは難しいです。
Authorization Code Grant も認可コードがブラウザに出る
Implicit Grantではアクセストークンがブラウザに出るリスクがありますが、Authorization Code Grantも認可コードがブラウザに出ます。しかし、トークンリクエスト時にクライアント検証が必要なため、認可コードを他のクライアントで使用するとエラーになります。直接アクセストークンを取得できるわけではないのでリスクは軽減されます。
ただし、同じクライアントで別のユーザーに発行されたアクセストークンは弾かれません。対策として、認可コードの有効期限を10分にし、使用回数を1回に制限することで影響を最小限にすることが推奨されています(RFC6749)。
Implicit Grantまとめ
これまでの話を簡単にまとめると以下の通りです。
- Implicit Grant はアクセストークン漏洩の危険があるため非推奨
- OAuthを認証として使用する事によってアクセストークン漏洩による影響が大きくなってしまう
次にOIDCのImplicit Flowを見てみます。
OIDCのImplicit Flowとは?
処理の流れはOAuthのImplicit Grantと同じですが、トークンレスポンスでID Tokenが返されるようになります。(ID Tokenは後ほど説明します)
認可リクエスト(/authorize
)のresponse_type
パラメータに、id_token
または、id_token token
が指定された場合に該当します(token
を指定するとアクセストークンも返ります)。
また、ID Tokenを受け取るにはscope
パラメータにopenid
を付けるのが必須です(値が存在しない場合の定義は仕様にはなく、OAuthとして扱うことも可能と記述されています)。
以下はresponse_type=id_token
の場合です。(他にも必須パラメータありますが説明に関係する部分のみ記述してます)
また、OIDCの定義に合わせて用語を変更しています。
- Resource Owner -> EndUser
- Client -> Relying Party (RP)
- Authorization Server -> Identity Provider (IdP)
ID Tokenとは
ID TokenはOIDCで導入されたものでエンドユーザーの認証を可能にするトークンです。ID TokenはJWTでなければならず、以下のような JWT クレームを持ちます。
クレーム名 | 説明 |
---|---|
sub | ユーザーの識別子 |
name | ユーザー名 |
gender | ユーザーの性別 |
birthday | ユーザーの誕生日 |
仕様で定義されている全てのクレームはこちら
なぜOIDCのImplicit Flow(response_type=id_token)は非推奨ではないのか
さて、やっと本題に戻ります。
Implicitって言っても、OAuth 2.0 の response_type=token は使わん方がいいけど OIDC の response_type=id_token は使っていい
Implicit FlowではID Tokenを受け取ることでクライアントはJWT クレームからユーザー情報を取得し認証することが可能になります。なのでOAuthで行なっていたリソースサーバーにアクセストークンを使ってユーザー情報を取得しにいく処理はなくても認証可能になるため、OAuthでの脆弱性がなくなり非推奨ではないということです。
Implicit Flow(response_type=id_token token)は?
ちなみにOIDCのImplicit Flowでアクセストークンを取得するケース(response_type=id_token token)は非推奨になるのでしょうか?
OIDCではaccess-token-injection対策としてIDトークンと一緒にアクセストークンが発行された場合はID Tokenに追加のat_hash クレームが必要になります。at_hashにはアクセストークンのハッシュ値を入れます。
具体的にはID Tokenの署名と同じハッシュアルゴリズムでアクセストークンのハッシュ値を計算し、その左半分をBase64URLエンコードした文字列です。
クライアントはResource Ownerから認可レスポンスを受け取った際にat_hashの値と送られたアクセストークンを照らし合わせてすり替えが発生してないか確認することができます。
ID Tokenとアクセストークン両方がすり替えられると通り抜けられてしまいますが、IDトークンのJWTには置き換えを防ぐためのクレームがいくつかあるためそれらを検証することですり替えに気づくことができます。
例としてnonceクレームを取り上げます。nonceはクライアントが認可リクエスト送信時にクエリパラメータに含める文字列です。認可サーバーはnonceが認可リクエストに含まれていた場合に認可レスポンスのIDトークンにnonceクレームを追加し認可リクエストで受け取ったnonceの値を入れて返却します。
nonceはリプレイアタック対策として導入されました。リプレイアタックとは認証データを盗み出し、盗んだデータを使って被害者として認証するものです。ここでいう認証データはIDトークンに当たります。
nonceを使うことでIDトークンをワンタイムのものにすることが可能です。RPは認可レスポンスで受け取ったIDトークンのnonceクレームの値が、自身で生成したものかつ一度も使われてない場合有効であると判断します。
これにより攻撃者がIDトークンをすり替えても同じRPが生成していなければダメですし、同じRPでもすでに使われたIDトークンであればエラーを返します。
振り返り
OAuthからOIDCまでざっと復習しました。調査する中で色々なRFCを読めたのでよかったです。OAuth2.1でRFCがまとまるらしいので楽しみです。何か間違いがあればご指摘よろしくお願いします🙇
参考資料
Discussion
本投稿の流れのように、Implicitに限らず、フロントチャンネル(ブラウザのリダイレクトなど)で渡されるパラメータは(最低でもユーザーのブラウザから)漏洩の可能性がある を大前提として、その結果起こり得る実害を考えることが重要です。
Xで引用させていただきましたが、プロトコルが拡張という関係であること、名前が同じImplicitではあるものの、目的も出自も異なります。
といったように、個別に見ていくことが重要です。
参考にします!ありがとうございます!!