メインコンテンツまでスキップ

UserInfo実装ガイド

このドキュメントの目的

UserInfoエンドポイント(ユーザー情報取得)の実装を理解することが目標です。

所要時間

⏱️ 約20分

前提知識


UserInfoとは

Access Tokenを使ってユーザー情報を取得するエンドポイント

OpenID Connect Core 1.0 Section 5.3準拠


アーキテクチャ全体像

30秒で理解する全体像

HTTPリクエスト(Access Token + DPoP Proof)

Controller (UserinfoV1Api) - HTTP処理
├─ Authorization, x-ssl-cert, DPoP ヘッダー受け取り

EntryService (UserinfoEntryService) - オーケストレーション
├─ Tenant取得
├─ UserinfoRequest作成(clientCert, dpopProof, httpMethod, httpUri)
├─ UserinfoProtocol.request()(Delegate渡し)
└─ イベント発行

Core層 (UserinfoHandler)
├─ Access Token検証(署名・期限・失効チェック)
├─ Subject存在チェック
├─ Delegate.findUser() 呼び出し
├─ Sender-Constrained Token検証(MTLS/DPoP)
├─ Scope別Claims抽出
└─ レスポンス生成

UseCase層 (UserinfoDelegate.findUser())
└─ UserQueryRepository.get()

ユーザー情報返却

主要クラスの責務

クラス役割実装
UserinfoV1ApiControllerHTTPエンドポイントUserinfoV1Api.java
UserinfoEntryServiceUseCaseトランザクション・Delegate実装UserinfoEntryService.java:62-114
UserinfoProtocolCoreAccess Token検証・Claims抽出Core
UserinfoDelegateInterfaceCore層→UseCase層コールバックCore
OAuthTokenCoreAccess Token情報(subject/scope)Core Domain

Delegateパターン

重要: Core層はRepositoryに直接依存しない設計

Core層 (UserinfoProtocol)
↓ Delegate経由
UseCase層 (UserinfoEntryService.findUser())

Repository層 (UserQueryRepository)

理由: Hexagonal Architectureの原則(Core層の独立性維持)


エンドポイント

# Bearerトークンの場合
GET /{tenant-id}/v1/userinfo
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...

# DPoP-boundトークンの場合(RFC 9449)
GET /{tenant-id}/v1/userinfo
Authorization: DPoP eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
DPoP: eyJhbGciOiJFUzI1NiIsInR5cCI6ImRwb3Arand0IiwiandrIjp7...

実装:


フロー

1. [クライアント] Access Token取得済み

2. [クライアント] UserInfoリクエスト
GET /{tenant-id}/v1/userinfo
Authorization: Bearer eyJ...

3. [UserinfoEntryService] リクエスト受信

4. [UserinfoProtocol] Access Token検証

5. [UserQueryRepository] ユーザー情報取得

6. [Scope検証] 返却可能なClaimsをフィルタ

7. [レスポンス] ユーザー情報返却
{
"sub": "user-12345",
"name": "John Doe",
"email": "john@example.com",
"email_verified": true
}

EntryService実装

実装: UserinfoEntryService.java:62

@Transaction(readOnly = true)  // ✅ 読み取り専用
public class UserinfoEntryService implements UserinfoApi, UserinfoDelegate {

UserinfoProtocols userinfoProtocols;
UserQueryRepository userQueryRepository;
TenantQueryRepository tenantQueryRepository;
UserEventPublisher eventPublisher;

@Override
public UserinfoRequestResponse request(
TenantIdentifier tenantIdentifier,
String authorizationHeader,
String clientCert,
List<String> dpopProofHeaders, // ✅ DPoP対応(RFC 9449)
RequestAttributes requestAttributes) {

// 1. Tenant取得
Tenant tenant = tenantQueryRepository.get(tenantIdentifier);

// 2. UserinfoRequest作成
UserinfoRequest userinfoRequest = new UserinfoRequest(tenant, authorizationHeader);
userinfoRequest.setClientCert(clientCert); // MTLS対応
userinfoRequest.setDPoPProofHeaders(dpopProofHeaders); // DPoP対応(DPoPヘッダのリスト)
userinfoRequest.setHttpMethod(requestAttributes.optValueAsString("action", "GET"));
userinfoRequest.setHttpUri(requestAttributes.optValueAsString("request_url", ""));

// 3. Core層に委譲
UserinfoProtocol userinfoProtocol = userinfoProtocols.get(tenant.authorizationProvider());
UserinfoRequestResponse result = userinfoProtocol.request(userinfoRequest, this);

// 4. イベント発行(成功時)
if (result.isOK()) {
eventPublisher.publish(
tenant,
result.oAuthToken(),
DefaultSecurityEventType.userinfo_success,
requestAttributes);
}

return result;
}

// ✅ Delegate実装: Core層からのコールバック
@Override
public User findUser(Tenant tenant, Subject subject) {
UserIdentifier userIdentifier = new UserIdentifier(subject.value());
return userQueryRepository.get(tenant, userIdentifier);
}
}

ポイント:

  • @Transaction(readOnly = true): 読み取り専用トランザクション
  • UserinfoDelegate実装: Core層へのコールバック提供
  • ✅ DPoP Proof: @RequestHeaderで明示的に受け取り(Token Endpointと統一)
  • ✅ イベント発行: userinfo_success

UserinfoDelegate パターン

Core層からのコールバック

public interface UserinfoDelegate {
/**
* Core層がユーザー情報を取得する際に呼び出す
*/
User findUser(Tenant tenant, Subject subject);
}

実装例(Core層):

実装: UserinfoHandler.java:58-90

public class UserinfoHandler {

OAuthTokenQueryRepository oAuthTokenQueryRepository;
AuthorizationServerConfigurationQueryRepository authorizationServerConfigurationQueryRepository;
ClientConfigurationQueryRepository clientConfigurationQueryRepository;
UserinfoCustomIndividualClaimsCreators userinfoCustomIndividualClaimsCreators;

public UserinfoRequestResponse handle(UserinfoRequest request, UserinfoDelegate delegate) {

// 1. Validator: 入力形式チェック
AccessTokenEntity accessTokenEntity = request.toAccessToken();
Tenant tenant = request.tenant();

UserinfoValidator validator = new UserinfoValidator(request);
validator.validate();

// 2. Access Token取得
OAuthToken oAuthToken = oAuthTokenQueryRepository.find(tenant, accessTokenEntity);

if (!oAuthToken.exists()) {
throw new TokenInvalidException("not found token");
}

// 3. 設定取得
AuthorizationServerConfiguration authorizationServerConfiguration =
authorizationServerConfigurationQueryRepository.get(tenant);
ClientConfiguration clientConfiguration =
clientConfigurationQueryRepository.get(tenant, oAuthToken.requestedClientId());

// 4. Subject存在チェック
if (!oAuthToken.hasSubject()) {
throw new TokenInvalidException(
"token does not have a subject, userinfo endpoint requires a user-bound token");
}

// 5. Verifier: トークン検証(DPoP含む)
UserinfoVerifier verifier = new UserinfoVerifier(
oAuthToken,
request.toClientCert(),
request.dpopProof(), // DPoP対応
request.httpMethod(),
request.httpUri());
verifier.verifyToken();

// 6. Delegate経由でユーザー取得して検証
User user = delegate.findUser(tenant, oAuthToken.subject());
verifier.verifyUser(user);

// 7. Claims抽出(Scope別フィルタリング)
UserinfoClaimsCreator claimsCreator =
new UserinfoClaimsCreator(
user,
oAuthToken.authorizationGrant(),
authorizationServerConfiguration,
clientConfiguration,
userinfoCustomIndividualClaimsCreators);
Map<String, Object> claims = claimsCreator.createClaims();

// 8. レスポンス生成
UserinfoResponse userinfoResponse = new UserinfoResponse(user, claims);
return new UserinfoRequestResponse(UserinfoRequestStatus.OK, oAuthToken, userinfoResponse);
}
}

処理の8ステップ:

  1. Validator: 入力形式チェック
  2. Access Token取得(OAuthTokenQueryRepository)
  3. 設定取得(AuthorizationServerConfiguration/ClientConfiguration)
  4. Subject存在チェック(Client Credentialsトークン等を拒否)
  5. Delegate経由でユーザー取得 ← UseCase層への依存注入
  6. Verifier: ビジネスルール検証(トークン有効性・MTLS・DPoP等)
  7. Claims抽出(UserinfoClaimsCreator)
  8. レスポンス生成

Delegateパターンの理由: Core層はRepositoryに直接依存せず、UseCase層経由でデータ取得(Hexagonal Architecture原則)


Scope別の返却Claims

Scope返却されるClaims
openidsub(必須)
profilename, family_name, given_name, middle_name, nickname, picture, website, gender, birthdate, zoneinfo, locale, updated_at
emailemail, email_verified
phonephone_number, phone_number_verified
addressaddress (JSON)

:

Access Token scope: openid profile email

UserInfoレスポンス:
{
"sub": "user-12345",
"name": "John Doe",
"email": "john@example.com",
"email_verified": true
}

verified_claims(身元確認済みクレーム)

身元確認(eKYC)済みユーザーでは、UserInfo は verified_claims:<claim> / verified_claims:verification:<element> 形式のスコープに基づいて verified_claims を返却する。返却には認可サーバー設定 extension.access_token_selective_verified_claims: true が必要。verification の必須要素 trust_framework はスコープ要求の有無に関わらず常に含まれ(verification: {} は非準拠のため出さない)、evidence(生PII)等の任意要素は verified_claims:verification:evidence のように明示要求した時のみ返る(オプトイン)。

Access Token scope: openid verified_claims:given_name

UserInfoレスポンス:
{
"sub": "user-12345",
"verified_claims": {
"verification": { "trust_framework": "eidas" },
"claims": { "given_name": "Taro" }
}
}

上の例では verified_claims:verification:trust_framework を要求していないが、trust_framework は必須要素なので返る。evidence は要求していないので返らない。

要求方式と優先順位

verified_claims の要求方式は2つある。

方式要求方法位置づけ
claims パラメータclaimsuserinfo.verified_claims メンバOIDC4IDA 標準。value/values 制約や §5.7 の選択的省略が適用される
verified_claims:* スコープverified_claims:<claim> 等のスコープidp-server 独自拡張。extension.access_token_selective_verified_claims: true が必要

両経路はどちらもトップレベルの verified_claims キーを生成するため、1リクエストで両方が指定された場合は claims パラメータ(標準)を優先し、verified_claims:* スコープ(独自拡張)側は出力しない。claims パラメータはリクエスト単位で明示的かつ粒度の細かい要求方式であり、こちらを正とする(両方を同時指定する RP は実運用では想定していない)。

構造仕様・設定追従・過去構造からの移行は verified_claims 出力構造の変更(利用者対応) を参照。


Access Token検証

検証項目

UserInfoエンドポイントでは、以下を検証します:

  1. JWT署名検証: Access TokenのJWT署名が正当か
  2. 有効期限チェック: expクレームが期限内か
  3. 失効チェック: トークンが失効(revoke)されていないか
  4. Audience検証: トークンの用途が正しいか
  5. Subject存在チェック: トークンにsubjectが含まれるか(Client Credentialsトークン等を拒否)
  6. Sender-Constrained Token検証:
    • MTLS: クライアント証明書のThumbprintがAccess Tokenのバインディングと一致するか
    • DPoP(RFC 9449): DPoP Proofの署名検証、JWK ThumbprintがAccess Tokenのjktと一致するか
  7. ユーザー存在チェック: ユーザーが存在するか
  8. ユーザーステータスチェック: ユーザーがアクティブな状態か(LOCKED, DISABLED, SUSPENDED, DEACTIVATED, DELETED_PENDING, DELETEDは拒否)

UserinfoVerifierの検証フロー:

UserinfoVerifier.verify()
├─ throwExceptionIfNotFoundToken() // トークン存在・有効期限
├─ throwExceptionIfUnMatchClientCert() // MTLS Sender-Constrained
├─ throwExceptionIfUnMatchDPoPProof() // DPoP Sender-Constrained
├─ throwExceptionIfNotFoundUser() // ユーザー存在
└─ throwExceptionIfInactiveUser() // ユーザーステータス

検証エラー

# 無効なトークン
GET /{tenant-id}/v1/userinfo
Authorization: Bearer invalid-token

→ HTTP 401 Unauthorized
{
"error": "invalid_token",
"error_description": "The access token is invalid"
}
# 期限切れトークン
GET /{tenant-id}/v1/userinfo
Authorization: Bearer eyJ...(期限切れ)

→ HTTP 401 Unauthorized
{
"error": "invalid_token",
"error_description": "The access token has expired"
}
# 失効済みトークン
GET /{tenant-id}/v1/userinfo
Authorization: Bearer eyJ...(失効済み)

→ HTTP 401 Unauthorized
{
"error": "invalid_token",
"error_description": "The access token has been revoked"
}
# ユーザーが見つからない
GET /{tenant-id}/v1/userinfo
Authorization: Bearer eyJ...

→ HTTP 401 Unauthorized
{
"error": "invalid_token",
"error_description": "not found user"
}
# ユーザーがアクティブでない(LOCKED, DISABLED等)
GET /{tenant-id}/v1/userinfo
Authorization: Bearer eyJ...

→ HTTP 401 Unauthorized
{
"error": "invalid_token",
"error_description": "user is not active (id: xxx, status: LOCKED)"
}

Claims抽出ロジック

Scope → Claims マッピング

実装: Core層でScopeに基づいてClaimsをフィルタリング

Access Token:
- subject: "user-12345"
- scopes: ["openid", "profile", "email"]

User(DB):
- sub: "user-12345"
- name: "John Doe"
- email: "john@example.com"
- phone_number: "+81-90-1234-5678" ← phoneスコープなし
- address: {...} ← addressスコープなし

↓ Scope別にフィルタリング

UserInfoレスポンス:
{
"sub": "user-12345", ← openidスコープ
"name": "John Doe", ← profileスコープ
"email": "john@example.com", ← emailスコープ
"email_verified": true ← emailスコープ
}
※ phone_number, address は含まれない(スコープなし)

最小限のレスポンス

openidスコープのみの場合:

{
"sub": "user-12345"
}

subは常に返却されます(OpenID Connect仕様)。


よくあるエラー

エラー1: invalid_token - 無効なAccess Token

原因: 期限切れ・不正なトークン

解決策: 新しいAccess Tokenを取得

エラー2: Claimsが返却されない

原因: Scopeが不足

解決策: トークン取得時に必要なScopeを指定

// ✅ 正しい
scope: 'openid profile email' // profile, emailスコープ追加

エラー3: invalid_dpop_proof - DPoP証明が不正(RFC 9449)

原因: DPoPヘッダーが空、またはDPoP-boundトークンに対するDPoP検証が失敗

# DPoPヘッダーが空
GET /{tenant-id}/v1/userinfo
Authorization: DPoP eyJ...
DPoP:

→ HTTP 400 Bad Request
{
"error": "invalid_dpop_proof",
"error_description": "DPoP header is present but empty"
}

注意: DPoP-boundトークンに対してDPoP Proofが不足・不正な場合は invalid_token(401)が返されます。これはAccess Token自体が無効と見なされるためです。

# DPoP-boundトークンだがDPoPヘッダーなし
GET /{tenant-id}/v1/userinfo
Authorization: DPoP eyJ...

→ HTTP 401 Unauthorized
{
"error": "invalid_token",
"error_description": "access token is DPoP-bound, but DPoP proof header is missing"
}

エラー4: invalid_token - ユーザーがアクティブでない

原因: ユーザーのステータスがLOCKED, DISABLED, SUSPENDED, DEACTIVATED, DELETED_PENDING, DELETEDのいずれか

解決策: 管理者がユーザーステータスをアクティブな状態(INITIALIZED, FEDERATED, REGISTERED, IDENTITY_VERIFIED, IDENTITY_VERIFICATION_REQUIRED)に変更する


次のステップ

✅ UserInfoの実装を理解した!

📖 次に読むべきドキュメント

  1. 06. CIBA Flow実装 - バックチャネル認証

🔗 詳細情報


情報源: UserinfoEntryService.java 最終更新: 2026-03-12