💪

OAuth 2.0 認可コードフロー+PKCE をシーケンス図で理解する

2020/12/28に公開
2

はじめに

OAuth 2.0 のフローをシーケンス図で説明したWeb上の記事や書籍を何度か見かけたことがありますが、

  • フローの概要に加え、クライアントや認可サーバー側でどういったパラメータを元に何を検証しているのかも一連のフローとして理解したかった
  • RFC 7636 Proof Key for Code Exchange (PKCE) も含めた流れを整理したかった

というモチベーションがあり、自分でシーケンス図を書きながら流れを整理してみた、という趣旨です。

記事の前提や注意事項

  • OAuth 2.0 の各種フローのうち、認可コードフローのみ取り上げています
  • 認可コードフローとはなにか、PKCE とはなにかという説明は割愛しています
  • 文中でたびたび RFC 6749 を参照していますが、リンク先および引用文は OpenID Foundation Japan による翻訳版(https://openid-foundation-japan.github.io/rfc6749.ja.html)になっています
  • リクエスト・レスポンス例では、クライアントおよび認可サーバーのエンドポイントは以下のURLの想定で書いています
- クライアント: web アプリ https://client.example.com
  - リダイレクト URI: https://client.example.com/cb
- 認可サーバー:
  - 認可エンドポイント: https://server.example.com/authorize
  - トークンエンドポイント: https://server.example.com/token

1) 認可コードフロー

はじめに、PKCE を含まない通常の認可コードフローについて見ていきます。
シーケンス図はこちらです。

(画像が小さい場合は https://github.com/zaki-yama/zenn.dev/blob/main/articles/images/oauth2-authorization-code-grant-and-pkce/authorization-code-flow.png を参照してください)

以下、この図の説明です。
フロー開始から認可コードを取得するまでの流れを見ていきます。

0. クライアント登録

フローを開始する前に、クライアントを認可サーバーに登録する必要があります。
クライアント登録時に保存する情報として、 RFC 6749 「2. クライアント登録」 には以下のように記載されています。

クライアントを登録する場合, クライアント開発者は以下を満たすものとする (SHALL).

  • Section 2.1 で説明されているようなクライアントタイプを指定し,
  • Section 3.1.2 で説明されているようなリダイレクト URI を提供し,
  • 認可サーバーが要求するその他の情報 (例えばアプリケーション名, Web サイト, 説明, ロゴイメージ, 利用規則など) を提供する.

1. フロー開始 〜 2. state の生成

フローを開始する際、クライアントは state パラメータというランダムな文字列を生成します。
RFC 6749 「4.1.1. 認可リクエスト」 に以下の記載があります。

state
推奨 (RECOMMENDED). リクエストとコールバックの間で状態を維持するために使用するランダムな値. 認可サーバーはリダイレクトによってクライアントに処理を戻す際にこの値を付与する.

認可レスポンスを受けとった際、レスポンスに含まれる値と突き合わせるため、生成した文字列はクライアント内部で保持しておきます。

4. 認可リクエスト

クライアントはリダイレクトを利用して、リソースオーナーを認可エンドポイントに導きます。
リクエストは以下のような形です。

GET /authorize
  ?response_type=code
  &client_id=<client_id>
  &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb
  &scope=read
  &state=xyz
Host: server.example.com

送信するパラメータ、および各パラメータが必須か任意かについては、RFC 6749 「4.1.1. 認可リクエスト」 によれば以下の通りです。

  • response_type: 必須(REQUIRED)。値は必ず code にしなければならない(MUST)
  • client_id: 必須(REQUIRED)
  • redirect_uri: 任意(OPTIONAL)
  • scope: 任意(OPTIONAL)
  • state: 推奨(RECOMMENDED)

5. パラメータの検証

認可エンドポイントへのリクエストを受け取った認可サーバー側では、はじめに送られてきたパラメータが正しいか検証します。
具体的には、 state を除く各種パラメータについて

  • response_type: 値が code
  • client_id: 登録済みのクライアントの中で、ID が一致するものがあるか
  • redirect_uri: 登録済みの内容と一致するか
  • scope: (登録されていれば) 登録済みの内容と一致するか

であることを検証します。

6. 認証画面表示 〜 9. 認可

認可サーバーはリソースオーナーに対し、クライアントへ各種リソースへの認可を行うかどうか確認します。
一般的には、リソースオーナーが未ログインであればログイン画面を表示してユーザー名・パスワードによる認証を行った後、「◯◯(クライアント)へ以下のリソースへのアクセスを許可しますか?」という画面を表示することで許可/拒否を尋ねます。

なお、RFC 6749 「3.1. 認可エンドポイント」 には

認可サーバーが用いるリソースオーナーの認証方法 (ユーザー名とパスワードによるログイン, セッションクッキー) については, 本仕様の定めるところではない.

と記載されています。

10. 認可コード発行

リソースオーナーの許可が得られた後、認可サーバーは認可コードを発行します。
発行した認可コードはこの後のアクセストークンリクエストの際にリクエストパラメータと突き合わせるため、 client_id とひもづけて保存しておきます。

参考: RFC 6749 「4.1.2. 認可レスポンス」

認可コードはクライアント識別子とリダイレクト URI に紐づく.

また、認可コードの有効期限については、同じ項に

漏洩のリスクを軽減するため, 認可コードは発行されてから短期間で無効にしなければならない (MUST). 認可コードの有効期限は最大でも 10 分を推奨する (RECOMMENDED).

という記載があります。

11. 認可レスポンス

認可コードを発行した後、認可サーバーはリクエスト時に送られてきた redirect_uri にリソースオーナーをリダイレクトさせます。
このとき、リダイレクト URI には 2 つのクエリパラメータが付与されています。

  • code: 直前に発行した認可コード
  • (optional) state: リクエスト時にクライアントから送られてきた場合のみ。受け取った値をそのまま返す

以下は認可レスポンスの例です。

HTTP/1.1 302 Found
Location: https://client.example.com/cb?code=<認可コード>&state=xyz

12. state の検証

Step 10 で発行した state の値と、認可レスポンスで返された state の値が一致することを検証します。
一致しなかった場合はエラーとします。

13. アクセストークンリクエスト

先ほど取得した認可コードを付与して、トークンエンドポイントに POST リクエストを送信します。
リクエストは以下のような形式です。

POST /token HTTP/1.1
  Host: server.example.com
  Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
  Content-Type: application/x-www-form-urlencoded

  grant_type=authorization_code
  &code=<認可コード>
  &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb

送信するパラメータ、および各パラメータが必須か任意かについては、RFC6749 「4.1.3. アクセストークンリクエスト」 によれば以下の通りです。

  • grant_type: 必須(REQUIRED)。値は必ず authorization_code にしなければならない(MUST)
  • code: 必須(REQUIRED)。認可コード
  • redirect_uri: 認可リクエスト時に送信した場合は必須(REQUIRED)。認可リクエスト時と同じ値でなければならない(MUST)
  • client_id: クライアント認証を行わない場合は必須(REQUIRED)

また、コンフィデンシャルクライアントの場合、client_idclient_secretを Basic 認証を使って送信します。
具体的には、client_id, client_secret それぞれを URL エンコードし、それらを : で結合した文字列を Base64 エンコードしたものを Authorization ヘッダーに設定します。

なお RFC 6749 「2.3.1 クライアントパスワード」 によれば、Basic 認証スキームの代わりに 2 つのパラメータを直接リクエストボディーに含める方法もありますが、非推奨(NOT RECOMMENDED)とされています。

パブリッククライアントの場合、client_id のみをリクエストボディーに設定して送信することになります。

14. & 15. パラメータの検証

認可サーバー側でアクセストークンリクエストに含まれる各種パラメータを検証します。
検証する内容については以下のとおりです。

  • grant_type: 値が authorization_code
  • Authorization ヘッダーをデコードして client_idclient_secret を取り出し、登録済みのクライアント情報に一致するものが存在することを確認する
  • code と一致する保存済みの認可コードが存在するか確認する
  • さらに、認可コードにひもづいて保存されている内容から以下を確認する
    • 有効期限が切れてないこと
    • client_idredirect_uri が保存済みの内容と一致すること

参考: RFC 6749 「4.1.3. アクセストークンリクエスト」

  • クライアントがコンフィデンシャルクライアントの場合は, 認可コードが確かに認証されたクライアントに対して発行されたことを確認する. クライアントがパブリッククライアントの場合は, 認可コードが確かに指定された client_id に紐づくクライアントに対して発行されていることを確認する.
  • 認可コードが正当であることを検証する.
  • Section 4.1.1 で述べた認可リクエスト時に redirect_uri パラメータが含まれていた場合, ここでも redirect_uri が存在し認可リクエスト時と同じ値であることを確認する.

16. 認可コードの削除

Step 15 のパラメータの検証で、保存済みの認可コードが存在することが確認できたら、同じ認可コードを再利用できないよう削除します。

[RFC 6749 「10.5. 認可コード」に以下の記載があります。

認可コードの有効期間は短く, かつ一度限りしか利用されてはならない (MUST). もし認可サーバーが単一の認可コードをアクセストークンへ交換しようとする複数の試行を検出したならば, 認可サーバーはその認可コードに基づき既に付与されたすべてのアクセストークンを無効化することを試みるべきである (SHOULD).

17. アクセストークン発行 (& 18, リフレッシュトークン発行)

送られてきたパラメータの正当であることを検証した後、認可サーバーはアクセストークンを発行します。
また、任意でリフレッシュトークンを発行します。

アクセストークン、リフレッシュトークンとひもづけて登録するその他の情報としては、以下があります。

  • client_id
  • 有効期限
  • scope: 認可コードにひもづけて保存していた値
  • user_id: リソースオーナーを識別するための識別子

scope を保存する理由は、この後クライアントが取得したアクセストークンを使ってリソースにアクセスする際、アクセストークンに与えられたスコープがリソースにアクセスするだけの条件を満たしているかをチェックする必要があるためです。

また、リソースオーナーの識別子については RFC 6749 に記載があるわけではありませんが、「管理画面などから自分が許可した OAuth クライアントの一覧を表示し、必要であれば手動で revoke する」ことができるようにしてあるのが一般的ですので、「誰が発行したアクセストークンか」という情報もひもづけて保存すると考えます。

19. アクセストークンレスポンス

発行したアクセストークンをクライアントに送信します。
レスポンスに含めるパラメーターについては RFC 6749 「5.1. 成功レスポンス」 に記載があります。

  • access_token: 必須(REQUIRED)
  • token_type: 必須(REQUIRED)。 トークンのタイプ。Bearer の場合がほとんど。
  • expires_in: 推奨(RECOMMENDED)。アクセストークンの有効期限を表す秒数 (例: 3600 ならば1時間後に期限切れ)
  • refresh_token: 任意(OPTIONAL)
  • scope: アクセストークンのスコープ。クライアントから全く同一のスコープが要求された場合は任意 (OPTIONAL)。その他は必須 (REQUIRED)

以下はレスポンスの例です。

HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache

{
  "access_token":"2YotnFZFEjr1zCsicMWpAA",
  "token_type":"Bearer",
  "expires_in":3600,
  "scope": "read",
  "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA"
}

2) 認可コードフロー + PKCE

ここまで見てきた認可コードフローにPKCEも含めたシーケンス図がこちらです。

(画像が小さい場合は https://github.com/zaki-yama/zenn.dev/blob/main/articles/images/oauth2-authorization-code-grant-and-pkce/authorization-code-flow-pkce.png を参照してください)

赤字部分が PKCE によって追加された処理です。それ以外はここまで説明した内容と変わらないため、以下ではこの赤字部分のみ説明します。

3. code_verifier の生成

フロー開始時、クライアントは code_verifier と呼ばれるランダムな文字列を生成します。

4. code_verifier から code_challenge の算出

生成した code_verifier から、決められたメソッドでハッシュ値を計算し、 code_challenge とします。
code_challenge 算出の際のメソッドは code_challenge_method パラメーターと呼ばれ、 plain または S256 のいずれかの値を取ります。
それぞれの値に対する code_challenge の算出方法は以下のとおりです。

  • plain: code_challengecode_verifier の値そのもの
  • S256: code_verifier の SHA-256 ハッシュ値を計算し、Base64URL エンコードした値

ただし、RFC 7636 「4.2. Client Creates the Code Challenge」 にも

If the client is capable of using "S256", it MUST use "S256", as
"S256" is Mandatory To Implement (MTI) on the server. Clients are
permitted to use "plain" only if they cannot support "S256" for some
technical reason and know via out-of-band configuration that the
server supports "plain".

という記載があり、実際には特別な事情がない限り code_challenge_methodS256 一択のようです。

6. 認可リクエスト

認可リクエスト時、通常の認可コードフローのパラメータに加え、code_challengecode_challenge_method パラメーターも認可サーバーに送信します。

GET /authorize
  ?response_type=code
  &client_id=<client_id>
  &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb
  &scope=read
  &state=xyz
  &code_challenge=<計算したハッシュ値>
  &code_challenge_method=S256
Host: server.example.com

13. code_challenge, code_challenge_method を認可コードにひもづけて保存

認可コードを発行して保存する際、リクエストパラメータに送られてきた code_challengecode_challenge_method も認可コードにひもづけて保存します。
これは、この後のアクセストークンリクエスト時に検証のために使用します。

16. アクセストークンリクエスト

認可コードを受け取ったクライアントは、通常の認可コードフローと同じようにアクセストークンリクエストを送信します。
その際、今度は code_verifier パラメータを追加して送信します。

POST /token HTTP/1.1
  Host: server.example.com
  Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
  Content-Type: application/x-www-form-urlencoded

  grant_type=authorization_code
  &code=<認可コード>
  &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb
  &code_verifier=<Step 3 で生成したランダムな文字列>

20. code_verifier の検証

アクセストークンリクエストを受け取った認可サーバーは、以下の手順に従って code_verifier が正しいか検証します。

  1. 保存済みの認可コードにひもづく code_challengecode_challenge_method を取り出す
  2. リクエストパラメータで受け取った code_verifier と、保存されていた code_challenge_method に従ってハッシュ値を計算する
  3. 計算して得られたハッシュ値と、保存されていた code_challenge が一致することを確認する

これにより、認可リクエストを送ってきたクライアントとアクセストークンリクエストを送ってきたクライアントが同一であることを担保できます。

参考リンク

Discussion

ritouritou

また、リソースオーナーの識別子については RFC 6749 に記載があるわけではありませんが、「管理画面などから自分が許可した OAuth クライアントの一覧を表示し、必要であれば手動で revoke する」ことができるようにしてあるのが一般的ですので、「誰が発行したアクセストークンか」という情報もひもづけて保存すると考えます。

Revokeもそうですが、まずはリソースサーバーがリクエストを受けて "誰のリソースに対するリクエストか" を知る必要があるので、リソースオーナーの情報を含むのが一般的です。

一連の流れを理解してさらに理解を深めたいのであれば、

  • リソースサーバーへのアクセスに必要な情報 : アクセストークンの要件
  • アクセストークンに必要な情報 : 認可コード、リフレッシュトークンの要件
  • リフレッシュトークンに必要な情報 : 認可コードの要件
  • 認可コードに必要な情報 : 認可エンドポイントの要件

と言うように、リソースサーバーへのリクエストの部分から必要な情報を逆算して整理してみても良いかもしれません。

あと、クライアント側でセッションに紐付けて保持しておく値を図にしておくとstate, PKCEの説明がわかりやすくなりそうですね。

Shingo YamazakiShingo Yamazaki

コメントありがとうございます。
内容を拝見し、私の理解がアクセストークンを取得するところまでに終始しており、
その後リソースサーバーでアクセストークンがどう扱われるかについてまだ理解が不十分だと感じました。

アドバイスいただいたように、リソースサーバーへのアクセスに必要な情報は?というところを引き続き調べてみて、そこからまた記載した内容を見直してみようと思います。
ありがとうございます。