OAuth 2.0 認可フローとトークン管理実装指針
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://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: アカウントの削除
認証レスポンス
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
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
アクセストークンリクエスト
クライアントは認可コードを使用してアクセストークンを要求します。
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
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が複数に分散されているため、リアルタイム性の担保やシステム負荷が課題となり、歴史的にアクセストークンが生まれた原因となる課題に立ち向かうことになる。
アクセストークン検証内容
-
リソースサーバーは、「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
: ユーザーのメールアドレス(リクエストしたので
リフレッシュトークンフロー
リフレッシュトークンを使用して新しいアクセストークンを取得
参考 Refreshing an Access TokenとToken Exchangeの違い
Refreshing an Access Token
: アクセストークンの有効期限が切れた際に、リフレッシュトークンを使って新しいアクセストークンを取得するプロセス。OAuth 2.0の標準的な機能の一つ。
Token Exchange
: ある種類のトークンを別の種類のトークンに交換するプロセス。OAuth 2.0の拡張機能であり、より複雑なユースケースに対応するために設計されています。
リフレッシュトークンリクエスト
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.継続利用 + 再設定
- 一定期間でログインさせるなら 4. 再発行 + 継続
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