OAuth2.0について勉強したので認可コードフローについてまとめてみる
概要
OAuth2.0は、ユーザーが認証情報を直接渡すことなく、第三者アプリケーションに限定的なアクセス権を与えるための認可(権限委譲)フレームワーク。アクセストークンを介して保護対象リソースへアクセスできる。認可コードフローなど複数のフローが存在する。
シーケンス
認可コードフロー(Authorization Code Grant)の基本的なシーケンス
💡アクターの説明
シーケンス図に登場するアクターについての説明と、Google Calendarに保存してある画像を読み込む場合の例
リソース所有者
- リソースへのアクセス権を持つ人
- e.g. Google Calendarに予定を登録しているユーザー
クライアント
リソース所有者に代わってリソースにアクセスしようとするアプリやサービス- e.g. Google Calendarに保存してある予定を参照できるアプリ(Notionなど)
認可サーバー
リソース所有者の同意を確認し、クライアントにアクセストークンを発行するサーバー- e.g. GoogleのOAuth認可サーバー
保護対象リソース
リソース所有者がアクセス権を持つリソース、または、そのリソースへアクセスできるAPIサーバー- e.g. Google Calendar API
sequenceDiagram
participant RO as リソース所有者<br/>(Resource Owner)
participant C as クライアント<br/>(Client)
participant AS as 認可サーバー<br/>(Authrization Server)
participant PR as 保護対象リソース<br/>(Protected Resource)
RO->>C: ログイン開始
C-->>RO: 認可エンドポイントへの<br/>リダイレクトを返却
RO->>AS: 認可エンドポイントにアクセス<br/>(リダイレクト)
AS->>RO: 認証を要求
RO->>AS: 認証を実施<br/>クライアントを許可
AS-->>RO: 認可コードを返却<br/>リダイレクトを行わせる
RO->>C: リダイレクトで<br/>認可コードなどを送信
C->>AS: 自身のクレデンシャルと認可コードを送信
AS->>C: アクセストークンを返す
C->>PR: アクセストークンを併せてリソース取得のリクエスト
PR->>C: 保護対象リソースを返す
認可エンドポイント(/authorize)
クライアントから保護対象リソースへアクセスしようとすると、まずは認可サーバーの認可エンドポイント(/authorize)へリダイレクトされる。ここで、認証を行うことで、クライアントからリソースへのアクセスを許可することができる。このとき、許可するスコープを指定することができ、必要最低限の権限のみをクライアントに付与することが可能。認証に成功すると、認可コードが発行/返却される。
トークンエンドポイント(/token)
認可コードをトークンエンドポイントに送信することで、アクセストークンを取得することができる。以降では、このアクセストークンを使うことで、保護対象リソースにアクセスできるようになる。
セキュリティ対策
stateパラメータ
stateパラメータはCSRF対策として機能する。
例えば下記のように、攻撃者が取得した認可コードを含むcallbackURLを他者に踏ませると、他者のアカウントに攻撃者のOAuthアカウントが紐づいてしまう。
sequenceDiagram
participant A as 攻撃者
participant RO as 被害者
participant C as クライアント
participant AS as 認可サーバー
note over A: 攻撃者が自分のアカウントで正規に認可コードを取得
A->>C: ログイン開始
C-->>A: 認可エンドポイントへの<br/>リダイレクトを返却
A->>AS: 認可エンドポイントにアクセス
AS->>A: 認証を要求
A->>AS: 認証を実施<br/>クライアントを許可
AS-->>A: 認可コードがついたcallbackを<br/>リダイレクトとして返却<br>/callback?code=ATTACKER_CODE
note over A: リダイレクトを行わずcallbackURLを保存<br>攻撃対象者にそのURLを踏ませる
A->>RO: 返却されたcallbackURLへ誘導
RO->>C: GET /callback?code=ATTACKER_CODE
RO->>C: リダイレクトで<br/>認可コードなどを送信
C->>AS: 認可コード: ATTACKER_CODE<br>+ クライアントクレデンシャルで<br>トークンをリクエスト
AS->>C: アクセストークン
C->>C: 被害者のアカウントに攻撃者のOAuthアカウントを紐づける
下記を行うことでCSRF対策として機能する。
- 認可リクエストを受けると、クライアントが
stateパラメータとしてランダムな文字列を付与/保存する - callbackで同じ
stateの値を送信 - クライアントが認可リクエストとcallbackの
stateの値を比較(一致していればOK)
sequenceDiagram
participant A as 攻撃者
participant RO as 被害者
participant C as クライアント
participant AS as 認可サーバー
A->>C: ログイン開始
C->>C: state=ABCを生成<br>攻撃者のブラウザセッションに保存
C-->>A: 認可エンドポイントへの<br/>リダイレクトを返却<br>/authorize?state=ABC
A->>AS: 認可エンドポイントにアクセス
AS->>A: 認証を要求
A->>AS: 認証を実施<br/>クライアントを許可
AS-->>A: 認可コードがついたcallbackを<br/>リダイレクトとして返却<br>/callback?code=ATTACKER_CODE&state=ABC
note over A: リダイレクトを行わずcallbackURLを保存<br>攻撃対象者にそのURLを踏ませる
A->>RO: 返却されたcallbackURLへ誘導
RO->>C: GET /client/callback?code=ATTACKER_CODE&state=ABC
C->>C: 被害者のブラウザセッションからstate=ABCを検索
note over C: セッションにstate=ABCは存在しない
C ->> RO: エラー
PKCE
中間者攻撃を防ぐために使われる。
例えば下記のように、発行された認可コードが横取りされた場合、stateパラメータを使っていても被害を防げない。
sequenceDiagram
participant A as 攻撃者
participant RO as 被害者
participant C as クライアント
participant AS as 認可サーバー
RO->>C: ログイン開始
C-->>RO: リダイレクト
RO->>AS: 認可リクエスト/認証
AS-->>RO: code=VICTIM_CODE
Note over A: 攻撃者が何らかの経路で<br/>VICTIM_CODEを横取り
A->>C: 攻撃者のブラウザでログイン開始
C->>C: state=ATTACKER_STATEを<br/>攻撃者のセッションに保存
C-->>A: リダイレクトURLを返却<br/>/authorize?state=ATTACKER_STATE
Note over A: 攻撃者は認可エンドポイントへのリダイレクトを行わず<br>自分のstateに盗んだVICTIM_CODEを組み合わせ<br>callbackURLにリクエスト
A->>C: /callback?code=VICTIM_CODE&state=ATTACKER_STATE
C->>C: stateを検証
Note over C: ATTACKER_STATEは攻撃者の<br/>セッションと一致するため検証成功
C->>AS: 認可コード: VICTIM_CODE<br>+ クライアントクレデンシャルで<br>トークンをリクエスト
AS-->>C: アクセストークン
C-->>A: 攻撃者のブラウザを<br/>被害者としてログインさせる下記を行うことで対応できる。
クライアント(認可リクエスト)
- ランダムな文字列を生成し、codeVerifierとする
- codeVerifierをハッシュ化し、Base64文字列に変換し、codeChallengeとする
- (stateをキーに、)codeVerifierとcodeChallengeを保存
- codeChallengeとハッシュ化時に使用したアルゴリズム(codeChallengeMethod)を認可リクエストに付与する
認可サーバー(認可エンドポイント)
- codeChallengeとcodeChallengeMethodがリクエストに含まれることを確認
- 上記の値を認可コードと併せて保存
クライアント(トークンリクエスト)
- 返却されたstateに紐づくcodeVerifier/codeChallengeを取得
- codeVerifierをトークンリクエストに付与
認可サーバー(トークンエンドポイント)
- 送られてきた認可コードに紐づくcodeChallenge/codeChallengeMethodを取得
- 送られてきたcodeVerifierとcodeChallengeMethodを使用し、codeChallengeを計算
- 認可リクエスト時に送られてきたCodeChallengeの値を一致するか確認
sequenceDiagram
participant A as 攻撃者
participant RO as 被害者
participant C as クライアント
participant AS as 認可サーバー
RO->>C: ログイン開始
C->>C: code_verifier<br>code_challenge(=base64(sha256(code_verifier)))<br>state生成
C->>C: stateをキーにcode_verifier、code_challengeを保存
C-->>RO: リダイレクト<br>/authorize?state=VISTIM_STATE<br>&code_challenge=...<br>&code_challenge_method=S256
RO->>AS: 認可リクエスト
AS->>AS: code_challengeと<br>code_challenge_methodを<br>保存
AS->>RO: 認証要求
RO->>AS: 認証/認可
AS->>AS: 認可コード=VICTIM_CODE<br>認可コードにcode_challengeなどを紐づけ
Note over A: 攻撃者が何らかの経路で<br/>VICTIM_CODEを横取り
A->>C: ログイン開始
C->>C: code_verifier、code_challenge、state生成
C->>C: stateをキーにcode_verifierを保存
C-->>A: リダイレクト<br/>/authorize?state=ATTACKER_STATE<br>&code_challenge=...<br>&code_challenge_method=S256
Note over A: 攻撃者は認可エンドポイントへのリダイレクトを行わず<br>自分のstateに盗んだVICTIM_CODEを組み合わせ<br>callbackURLにリクエスト
A->>C: /callback?code=VICTIM_CODE&state=ATTACKER_STATE
C->>C: stateを検証
Note over C: ATTACKER_STATEは攻撃者の<br/>セッションと一致するため検証成功
C->>C: ATTACKER_STATEに紐づくcode_verifierなどを取得
C->>AS: /token?code=VICTIM_CODE<br>&code_verifier=ATTACKER_VERIFIER
AS->>AS: 受け取ったcode_verifierから<br>code_challengeを計算<br>=base64(sha256(ATTACKER_VERIFIER))
AS->>AS: VICTIM_CODEに紐づくcode_challengeを取得
AS->>AS: 2つのcode_challengeの値を比較
Note over AS: 不一致
AS->>C: invalid grant
C->>A: ログイン失敗
実装メモ
試しに書いてみたコード
