OAuth 2.0 に入門する
こちらの本に記載されている以外の事柄についても派生して諸々記載してある
OAuth の必要性
username / password を与えてしまうと該当ユーザーが有している権限すべてをサードパーティー製アプリケーションが行えるようになってしまう。OAuth では username / password の代わりに細分化された権限を持ったトークンを発行しサードパーティー製アプリケーションに与えるため、サードパーティー製アプリケーションが過剰な権限を持つ必要がなくなる
サードパーティー製アプリケーションから認証情報が攻撃者に漏れてしまった場合、username / password の場合はユーザーが使用している API を使用しているすべてのサードパーティー製アプリケーションに対して username / password を変更する必要があるが、トークンの場合はサードパーティー製アプリケーションがそれぞれに対して発行しているトークンを revoke して再発行すれば良い
username / password は重い権限を有しているため攻撃者に漏れて攻撃されてしまった場合の影響は大きくなってしまうが、トークンは必要最低限の権限のみ有しているため影響は限定的である
OAuth のトークン
トークンは大きく分けて2種類ある
アクセストークン
リフレッシュトークン
(認可コード)
アクセストークン
アクセストークンが持っている情報は、
誰のどのリソースにどのような操作を行うことが許可されているか
スコープと呼ばれる
有効期限
となる。アクセストークンは権限が必要となるすべてのリクエストに含まれるべきで、アクセストークンの持っている権限にリクエストに必要な権限が含まれていればそのリソースにアクセスすることができる。
有効期限はアクセストークンを取得するときのレスポンスボディに含まれている。レスポンスボディの例としては、
code: response.json
{
"access_token": "xxx",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "yyy"
}
リフレッシュトークン
リフレッシュトークンはアクセストークンの再発行を要求する際に用いられる。リフレッシュトークンの発行は標準仕様では必須とされていない。リフレッシュトークンは基本的にアクセストークンと比較して長い有効期限(30日間など)となる。
認可コード
認可コードは「リソースオーナーがクライアントへの権限委譲に同意した証」として発行される。そして、クライアントから認可サーバーに対してアクセストークンを要求する際に利用される。
認可サーバーはリソースオーナーの、username / password を確認し、アクセス権委譲に関する同意を得ると認可コードを生成し、HTTP リダイレクトを利用してクライアントに送信する。クライアントはトークンエンドポイントにアクセストークンを要求する際に認可コードを利用します。
認可コードを利用できるのは一度のみで、また有効期限は標準仕様では10分以内を推奨している。
気になること
トークンの値の形式
おそらく Base64 エンコードするのだろうが、デコードした際はどのようなフィールド・値を用意しておけば良いんだろうか?無作為な文字列で OK?
OAuth のエンドポイント
OAuth には3つのエンドポイントが登場する
認可サーバーが提供する URI
認可エンドポイント
トークンエンドポイント
クライアントが提供する URI
リダイレクトエンドポイント
認可エンドポイント
クライアントがアクセス権を持っていないリソースにアクセスする際には、まず認可エンドポイントにアクセスし、username / password を入力させリソースオーナーの認証を行う。認証が完了したあと、クライアントにリソースへのアクセス権を委譲することに同意を求められ、リソースオーナーが同意すると同意の証として認可エンドポイントがリダイレクトエンドポイントに送信される。
トークンエンドポイント
認可コードを受け取ったクライアントはそれとともにともに必要なパラメータを指定してトークンエンドポイントにリクエストを投げることでアクセストークンを取得できる。トークンエンドポイントでは Basic 認証によってクライアントのアイデンティティが確認され、Basic 認証として Authorization ヘッダに設定されるのは client_id (クライアントの識別子) と client_secret (パスワードに相当)となる。client_id / client_secret は認可サーバーにクライアントを事前登録する際に発行される。
リダイレクトエンドポイント(redirect_uri)
認可サーバーから認可コードを受け取るために使用される。リソースオーナによる権限委譲の同意が行われると redirect_uri に対して 302 リダイレクトされる。
気になること
事前登録のタイミングは認可コードを発行するタイミング?
client_id / client_secret は無作為に生成された文字列で良い?
OIDC では redirect_uri をサーバーに事前に登録しておく必要があった気がするが、OAuth では必要ない?
OAuth のグラントタイプ
OAuth では4つのグラントタイプが定義されている
認可コードグラント
最も複雑かつ重要なグラントタイプ
インプリシットグラント
クライアントクレデンシャルグラント
リソースオーナーパスワードクレデンシャルグラント
パブリッククライアントの場合のグラントタイプとして推奨されている「PKCE を用いた認可コードグラント」もある。
認可コードグラント
認可コードグラントではリソースオーナー、クライアント、認可サーバーの3者でのやり取りとなるため 3-legged OAuth とも呼ばれる。
認可コードグラントの特徴はセキュアなことで、理由として、
アクセストークンがブラウザを介さずに直接クライアントと認可サーバ間で受け渡されるため流出するリスクが低いこと
相手が正しいことを確認しながらやり取りを行うため
認可サーバーは ID / password の入力などの方法によってリソースオーナーの認証を行う
認可サーバーは HTTP Basic 認証によってアクセス元のクライアントの client_id / client_secret を確認する
クライアントは URI と SSL 証明書によってアクセス先の認可サーバーのアイデンティティを確認する
リフレッシュトークンが発行可能であること(標準仕様ではない)
リソースオーナーが再度認証を行わなくても新しいアクセストークンを取得することができる
感じたこと
クレデンシャルがブラウザを介さなければそれなりにセキュアに認可のフローが行えるということ
安易にブラウザを経由してはいけない
既存の IdP であっても URL に id_token 等が返って来ているサービスがあったりするが、おそらく望ましい実装方法ではないんだろうな
OAuth の大まかなステップ
1. 認可エンドポイントから認可コードの取得
2. トークンエンドポイントからアクセストークンの取得
3. クライアントがアクセストークンを利用してリソースにアクセスをする
認可エンドポイントからの認可コードの取得
認可コードへのリクエストパラメータは以下の通り。
response_type
値として code を設定する。認可サーバーはこの値により認可コードの発行を求められていることを知る
client_id
クライアントの事前登録時に発行された client_id を設定
state
クライアントが生成したランダムな値を設定。state はユーザーのセッションと紐付けて管理することで CSRF を防ぐ
標準仕様では必須とはなっていない
クライアントは state パラメータの値としてランダムな文字列を生成し、セッションと紐付けて管理。そして認可サーバーに state の値が渡され、クライアントにレスポンスとして state の値を返します。このとき、クライアントではフローの冒頭に生成した state の値とクライアントが受け取ったレスポンス内の state の値が一致しているかどうかを確認することで CSRF を防ぐ。
scope
クライアントが要求するスコープを記載
redirect_uri
クライアントの事前登録の際に登録した redirect_uri を設定
ちなみに、リソースオーナーの認証でよく用いられるのは username / password ではあるが、OAuth の標準仕様で定められているわけではなく、2段階認証や生体認証でも問題ない。
リソースオーナーの認証のやり取りはリソースオーナーと認可サーバーの間でおこなわれれ、クライアントはこのやり取りには関わらないためクライアントはリソースオーナの username / password を知ることはない。
redirect_uri にリダイレクトする際にはクエリパラメータとして以下の値が追加されている。
code
認可コード。このあとのアクセストークン取得のステップに必要となる
state
先程認可コードを取得する際に追加した state の値が付与されている
認可コードはリソースオーナーのブラウザを一度介してからクライアントに渡るため認可コードを抽出することは比較的容易である。したがって認可コードの有効期限は比較的に短めに設定されており、標準仕様では10分以内を推奨している。
感じたこと
ブラウザを介する必要のあるクレデンシャルは有効期限を短めに設定することで流出した際のリスクを低減する
標準仕様としては策定されていないがセキュリティ的に行っておいたほうが良い実装が結構ありそうな印象。RFC だけではなくその時点でのベストプラクティスをしっかりと追っていかないとすぐダメになりそうな感がある
トークンエンドポイントからアクセストークンの取得
取得した認可コードを付与して、クライアントあらトークエンドポイントにリクエストを投げる(これをトークンリクエストと呼ぶ)。トークンリクエストではクライアント認証が行われるため、Basic 認証の仕組みを使用して client_id / client_secret の値を送付する。リクエストボディに必要なパラメータは、
grant_type
値に authorization_code を設定する。認可サーバーはこの値をもとに認可コードグラントによるリクエストであることを知る
code
redirect_uri
client_id
client_secret
そしてトークンリクエストのレスポンスのことをトークンレスポンスと呼ぶ。トークンレスポンスのボディにはリフレッシュトークン及びアクセストークンの有効期限がクライアントに渡される。トークンレスポンスの形式として、
code: token_response.json
{
"access_token": "xxx",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "yyy"
}
となる。それぞれのフィールドの説明は以下の通り。
access_token
この値がアクセストークンとなる
token_type
このトークンが Bearer トークンであることを示す
expires_in
トークンの有効期限が秒単位で入っている。
refresh_token
この値がリフレッシュトークンとなる。標準仕様ではないため必ず返ってくるとは限らない。
クライアントがアクセストークンを利用してリソースにアクセスをする
クライアントはリソースサーバーのリソースにアクセスする。このリクエストの投げ方はリソースサーバーの API 仕様を確認する必要があるが、Authorization Header の値として Bearer という文字列とともにアクセストークンの値をsっていすることは共通している。
インプリシットグラント
現在では非推奨で、代わりに PKCE を使用した認可コードグラントが推奨されている。
必要なパラメータはresponse_type として token が設定される
インプリシットグラントはパブリッククライアントのためのグラントタイプで、例えばクライアント再度 JavaScript で実装されたアプリやネイティブアプリケーションでの利用が想定されている。パブリッククライアントのためのグラントタイプであるためここで登場するクライアントは client_id / client_secret を安全に保管することができない。したがって、クライアントから認可サーバーのリクエストの中でクライアントの認証が行われないことが1つの特徴となる。リクエストに client_id は含まれるが client_secret は含まれず、事前登録した redirect_uri にアクセストークンを渡すことを持って正しいクライアントのへの受け渡しを担保している。
インプリシットグラントは1度のリクエストでアクセストークンの発行が行われる。また、リフレッシュトークンの発行は禁止されている。リフレッシュトークンがないためアクセストークンのゆこう期限が切れた場合は再度インプリシットグラントのアクセストークン発行のシーケンスをやり直す必要がある。
アクセストークンは URL のフラグメント(# 以降の文字列)に格納されており、これはリダイレクトを受け取った Web サーバー上での流出に配慮したものとなる。JavaScript はフラグメントにアクセスできるが Web サーバーにはフラグメント部分は送信されないため、Web サーバー側からのアクセストークンの流出は防ぐことができる。
インプリシットが非推奨となった理由
Moreover, no viable method for sender-constraining exists to bind access tokens to a specific client (as recommended in Section 2.2) when the access tokens are issued in the authorization response. This means that an attacker can use leaked or stolen access token at a resource endpoint.
アクセストークンはリダイレクトでクライアントに受け渡されるため漏洩や置き換えのリスクがある。
このトークン置き換え攻撃を防ぐ方法として、Sender-Constrained アクセストークンが上記ドキュメントで提唱されている。Sender-Constrained アクセストークンとはクライアントに紐付けられたアクセストークンのことで、アクセストークンを受け取ったリソースサーバーはアクセストークンの送信者が正当なクライアントであることを確認できるためトークン置き換え攻撃を防ぐことができる。
この Sender-Constrained アクセストークンを実現するためにいくつかの仕様が検討されているが、いずれも Web サーバー間の通信を想定しているためパブリッククライアントを前提としているインプリシットグラントでは実現できない。これがインプリシットが非推奨となった大きな理由である。
感じたこと
OIDC のフローでも id_token をフラグメント部に追加してリダイレクトするという仕様のサービスがあるが、「JavaScript はフラグメントにアクセスできるが Web サーバーにはフラグメント部分は送信されないため、Web サーバー側からのアクセストークンの流出は防ぐことができる」という理由だったのは初めて知れたので良かった。
RFC は非常に膨大な資料ではあるが、章ごとに細かく読んでいくと結構わかりやすい英語で書かれているなと感じた。これなら自分でも読める。
トークンの漏洩と置き換えに細心の注意を払う必要がある。逆にそこさえしっかりとやっていれば他の脆弱性はあまり生まれない?
クライアントクレデンシャルグラント
クライアントと認可サーバー間だけのやりとりでアクセストークンを発行するため 2-legged OAuth とも呼ばれる。
このグラントタイプを利用する際には次の条件を満たす。
認可サーバーが提供するアクセストークンの権限はエンドユーザー単位ではなくアプリ単位
クライアントがコンフィデンシャルクライアント
クライアントが登場しないため、username / password の入力ステップも権限同意のステップもない。また、このグラントタイプではリフレッシュトークンは含むべきではないとされている。
必要なパラメータは 、grant_type として client_credentials を設定する
リソースオーナーパスワードクレデンシャルクライアント
リソースオーナーの username / password がクライアントを通して認可サーバーに送られるグラントタイプ。このグラントタイプを利用できるのはリソースサーバー及び認可サーバーとクライアントの提供元が同じ組織である場合。
必要なパラメータは以下の通りとなる。
grant_type
password を設定する
username
password
scope
クライアントはリソースオーナーの username / password をローカルに保存しないということが重要。クライアントはリクエストが完了したら直ちに username / password を削除するべき。
リフレッシュトークンによるアクセストークン再発行
必要なパラメータは以下の通りとなる。
grant_type
refresh_token を設定する
refresh_token
レスポンスには refresh_token が含まれる場合もあり、その場合は古い refresh_token は使用できなくなる。
認可コードグラント + PKCE
ネイティブアプリにおいて OAuth を利用する場合の脆弱性から守るための仕組み。
しかし、RFC にはブラウザベースのアプリケーションにおいても PKCE を仕様すべきという記載がある。
In recent years, widespread adoption of Cross-Origin Resource Sharing (CORS), which enables exceptions to the same-origin policy, allows browser-based apps to use the OAuth 2.0 Authorization Code flow and make a POST request to exchange the authorization code for an access token at the token endpoint. In this flow, the access token is never exposed in the less-secure front channel. Furthermore, adding PKCE to the flow ensures that even if an authorization code is intercepted, it is unusable by an attacker.
For this reason, and from other lessons learned, the current best practice for browser-based applications is to use the OAuth 2.0 Authorization Code flow with PKCE. There are various architectural patterns for deploying browser-based apps, both with and without a corresponding server-side component, each with their own trade-offs and considerations, discussed further in this document. Additional considerations apply for first-party common-domain apps.
PKCE について
PKCE は Proof Key for Code Exchange の略。
PKCE については以下のページが非常にわかりやすかった。
RFC 7636 は Natsuhiko Sakimura 氏が 1st Author となっていた。
iOS / Android ではともにカスタムスキームの重複が許されており、OS のバージョンによって重複カスタムスキームに対しての挙動が異なるため、アプリの開発者が重複カスタムスキームによる入れ替わりを防ぐのは困難。したがって、入れ替わりを発生しうることを前提に「入れ替わりが発生した場合はアクセストークンを発行させない」という方針を取っているのが PKCE。
PKCE では認可コードフローの各種リクエストにパラメータが追加された。そのパラメータは以下の通り。
許可パラメータ
code_challenge
code_challenge_method
plain もしくは S256 のみが有効の値となる。何も指定しない場合は plain となる
トークンリクエスト
code_verifier
43〜128 文字のランダムな文字列
この仕様により、認可サーバーは code_verifier を持たない悪意のあるクライアントからのトークンリクエストを排除することができるようになった。
PKCE 認可レスポンス
認可コードを生成したあと、認可サーバーはその値と合わせて認可リクエストに含まれている code_challenge の値と code_challenge_method の値を保存する。認可サーバーは保存したこれらの値をその後クライアントから送られてくるトークンリクエストの検証に用いる。
PKCE トークンリクエスト
認可サーバーから認可コードを受け取ったあと、クライアントは認可コードと code_challenge の値から計算した code_verifier を埋め込む必要がある。
PKCE トークンレスポンス
grant_type が authorization_code であり、トークンリクエスト内の認可コードが code_challenge と紐付いている場合に限り、認可サーバーのトークンエンドポイントはトークンリクエスト内に正規の code_verifier が含まれているかどうかを確認する。
grant_type が authorization の場合出会ってもトークンリクエスト内に code_verifier が含まれていない場合はそのリクエストは悪意のあるアプリケーションからのリクエストと認識し、認可サーバーはエラーを返す。
そして認可リクエストに含まれていて認可サーバが DB に保存しておいた code_challenge と code_challenge_method で指定された方法を用いてトークンリクエスト内の code_verifier から計算された値が一致しているかどうかを確認します。
これらのフローに成功してアクセストークンを返す。
PKCE に対する攻撃
PKCE がされうる攻撃は、不正に取得した認可コードに対応する code_verifier を推測しトークンリクエストに送信すること。PKCE では攻撃は2種類想定している。
認可レスポンスのみを観測可能な場合
カスタム URI スキームの乗っ取りなど
オンライン攻撃のみを行うことができる
認可リクエストと認可レスポンスを観測可能な場合
OS ログなどから観測可能
オフライン攻撃も可能
オンライン攻撃
オンライン攻撃は認可コードを取得した悪意あるクライアントが認可コードと紐づく code_verifier を推測しトークンエンドポイントにリクエストをする方法。攻撃するには code_verifier の値を総当りで検証する必要があるが認可コードには有効期限があること(10分以内が推奨)から有効期限内での試行回数はそれほど多くない。
オフライン攻撃
オフライン攻撃は認可リクエストと認可レスポンスの療法を観測できる場合にのみ実行できる。この場合 code_challenge / code_challenge_method を観測できているため code_challenge_method の方式で code_challenge となるような値をトークンエンドポイントへリクエストすることなく手元で探索することが可能となる。
したがって code_challenge_method が plain の場合は簡単に破られてしまうためここまでの攻撃を想定するのであれば code_challenge_method を S256 とするべき。基本的にはここまでの攻撃を想定していなくても S256 の実装を行える場合は code_challenge_method を S256 とすべきである。