Site cover image

根と葉

OAuth2.0

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: ログイン失敗

実装メモ

試しに書いてみたコード