OAuth 2.0 で Authorization code grant を実装しようとする場合の設計/実装ポイントをまとめてみました。 RFC 6749 を読みながら書いていますが、「普通に作ったらこんなことしねーだろ」というポイントとかはガンガン除外しています。 なので、本気で OAuth 2.0 を理解したり作ったりしようとする人は、RFC 6749 を読みましょう。
共通
あたりまえといえばあたりまえですが、認可サーバは発行するいかなる識別子に関しても、そのサイズに関する情報を提供すべき(SHOULD)なので、そのあたりは仕様書に書いておきましょう。
クライアント登録
クライアント登録は OAuth 2.0 の範疇ではありませんが、OAuth 2.0 の前提はクライアントが認可サーバに登録されていることです。2.4. Unregistered Clients に記述がありますが、未登録のクライアントに対する扱いは OAuth 2.0 の対象外になっています。 クライアント開発者に対する要件(SHALL)として以下が要求されている(2)ため、これを登録しておくのは認可サーバの機能として認識しておくべきでしょう。 - クライアントタイプの指定 - リダイレクト URI の登録 - 認可サーバが要求するその他の情報
クライアントタイプの指定はクライアント開発者への要件であるのと関連して、認可サーバはクライアントタイプを憶測に頼らないという要件が明記されています(SHOULD NOT)。 リダイレクト URI については、Auth 2.0 の仕様書は複数個の登録が可能なのですが、OAuth 2.0 の実装によって違いがあります。
クライアント認証
認可サーバは登録済のクライアントに対し、認可サーバ毎に一意のクライアント識別子を発行します。なお、この識別子の長さについては仕様で定められていませんので、OAuth 2.0 を提供する場合は、仕様書上に書いておく必要があります。
クライアント認証に関しては、どのような方式の認証に対応しても良いことになっていますが、後述する Basic 認証のサポートは必須 (MUST) です。これは TLS で保護されなければなりません(MUST)。なお、クライアント識別子のみでクライアント認証を行ってはいけません (MUST NOT)。 また、パブリッククライアントに関してはクライアント認証をすることは良い(MAY)のですが、クライアントを識別する目的でその認証に信を置いてはいけないとなっています。これは、クライアント認証用のクレデンシャルが漏れ得ることが、パブリッククライアントの定義になっているからですね。 他の認証方式を採る場合は、クライアント識別子と認証方式のマッピングを保持しておかない(MUST)といけないので、Basic 認証以外を採用しようとする場合は、クライアント用のマスタテーブルなんかに認証方式のカラムが必要でしょう。それを元に、認証方式を切り替える実装になります。
クライアント認証でサポートが必須な Basic 認証に関しては、username に相当するものがクライアント識別子、password に相当するものがクライアント用パスワードになります。 他に、HTTP BODY でこれらの情報を送る方法もサポートはして良い(MAY)ですが、それは Basic 認証が使用できないようなクライアントに限られるべき (SHOULD) で、基本的には非推奨です (NOT RECOMMENDED) です。 また、パスワードで認証されるが故に、ブルートフォースアタックへの対策を採ることが必須(MUST)です。
認可エンドポイント
リソースオーナーに対して認可を得るために認可サーバ上に設けるエンドポイントです。もちろん TLS で保護されます(MUST)。
認可サーバは、最初にリソースオーナーの認証を行わなければなりません。が、もちろん OAuth は認可の仕様なので、認証仕様の記載はありません。認可サーバ上で Form 認証を行ったり、OpenID Connect を利用するなんてことが多いんじゃないかと思います。
このエンドポイントは GET
のアクセスが必須 (MUST) で、同様に POST
のサポートはしても良い (MAY) とされています。
パラメータの値が無い場合はパラメータ自体が無いものと捉えなければならず(MUST)、認識できないようなパラメータは無視する必要があります(MUST)。また、リクエスト/レスポンスのパラメータは重複してはなりません(MUST)。
認可リクエスト
認可リクエストは以下のような形式で、GET のサポートは MUST です。
パラメータ | REQUIRED/OPTIONAL | 備考 |
---|---|---|
response_type |
REQUIRED | code か token の二択ですが、Authorization code の場合は code になる |
client_id |
REQUIRED | クライアント登録時に発行されるクライアント識別子 |
redirect_uri |
OPTIONAL | リソースオーナー認証後に user-agent をリダイレクトさせる先 |
scope |
OPTIONAL | アクセスの要求範囲を示します |
state |
RECOMMENDED | CSRF 対策のためクライアント側で設定される |
response_type
がcode
の場合、その後の OAuth の認可フローが Authorization code grant、token
の場合はImpolicit grant
に進むことになります。- このパラメータが送られてこなかった場合、認可サーバはエラー応答することが求められます(MUST)。
redirect_uri
については OPTIONAL となっているものの、仕様上、認可サーバはクライアントに要求すべき (SHOULD) となっています。そして、クライアントが URI を指定した場合、その値がクライアント登録時に登録された値と同じであることを指定しなければなりません。これはオープンリダイレクタ対策だと思います。- 検証に失敗した場合、エラー応答を行うべき(SHOULD)とされ、リダイレクトさせてなりません(MUST NOT)。
scope
が指定された場合、認可サーバは受け取った情報をそのままレスポンスとしてクライアントに送り返ります。リダイレクト先の CSRF 対策はクライアントの義務 (MUST) なのですが、それを達成するための推奨 (SHOULD) がこの state の利用になります。- クライアントは、この
state
に他者に類推できず(MUST)、クライアント・User-Agent 間でしかやりとりされない形で保持されなければなりません(MUST)。
- クライアントは、この
以下のようなかんじ。
GET /authorize?response_type=code&client_id=s6BhdRkqt3&state=xyz&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb HTTP/1.1 Host: server.example.com
scope は、空白で区切られた case-sensitive な文字列で構成されます。リクエストパラメータで送られてきたこの scope
を無視するか否かは認可サーバ側に判断権があります。
ただし、要求された scope と別の scope を適用することを決定した場合、その結果を認可レスポンスとして返却するのは必須です (MUST)。
また、scope が要求されなかった場合、認可サーバはデフォルトの scope を採用して処理を進めるか、エラーとするかを決めなければなりません。
認可サーバは、このリクエストを受信した後、必須パラメータの存在とその値の妥当性を判定し、リソースオーナーを認証・認可した後で認可レスポンスを返却します。
認可レスポンス
リソースオーナーの認証後、認可サーバはユーザエージェントを、事前のクライアント登録で登録されていたリダイレクト URI にリダイレクトさせます。 リダイレクト先が TLS をサポートするのは必須ではありませんが SHOULD となっており、リダイレクト先が TLS でない場合は認可サーバ側でリソースオーナーに警告すべき(SHOULD)となっています。まぁリダイレクト先がスマフォのカスタム URI になっているような場合はどうしようもないですが、まさにこのケースは PKCE で保護しないといけない部分ですね。
パラメータ | REQUIRED/OPTIONAL | 備考 |
---|---|---|
code |
REQUIRED | 認可サーバによって生成された認可コード |
state |
REQUIRED | クライアントから要求された state の値 |
HTTP/1.1 302 Found Location: https://client.example.com/cb?code=SplxlOBeZQQYbYS6WxSbIA&state=xyz
認可コードは漏洩時のリスクを軽減するために有効期限を短かくすることが求められて(MUST)おり、最大でも 10 分が推奨(RECOMMENDED)されています。また、クライアントは同じ認可コードを 2 度以上使用してはならず(MUST)、 認可サーバはそういう重複利用は拒否しないといけません(MUST)。これを実現するためには、認可コードの有効フラグみたいなものを保持するなどの実装になりそうです。
一方、認可する際に何かしらのエラーがあった場合は、クライアントに対してエラーを返却します。
パラメータ | REQUIRED/OPTIONAL | 備考 |
---|---|---|
error |
REQUIRED | 下記参照 |
error_description |
OPTIONAL | クライアントに対する追加情報 |
error_uri |
OPTIONAL | クライアント開発者に対し、エラー情報を通知するページ |
state |
REQUIRED | クライアントから要求された state の値 |
HTTP/1.1 302 Found Location: https://client.example.com/cb?error=access_denied&state=xyz
error
については、いくつかの値になります。もちろん、リダイレクト URI が不正の場合は、リダイレクトさせてはいけません。。。
invalid_request
: パラメータ不足、パラメータ不正unauthorized_client
: クライアントにリクエストの際に指定したresponse_type
が許可されていない場合access_denied
: リソースオーナーか認可サーバによる認可拒否unsupported_response_type
: 認可サーバーがリクエストの際に指定したresponse_type
による認可コード取得をサポートしていない場合invalid_scope
: 要求されたスコープの不正server_error
: 未知のエラーtemporarily_unavailable
: 過負荷、メンテナンス中
トークンエンドポイント
トークンエンドポイントは、Authorization code grant における認可コードとアクセストークンの交換、および、リフレッシュトークンによるアクセストークンの更新に使用される、認可サーバ上のエンドポイントです。
もちろんこちらも認可エンドポイントと同様、TLS で保護されることが必須 (MUST) です。認可エンドポイントが GET
へのサポートが必須だったのに対し、こちらは POST
が必須です(MUST)。
パラメータの値が無い場合はパラメータ自体が無いものと捉えなければならず(MUST)、認識できないようなパラメータは無視する必要があります(MUST)。また、リクエスト/レスポンスのパラメータは重複してはなりません(MUST)。
なお、認可サーバは、
- クレデンシャルを発行したクライアントに対しては、クライアント認証を行わなければなりません。
- そのクライアントに対して発行された認可コードであることを検証しないといけません
- 認可リクエストと同じ
redirect_uri
が要求されていることを検証しないといけません
トークンリクエスト
パラメータ | REQUIRED/OPTIONAL | 備考 |
---|---|---|
grant_type |
REQUIRED | Authroization code grant の場合 authorization_code |
code |
REQUIRED | 認可サーバから受け取った認可コード |
redirect_uri |
REQUIRED(※1) | 認可リクエストで送信した redirect_uri の値 |
client_id |
REQUIRED(※2) |
- ※1: 認可リクエストで送信していた場合
- ※2: 認可リクエストでクライアントが認証されていなかった場合
POST /token HTTP/1.1 Host: server.example.com Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW Content-Type: application/x-www-form-urlencoded grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb
トークンレスポンス
これに対して、認可サーバが返却するレスポンスはこんなかんじになります。
パラメータ | REQUIRED/OPTIONAL | 備考 |
---|---|---|
access_token |
REQUIRED | 認可サーバが発行したアクセストークン |
token_type |
REQUIRED | bearer や mac など、アクセストークンのタイプを示す |
expires_in |
RECOMMENDED | アクセストークンの有効期限(秒) |
refresh_token |
OPTIONAL | アクセストークンの更新に使用する |
scope |
OPTIONAL(※) |
expires_in
については省略可能ですが、scope
: クライアントが要求してきたスコープと同じなのであれば省略可能です。違うのであれば、REQUIRED になります。
このトークンレスポンスについては、MediaType が applicaiton/json
になります。
また、HTTP ヘッダとして、Cache-Control
を no-store
に、Pragma
を no-cache
にするのは必須(MUST) となっています。
HTTP/1.1 200 OK Content-Type: application/json;charset=UTF-8 Cache-Control: no-store Pragma: no-cache { "access_token":"2YotnFZFEjr1zCsicMWpAA", "token_type":"example", "expires_in":3600, "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA", "example_parameter":"example_value" }
エラー時のレスポンスについては、ステータスコードは 400 (Bad Request
) とし、
一方、認可する際に何かしらのエラーがあった場合は、クライアントに対してエラーを返却します。
パラメータ | REQUIRED/OPTIONAL | 備考 |
---|---|---|
error |
REQUIRED | 下記参照 |
error_description |
OPTIONAL | クライアントに対する追加情報 |
error_uri |
OPTIONAL | クライアント開発者に対し、エラー情報を通知するページ |
state |
REQUIRED | クライアントから要求された state の値 |
error
については、いくつかの値になります。もちろん、リダイレクト URI が不正の場合は、リダイレクトさせてはいけません。。。
invalid_request
: パラメータ不足、パラメータ不正invalid_client
: クライアント認証の失敗invalid_grant
: リフレッシュトークン不正や有効期限切れ、認可リクエストに含まれていたrequest_uri
やclient_id
と一致しないなど。unauthorized_client
: クライアントにリクエストの際に指定したresponse_type
が許可されていない場合invalid_scope
: 要求されたスコープの不正
invalid_client
については、トークンリクエストヘッダに Authentication
ヘッダがない場合は HTTP ステータスコードは 401 でも良く(MAY)、ある場合は 401 とし、レスポンスに WWW-AUthenticate
ヘッダを含むことが MUST です。
アクセストークンの更新
リフレッシュトークンによるアクセストークンの更新についても、トークンエンドポイントで提供されます。
これ、Content-Type
も HTTP メソッドも同じなんですね。実装しづらい。
パラメータ | REQUIRED/OPTIONAL | 備考 |
---|---|---|
grant_type |
REQUIRED | refresh_token |
refresh_token |
REQUIRED | リフレッシュトークン |
scope |
OPTIONAL |
もちろん、アクセストークンの更新の際も、(一度クライアントにクレデンシャルを発行しているのであれば)クライアントの認証は MUST です。 また、リフレッシュトークンがそのクライアントに発行されたものであること、および、リフレッシュトークンの検証も実施しなければなりません (MUST)。
なお、このタイミングで、リフレッシュトークンごと新しく発行しても良い(MAY)ことになっています。 この場合、クライアントは古いリフレッシュトークンを無効化しないといけません(MUST)。また、認可サーバは古いリフレッシュトークンを無効化して良い(MAY)ことになっています。
リソースへのアクセス
クライアントがリソースにアクセスしようとする際、リソースサーバはアクセストークンを検証し、スコープで認められたアクセスであること、および、有効期限が切れていないことを確認しなければなりません(MUST)。 アクセストークンに何を用いるかは RFC6749 では定義していませんが、この検証に失敗した場合、リソースサーバはクライアントにエラーを通知しなければなりません。