🚀

OAuth 2.0 認可フローとトークン管理実装指針

2025/01/08に公開

APIでの認証認可を実装するにあたり、範囲が広すぎて情報量が多すぎなので、必須な項目は何かをまとめたい。

前提知識

OAuth 2.0のアクター(by AI)

Resource Owner (リソース所有者)

通常はエンドユーザーであり、自身のリソースにアクセスする権限を持っています。リソース所有者はクライアントに対してリソースへのアクセス権を付与します。

Client (クライアント)

リソース所有者の代理としてリソースにアクセスするアプリケーションやサービスです。クライアントは、認可サーバーからアクセストークンを取得し、それを使用してリソースサーバーにリクエストを送信します。

Authorization Server (認可サーバー)

リソース所有者の認証を行い、クライアントに対してアクセストークンを発行します。認可サーバーは、認証および認可のプロセスを管理します。

Resource Server (リソースサーバー)

保護されたリソース(データやサービス)をホストするサーバーです。リソースサーバーは、クライアントから受け取ったアクセストークンを検証し、そのトークンに基づいてリソースへのアクセスを許可または拒否します。

例示内のサーバー構成

  • 認可エンドポイント: authorization-server.example.com/auth
  • トークンエンドポイント: authorization-server.example.com/token
  • クライアント(WEBホスト): client-app.example.com

事前準備

クライアント登録

  • Client ID: 認可サーバーがクライアントを識別するための一意のID
  • Redirect URI: 認可コードを送信する先のURI
  • Scope: クライアントが要求できるリソースの範囲
  • オプション:
    • Application Type: サーバー側の実装に合わせて設定(WEBやアプリ)
    • Client Name: 表示名
    • Client Secret: クライアントの認証に使用(confidentialの場合必須)
    • Contact Information: コンタクト情報
    • Policy URL: ポリシーURL
    • Client Image: ロゴやアイコン

認可コードフロー (Authorization Code Flow)

認証リクエスト

https://www.rfc-editor.org/rfc/rfc6749.html#section-4.1.1

https://authorization-server.example.com/auth
?response_type=code
&client_id=CLIENT_ID
&redirect_uri=https%3A%2F%2Fclient-app.example.com%2Fcallback%3Fkey%3Dvalue
&scope=read%20write
&state=STATE

このURLに含まれるパラメータ:
response_type=code:認可コードを要求することを示す
client_id=CLIENT_ID:事前に払い出された CLIENT_ID
redirect_uri=https%3A%2F%2Fclient-app.example.com%2Fcallback%3Fkey%3Dvalue:認可コードを送信するリダイレクトURI。必要に応じてクエリパラメータは追加可能
scope=read.function1%20write.function2:要求するスコープをスペース区切りで列挙(スペースはURLエンコードする)
state=STATE:CSRF攻撃を防ぐためのランダムな文字列

参考)スコープの命名ルールについて

[動詞:機能] が良い

read:profile: プロフィール情報の読み取り
write:profile: プロフィール情報の書き込み
delete:account: アカウントの削除

認証レスポンス

https://www.rfc-editor.org/rfc/rfc6749.html#section-4.1.2

302
Location: https://client-app.example.com/callback?key=value
&code=AUTHORIZATION_CODE
&state=STATE

このURLに含まれるパラメータ:
code=AUTHORIZATION_CODE:クライアントがアクセストークンを取得するために使用する認可コード
state=STATE:リクエスト時に送信したstateパラメータがそのまま返されます。CSRF対策として使用される

Error Response

https://www.rfc-editor.org/rfc/rfc6749.html#section-4.1.2.1

302
Location: https://client.example.com/callback?error=access_denied&state=STATE

error=ERROR_CODE: 利用できるエラーコードは以下の通り

  • invalid_request
  • unauthorized_client
  • access_denied
  • unsupported_response_type
  • invalid_scope
  • server_error
  • temporarily_unavailable

error_description=ERROR_DESCRIPTION: 英文
error_uri=ERROR_URI
state=STATE

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

https://www.rfc-editor.org/rfc/rfc6749.html#section-4.1.3

クライアントは認可コードを使用してアクセストークンを要求します。

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

grant_type=authorization_code
&code=AUTHORIZATION_CODE
&redirect_uri=https%3A%2F%2Fclient-app.com%2Fcallback%3Fkey%3Dvalue
&client_id=CLIENT_ID
&client_secret=CLIENT_SECRET

このリクエストに含まれるパラメータ:

grant_type=authorization_code:認可コードフロー
code=AUTHORIZATION_CODE:取得した認可コード
redirect_uri=https%3A%2F%2Fclient-app.com%2Fcallback:認可コードを取得するために使用したリダイレクトURI
client_id=CLIENT_ID:事前に払い出された CLIENT_ID

client_secret=CLIENT_SECRET:事前に払い出された CLIENT_SECRET(オプション)

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

認可サーバーはアクセストークンをクライアントに返します。

{
  "access_token": "ACCESS_TOKEN",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "REFRESH_TOKEN",
  "scope": "read write"
}

このレスポンスに含まれるフィールドの説明:

access_token:JWTアクセストークン
token_type: "Bearer"
expires_in:アクセストークンの有効期間(秒)
refresh_token:新しいアクセストークンを取得するためのリフレッシュトークン
scope:アクセストークンに関連付けられたスコープ

参考 リフレッシュトークン取得のための条件

scope=offline_access

アクセストークンJWT

https://www.rfc-editor.org/rfc/rfc9068.html#name-data-structure

Header:
   {"typ":"at+JWT","alg":"RS256","kid":"RjEwOwOA"}

Claims:
   {
     "iss": "https://authorization-server.example.com/",
     "sub": "5ba552d67",
     "aud": "https://rs.example.com/",
     "exp": 1639528912,
     "iat": 1618354090,
     "jti" : "dbe39bf3a3ba4238a513f51d6e1691c4",
     "client_id": "s6BhdRkqt3",
     "scope": "openid profile reademail"
   }

iss (Issuer): トークンを発行した認可サーバーのURL。
sub (Subject): トークンの対象となるユーザーの一意識別子。
aud (Audience): リソースサーバーは、トークンを受け取った際に、このaudクレームをチェックして、自分がトークンの対象者であるかどうかを確認する。
iat (Issued At): トークンが発行された時刻を示すUNIXタイムスタンプ。
exp (Expiration Time): トークンの有効期限を示すUNIXタイムスタンプ。
scope: トークンに付与されたアクセス権の範囲(例: "read write")。
jti (JWT ID): トークンの一意識別子。

アクセストークン検証

参考)アクセストークンの管理方法とその課題について

通常、アクセストークンはDBなどで管理せずトークン自体に含まれる情報とその署名を検証すること(以下に詳細を記載)で正当性を確認する。
ただし、スコープのリアルタイム検証などを行う場合では、DB管理し無効なアクセストークンであるかを判断することがある。その場合、アクセストークンはある程度長い時間を設定できるが、ユーザーの状態変更によって無効化する処理を行わなければ、管理されているとは言えないため、作りこみが必要となってくる。特に分散システムでは、DBが複数に分散されているため、リアルタイム性の担保やシステム負荷が課題となり、歴史的にアクセストークンが生まれた原因となる課題に立ち向かうことになる。

アクセストークン検証内容

https://www.rfc-editor.org/rfc/rfc9068.html#name-validating-jwt-access-token

  • リソースサーバーは、「typ」ヘッダーの値が「at+jwt」または「application/at+jwt」であることを確認し、他の値を持つトークンを拒否しなければなりません (MUST)。

  • JWTアクセストークンが暗号化されている場合、リソースサーバーは登録時に指定したキーとアルゴリズムを使用してそれを復号化します。登録時に認可サーバーが暗号化すること前提であるにもかかわらず、受信したJWTアクセストークンが暗号化されていない場合、リソースサーバーはそれを拒否すべきです (SHOULD)。

  • 認可サーバーの発行者識別子(通常は検出中に取得される)は、「iss」クレームの値と完全に一致しなければなりません (MUST)。

  • リソースサーバーは、「aud」クレームがリソースサーバー自身の識別子に対応するリソースインジケーター値を含むことを確認しなければなりません (MUST)。

    • もし「aud」に現在のリソースサーバーの有効なオーディエンスとしてのリソースインジケーターが含まれていない場合、JWTアクセストークンを拒否しなければなりません (MUST)。
  • リソースサーバーは、[RFC7515] に従って、JWT「alg」ヘッダーパラメーターに指定されたアルゴリズムを使用して、受信するすべてのJWTアクセストークンの署名を検証しなければなりません (MUST)。

    • リソースサーバーは、「alg」の値が「none」であるJWTを拒否しなければなりません (MUST)。
    • リソースサーバーは、認可サーバーから提供されたキーを使用しなければなりません (MUST)。
  • 現在の時刻は「exp」クレームで表される時刻より前でなければなりません (MUST)。

    • 実装者は、通常数分以内の小さな余裕を持たせて、時計のずれを考慮することができます (MAY)。

参考)IDトークン

認証リクエスト

scope=openid
を含めると取得できる

response_type=code
以外の場合はもう少し複雑・・(ここでは記載しない)

GET /auth HTTP/1.1
Host: authorization-server.example.com
response_type=code
&client_id=CLIENT_ID
&redirect_uri=https%3A%2F%2Fclient-app.example.com%2Fcallback
&scope=openid%20profile%20email
&state=STATE
&nonce=NONCE

scope=openid profile email: IDトークンを要求するために openid スコープを含める
nonce: IDトークン再生攻撃を防ぐためのランダム値

認証レスポンス

302
https://client-app.example.com/callback?code=AUTHORIZATION_CODE&state=STATE

トークンリクエスト

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

grant_type=authorization_code
&code=AUTHORIZATION_CODE
&redirect_uri=https%3A%2F%2Fclient-app.example.com%2Fcallback
&client_id=CLIENT_ID

トークンレスポンスjson

{
  "access_token": "ACCESS_TOKEN",
  "token_type": "Bearer",
  "expires_in": 3600,
  "id_token": "ID_TOKEN"
}

ID_TOKEN JWT

{
  "iss": "https://example.com",
  "sub": "1234567890",
  "aud": "client_id",
  "exp": 1516239022,
  "iat": 1516239022,
  "auth_time": 1516239022,
  "nonce": "nonce_value",
  "name": "John Doe",
  "email": "john.doe@example.com"
}

iss (Issuer): トークンを発行した認可サーバーのURL。
sub (Subject): トークンの対象となるユーザーの一意識別子。
aud (Audience): トークンを受け取る予定のクライアントの識別子(通常はクライアントID)。
exp (Expiration Time): トークンの有効期限を示すタイムスタンプ。
iat (Issued At): トークンが発行された時刻を示すタイムスタンプ。
auth_time: ユーザーが認証された時刻を示すタイムスタンプ。
nonce: リプレイ攻撃を防止するための一意の値。

name: ユーザーの名前(リクエストしたので
email: ユーザーのメールアドレス(リクエストしたので

リフレッシュトークンフロー

リフレッシュトークンを使用して新しいアクセストークンを取得
https://www.rfc-editor.org/rfc/rfc6749.html#section-6

参考 Refreshing an Access TokenとToken Exchangeの違い

Refreshing an Access Token: アクセストークンの有効期限が切れた際に、リフレッシュトークンを使って新しいアクセストークンを取得するプロセス。OAuth 2.0の標準的な機能の一つ。

Token Exchange: ある種類のトークンを別の種類のトークンに交換するプロセス。OAuth 2.0の拡張機能であり、より複雑なユースケースに対応するために設計されています。
https://www.rfc-editor.org/rfc/rfc8693.html

リフレッシュトークンリクエスト

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

grant_type=refresh_token
&refresh_token=tGzv3JOkF0XG5Qx2TlKWIA

grant_type=refresh_token: リフレッシュトークンフローを指定。
refresh_token: 取得済みのリフレッシュトークン。

client_id: クライアントID(Confidentialクライアントの場合)。
client_secret: クライアントシークレット(Confidentialクライアントの場合)。

リフレッシュトークンレスポンス

{
  "access_token": "ACCESS_TOKEN",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "REFRESH_TOKEN"
}

refresh_token: システムによっては新しいリフレッシュトークンが発行される場合もある

参考)リフレッシュトークン利用時の扱い

  1. リフレッシュトークン継続利用 + 有効期間再設定
  2. リフレッシュトークン継続利用 + 有効期間継続
  3. リフレッシュトークン再発行(無効化) + 有効期間再設定
  4. リフレッシュトークン再発行(無効化) + 有効期間継続
  • 利便性重視なら 1.継続利用 + 再設定
  • 一定期間でログインさせるなら 4. 再発行 + 継続

https://auth0.com/docs/secure/tokens/refresh-tokens/refresh-token-rotation
https://auth0.com/docs/secure/tokens/refresh-tokens/configure-refresh-token-rotation

auth0は3が使えない

クライアントクレデンシャルズフロー Client Credentials Flow

バックエンドのシステムなど、ユーザー操作を介したくない場合

クライアント証明書を使った認証 Confidentialクライアント

クライアント証明書の検証は別途必要

認可エンドポイントへのリクエスト

 GET /authorize?response_type=code
  &client_id=YOUR_CLIENT_ID
  &redirect_uri=YOUR_REDIRECT_URI
  &scope=YOUR_SCOPES
  &state=YOUR_STATE_VALUE HTTP/1.1
Host: authorization-server.example.com

トークンエンドポイントへのリクエスト

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

grant_type=client_credentials
&code=AUTHORIZATION_CODE
&redirect_uri=YOUR_REDIRECT_URI
&client_id=YOUR_CLIENT_ID
&client_secret=YOUR_CLIENT_SECRET

Client Credentials Grant

クライアントシークレットでJWTを署名

署名の関係上クライアントシークレット自体が256bit以上になっていてほしい・・。
512bit(HS512)推奨

認可エンドポイントへのリクエスト

不要

トークンエンドポイントへのリクエスト

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

grant_type=client_credentials&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer&client_assertion=SIGNED_JWT

SIGNED_JWT の内容

{
  "iss": "CLIENT_ID",
  "sub": "CLIENT_ID",
  "aud": "https://authorization-server.example.com/token",
  "exp": 1609459200,  // 有効期限(UNIXタイムスタンプ)
  "iat": 1609455600  // 発行時刻(UNIXタイムスタンプ)Option
}

Signature:

HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  YOUR_CLIENT_SECRET
)

Discussion