Token Endpoint実装ガイド
このドキュメントの目的
トークンエンドポイント(トークン発行・検証・失効)の実装を理解することが目標です。
所要時間
⏱️ 約30分
前提知識
- 02. Authorization Flow
- OAuth 2.0基礎知識
Token Endpointとは
OAuth 2.0のトークンエンドポイント(Token Endpoint) は、トークンに関する3つの機能を提供します:
- Token Request: トークン発行(RFC 6749 Section 3.2)
- Token Introspection: トークン検証(RFC 7662)
- Token Revocation: トークン失効(RFC 7009)
RFC準拠: OAuth 2.0 (RFC 6749), Token Introspection (RFC 7662), Token Revocation (RFC 7009), DPoP (RFC 9449)
エンドポイント一覧
# トークン発行(RFC 6749)
POST /{tenant-id}/v1/tokens
# トークンイントロスペクション(RFC 7662)
POST /{tenant-id}/v1/tokens/introspection
# トークン失効(RFC 7009)
POST /{tenant-id}/v1/tokens/revocation
実装: TokenV1Api.java
実装アーキテクチャ全体像
30秒で理解する全体像
HTTPリクエスト
↓
Controller (TokenV1Api) - HTTP処理
↓
EntryService (TokenEntryService) - トランザクシ ョン管理
↓
Core層 (TokenProtocol → TokenRequestHandler)
├─ Validator: 入力形式チェック
├─ クライアント認証(5種類)
├─ Grant Type別Service選択(4種類+拡張)
└─ トークン発行
↓
Repository - データ永続化
主要クラスの責務
| クラス | 役割 | 主な処理 |
|---|---|---|
| TokenV1Api | HTTPエンドポイント | パラメータ受け取り、レスポンス返却 |
| TokenEntryService | オーケストレーション | トランザクション、イベント発行 |
| TokenRequestHandler | トークン発行処理 | Validator、クライアント認証、Service選択 |
| ClientAuthenticationHandler | クライアント認証 | 5種類の認証方式から選択・実行 |
| OAuthTokenCreationServices | Grant Type振り分け | 4種類+拡張からService選択 |
主要ドメインオブジェクト
| オブジェクト | 説明 | 保存場所 | 有効期限 |
|---|---|---|---|
| OAuthToken | Access Token/Refresh Token | DB | 設定による(デフォルト1時間) |
| AuthorizationCodeGrant | Authorization Code | Redis + DB | 5分(使用後即無効) |
| ClientCredentials | クライアント認証情報 | - | - |
1. Token Request(トークン発行)
トークンエンドポイントの最も重要な機能はトークン発行です。Authorization Codeやクライアント認証情報を使ってAccess Token/Refresh Token/ID Tokenを発行します。
1.1 処理フロー全体
実装: TokenRequestHandler.java:82-120
POST /{tenant-id}/v1/tokens
↓
TokenRequestHandler.handle()
↓
┌────────────────────────────────────────┐
│ 1. Validator(入力形式チェック) │
│ - grant_type必須 │
│ - パラメータ形式チェック │
└────────────────────────────────────────┘
↓
┌────────────────────────────────────────┐
│ 2. 設定取得 │
│ - AuthorizationServerConfiguration │
│ - ClientConfiguration │
└────────────────────────────────────────┘
↓
┌────────────────────────────────────────┐
│ 3. クライアント認証 │
│ → ClientAuthenticationHandler │
│ └─ 5種類から選択・実行 │
└────────────────────────────────────────┘
↓
┌────────────────────────────────────────┐
│ 4. Grant Type別Service選択 │
│ → OAuthTokenCreationServices │
│ └─ 4種類+拡張から選択 │
└────────────────────────────────────────┘
↓
┌────────────────────────────────────────┐
│ 5. トークン発行 │
│ → Service.create() │
│ ├─ Authorization Code検証 │
│ ├─ DPoP検証(RFC 9449) │
│ ├─ Access Token生成 │
│ ├─ Refresh Token生成 │
│ ├─ ID Token生成(OIDC) │
│ └─ DB保存 │
└────────────────────────────────────────┘
↓
TokenRequestResponse
1.2 クライアント認証(Client Authentication)
詳細: 10. Client Authentication実装 - 7つの認証方式の完全ガイド
標準5種類 + FAPI拡張2種類
実装: ClientAuthenticators.java:32-40
標準認証方式(5種類)
| 認証方式 | 送信方法 | 用途 | セキュリティレベル |
|---|---|---|---|
| client_secret_basic | Basic認証ヘッダー | 最も一般的 | ⭐⭐ |
| client_secret_post | POSTボディ | レガシー | ⭐ |
| client_secret_jwt | JWT署名(共有鍵) | 高セキュリティ | ⭐⭐⭐ |
| private_key_jwt | JWT署名(秘密鍵) | 最高セキュリティ | ⭐⭐⭐⭐ |
| none | 認証なし | パブリッククライアント(SPA/Mobile+PKCE) | - |
FAPI拡張認証方式(2種類)
実装: libs/idp-server-core-extension-fapi/src/main/java/org/idp/server/core/openid/extension/fapi/
| 認証方式 | 送信方法 | 用途 | セキュリティレベル |
|---|---|---|---|
| tls_client_auth | クライアント証明書(MTLS) | FAPI準拠・金融機関 | ⭐⭐⭐⭐⭐ |
| self_signed_tls_client_auth | 自己署名証明書(MTLS) | FAPI準拠・開発環境 | ⭐⭐⭐⭐ |
client_secret_basic の例
# Authorization ヘッダーにBase64エンコード
Authorization: Basic base64(client_id:client_secret)
# 実際のリクエスト
curl -X POST "http://localhost:8080/${TENANT_ID}/v1/tokens" \
-H "Content-Type: application/x-www-form-urlencoded" \
-H "Authorization: Basic $(echo -n 'my-client:my-secret' | base64)" \
-d "grant_type=authorization_code&code=${CODE}&redirect_uri=${REDIRECT_URI}"
client_secret_post の例
# POSTボディにclient_id/client_secretを含める
curl -X POST "http://localhost:8080/${TENANT_ID}/v1/tokens" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code&code=${CODE}&redirect_uri=${REDIRECT_URI}&client_id=my-client&client_secret=my-secret"
クライアント認証の処理フロー
TokenRequestHandler.handle()
↓
TokenRequestContext作成
├─ clientSecretBasic (Authorizationヘッダーから抽出)
├─ clientCert (MTLSヘッダーから抽出)
└─ parameters (POSTボディ)
↓
ClientAuthenticationHandler.authenticate(context)
↓
┌──────────────────────────────────────────┐
│ ClientAuthenticators.get(認証タイプ) │
├──────────────────────────────────────────┤
│ client_secret_basic │
│ → ClientSecretBasicAuthenticator │
│ └─ Base64デコード → 検証 │
│ │
│ client_secret_post │
│ → ClientSecretPostAuthenticator │
│ └─ POSTボディから抽出 → 検証 │
│ │
│ client_secret_jwt │
│ → ClientSecretJwtAuthenticator │
│ └─ JWT検証(HMAC署名) │
│ │
│ private_key_jwt │
│ → PrivateKeyJwtAuthenticator │
│ └─ JWT検証(RSA/ECDSA署名) │
│ │
│ none │
│ → PublicClientAuthenticator │
│ └─ PKCE必須チェック │
│ │
│ + FAPI拡張(プラグイン) │
│ │
│ tls_client_auth │
│ → TlsClientAuthAuthenticator │
│ └─ クライアント証明書検証(MTLS) │
│ │
│ self_signed_tls_client_auth │
│ → SelfSignedTlsClientAuthAuthenticator│
│ └─ 自己署名証明書検証(MTLS) │
└──────────────────────────────────────────┘
↓
ClientCredentials(認証済み情報)
※ FAPIモジュールがロードされている場合のみMTLS認証が 有効
1.3 Grant Type別のService
4種類の標準Grant Type
実装: OAuthTokenCreationServices.java:43-56
OAuthTokenCreationServices.get(grantType)
↓
┌────────────────────────────────────────┐
│ Grant Typeで振り分け │
├────────────────────────────────────────┤
│ authorization_code │
│ → AuthorizationCodeGrantService │
│ ├─ Validator(code必須等) │
│ ├─ AuthorizationCodeGrant取得 │
│ ├─ Verifier(used, 期限, URI) │
│ ├─ DPoP検証(RFC 9449) │
│ ├─ AccessToken生成 │
│ ├─ RefreshToken生成 │
│ ├─ IdToken生成(OIDC) │
│ └─ AuthorizationGranted保存 │
│ │
│ refresh_token │
│ → RefreshTokenGrantService │
│ ├─ DPoP検証(RFC 9449) │
│ └─ 新しいAccess Token発行 │
│ │
│ password │
│ → ResourceOwnerPasswordCredentials │
│ └─ ユーザー認証・トークン発行 │
│ │
│ client_credentials │
│ → ClientCredentialsGrantService │
│ └─ クライアント権限でトークン発行 │
│ │
│ + 拡張Grant Type(プラグイン) │
│ 例: urn:openid:params:grant-type:ciba │
└────────────────────────────────────────┘
AuthorizationCodeGrantService の処理詳細
実装場所: AuthorizationCodeGrantService.java:127-203
AuthorizationCodeGrantService.create(tokenRequestContext, clientCredentials)
↓
┌─────────────────────────────────────────────┐
│ 1. Validator(入力形式チェック) │
│ 実装: TokenRequestCodeGrantValidator │
├─────────────────────────────────────────────┤
│ - code必須チェック │
└─────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────┐
│ 2. AuthorizationCodeGrant取得 │
├─────────────────────────────────────────────┤
│ authorizationCodeGrantRepository.find() │
│ - 存在しない → invalid_grant エラー │
└─────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────┐
│ 3. AuthorizationRequest取得 │
├─────────────────────────────────────────────┤
│ authorizationRequestRepository.find() │
│ - 元の認可リクエスト情報(scope, nonce等) │
└─────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────┐
│ 4. Verifier(ビジネスルール検証) │
│ 実装: AuthorizationCodeGrantVerifier │
├─────────────────────────────────────────────┤
│ Base Verifier選択(OAuth2 or OIDC) │
│ ├─ 有効期限チェック │
│ ├─ used=false チェック │
│ ├─ redirect_uri完全一致 │
│ ├─ クライアント一致 │
│ └─ PKCE検証(該当時) │
│ │
│ Extension Verifiers(該当時のみ) │
│ └─ プラグインロード拡張検証 │
└────────────────── ───────────────────────────┘
↓
┌─────────────────────────────────────────────┐
│ 5. DPoP検証(RFC 9449) │
├─────────────────────────────────────────────┤
│ DPoPProofVerifier.verifyIfNeeded() │
│ ├─ DPoPヘッダーなし → スキップ(Bearer) │
│ ├─ DPoPヘッダー空 → invalid_dpop_proof │
│ └─ DPoPヘッダーあり → JWT検証 │
│ → DPoPProofVerifiedResult │
└─────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────┐
│ 6. トークン生成 │
├─────────────────────────────────────────────┤
│ AuthorizationGrant抽出 │
│ ├─ user, scope, authentication │
│ ↓ │
│ AccessTokenCreator.create() │
│ ├─ JWT生成(RS256等) │
│ ├─ DPoP時: jkt(JWK Thumbprint)埋め込み │
│ └─ 有効期限設定(デフォルト1時間) │
│ ↓ │
│ RefreshTokenCreator.create() │
│ └─ 有効期限設定(設定による) │
│ ↓ │
│ if (OIDC) │
│ IdTokenCreator.createIdToken() │
│ ├─ nonce含める │
│ ├─ at_hash, c_hash計算 │
│ └─ カスタムクレーム追加 │
│ ↓ │
│ OAuthTokenBuilder.build() │
│ └─ 全トークンを1つのOAuthTokenに統合 │
└─────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────┐
│ 7. AuthorizationGranted登録/更新 │
├─────────────────────────────────────────────┤
│ authorizationGrantedRepository.find() │
│ - 既存の同意情報を取得 │
│ ↓ │
│ exists? → merge() : register() │
│ - 同意情報を記録(次回の自動承認用) │
└─────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────┐
│ 8. クリーンアップ │
├─────────────────────────────────────────────┤
│ oAuthTokenCommandRepository.register() │
│ - OAuthToken保存 │
│ ↓ │
│ authorizationCodeGrantRepository.delete() │
│ - Authorization Code削除(使用済み) │
│ ↓ │
│ authorizationRequestRepository.delete() │
│ - AuthorizationRequest削除(不要) │
└─────────────────────────────────────────────┘
↓
OAuthToken(Access/Refresh/ID Token)
RFC 6749 Section 4.1.3準拠の実装
1.4 各Grant Typeの詳細実装
1.4.1 Authorization Code Grant(最重要)
用途: ユーザー認証後にトークン取得(最も一般的なフロー)
実装: AuthorizationCodeGrantService.java:127-203
リクエスト例:
curl -X POST "http://localhost:8080/${TENANT_ID}/v1/tokens" \
-H "Content-Type: application/x-www-form-urlencoded" \
-H "Authorization: Basic $(echo -n 'client-id:client-secret' | base64)" \
-d "grant_type=authorization_code&code=${CODE}&redirect_uri=${REDIRECT_URI}"
レスポンス:
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"id_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"scope": "openid profile email"
}
処理の詳細: 上記「AuthorizationCodeGrantService の処理詳細」セクション参照
DPoP(Demonstrating Proof of Possession)対応
RFC 9449準拠
DPoPヘッダーを送信すると、Access Tokenがクライアントの鍵ペアにバインドされます(Sender-Constrained Token)。
DPoPリクエスト例:
curl -X POST "http://localhost:8080/${TENANT_ID}/v1/tokens" \
-H "Content-Type: application/x-www-form-urlencoded" \
-H "Authorization: Basic $(echo -n 'client-id:client-secret' | base64)" \
-H "DPoP: eyJhbGciOiJFUzI1NiIsInR5cCI6ImRwb3Arand0IiwiandrIjp7Imt0eSI6...}" \
-d "grant_type=authorization_code&code=${CODE}&redirect_uri=${REDIRECT_URI}"
DPoPレスポンス:
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "DPoP",
"expires_in": 3600,
"refresh_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
}
注意: token_typeが"DPoP"になります(通常は"Bearer")。
DPoP検証フロー(GrantService層):
GrantService.create()
↓
DPoPProofVerifier.verifyIfNeeded(dpopProof, httpMethod, httpUri)
├─ DPoPヘッダーなし(null/空文字列) → スキップ(Bearer Token)
├─ DPoPヘッダー空("") → DPoPProofInvalidException(400)
└─ DPoPヘッダーあり → RFC 9449検証(12項目)
→ DPoPProofVerifiedResult(JWK Thumbprint含む)
↓
AccessTokenCreator.create(..., dpopResult)
└─ DPoP時: Access Tokenにjktクレームと してJWK Thumbprintを埋め込み
重要: DPoP検証はAccessTokenCreatorではなくGrantService層で実行されます。AccessTokenCreatorは検証済み結果(DPoPProofVerifiedResult)のみを受け取ります。
1.4.2 Client Credentials Grant
**用途
サーバー間通信(ユーザーなし)- バックエンドサービスがAPIにアクセス
使用場面:
- マイクロサービス間通信
- バッチ処理
- 管理用スクリプト
リクエスト例
実装: ClientCredentialsGrantService.java:49-88
curl -X POST "http://localhost:8080/${TENANT_ID}/v1/tokens" \
-H "Content-Type: application/x-www-form-urlencoded" \
-H "Authorization: Basic $(echo -n 'client-id:client-secret' | base64)" \
-d "grant_type=client_credentials&scope=api:read api:write"
レスポンス:
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "api:read api:write"
}
注意: Refresh TokenとID Tokenは発行されない(ユーザーコンテキストがないため)
処理フロー
ClientCredentialsGrantService.create()
↓
┌─────────────────────────────────────────────┐
│ 1. Validator │
│ 実装: ClientCredentialsGrantValidator │
├─────────────────────── ──────────────────────┤
│ - scope形式チェック │
└─────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────┐
│ 2. Scope検証 │
│ 実装: ClientCredentialsGrantVerifier │
├─────────────────────────────────────────────┤
│ - クライアント許可scopeでフィルタリング │
│ - 空なら例外 │
└─────────────────────────────────────────────┘
↓