理系学生日記

おまえはいつまで学生気分なのか

忍者TOOLS

OAuth 2.0 authorization code grant 実装ポイント

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 codetokenの二択ですが、Authorization code の場合は code になる
client_id REQUIRED クライアント登録時に発行されるクライアント識別子
redirect_uri OPTIONAL リソースオーナー認証後に user-agent をリダイレクトさせる先
scope OPTIONAL アクセスの要求範囲を示します
state RECOMMENDED CSRF 対策のためクライアント側で設定される
  • response_typecode の場合、その後の 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 bearermac など、アクセストークンのタイプを示す
expires_in RECOMMENDED アクセストークンの有効期限(秒)
refresh_token OPTIONAL アクセストークンの更新に使用する
scope OPTIONAL(※)
  • expires_in については省略可能ですが、
  • scope: クライアントが要求してきたスコープと同じなのであれば省略可能です。違うのであれば、REQUIRED になります。

このトークンレスポンスについては、MediaType が applicaiton/json になります。 また、HTTP ヘッダとして、Cache-Controlno-store に、Pragmano-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_uriclient_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 では定義していませんが、この検証に失敗した場合、リソースサーバはクライアントにエラーを通知しなければなりません。