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

CIBA Flow実装ガイド

このドキュメントの目的

**CIBA(Client Initiated Backchannel Authentication)**の実装を理解することが目標です。

所要時間

⏱️ 約30分

前提知識


CIBAとは

バックチャネル認証 - ユーザーがログインデバイスとは別のデバイス(スマホ等)で認証を承認する方式。

OpenID Connect CIBA Core 1.0準拠


通常のAuthorization Code Flowとの違い

項目Authorization Code FlowCIBA
認証デバイスブラウザ(同じデバイス)スマホ等(別デバイス)
リダイレクトあり(ブラウザリダイレクト)なし
ユーザー操作ブラウザでログインスマホでプッシュ通知承認
ポーリングなしあり(またはPing/Push)

アーキテクチャ全体像

30秒で理解する全体像

HTTPリクエスト

Controller (CibaV1Api) - HTTP処理

EntryService (CibaFlowEntryService) - オーケストレーション
├─ Tenant取得
├─ CibaRequest作成
├─ CibaProtocol.request()(UserHintResolver使用)
├─ AuthenticationTransaction作成
├─ デフォルト認証実行(プッシュ通知)
├─ イベント発行
└─ auth_req_id返却

Core層 (CibaProtocol)
├─ Validator: 入力形式チェック
├─ UserHintResolver: login_hint → User解決
├─ Verifier: ビジネスルール検証
└─ BackchannelAuthenticationRequest生成

プッシュ通知 → ユーザー承認 → トークン発行(Token Flow)

主要クラスの責務

クラス役割実装
CibaV1ApiControllerHTTPエンドポイントCibaV1Api.java
CibaFlowEntryServiceUseCaseトランザクション・オーケストレーションCibaFlowEntryService.java:86-142
CibaProtocolCoreCIBA仕様準拠処理Extension Core
UserHintResolverCorelogin_hint → User解決(Plugin)Extension Core
BackchannelAuthenticationRequestCoreCIBA認証リクエスト(5分TTL)Extension Core
AuthenticationTransactionCore認証状態管理Core Domain

CIBAの特徴

Authorization Code Flowとの違い:

  • リダイレクトなし: ユーザーは別デバイスで承認
  • auth_req_id: Authorization Codeの代わり
  • ポーリング: クライアントが定期的にトークンリクエスト
  • プッシュ通知: FCM/APNS/SMSでユーザーに通知
  • 非同期: 認証リクエストとトークン取得が分離

CIBAフロー

1. [クライアント] CIBA認証リクエスト
POST /{tenant-id}/v1/backchannel/authentications
{
"login_hint": "user@example.com",
"binding_message": "Code: 1234",
"client_notification_token": "xxx" // Push mode
}

2. [idp-server] auth_req_id返却
{
"auth_req_id": "auth-req-abc123",
"expires_in": 300,
"interval": 5 // Poll mode
}

3. [idp-server] ユーザーにプッシュ通知送信
FCM/APNS → [ユーザーのスマホ]
「Code: 1234でログイン承認しますか?」

4. [ユーザー] スマホで承認

5. [idp-server] 認証完了を記録

6. [クライアント] トークンリクエスト(ポーリング)
POST /{tenant-id}/v1/tokens
{
"grant_type": "urn:openid:params:grant-type:ciba",
"auth_req_id": "auth-req-abc123",
"client_id": "xxx",
"client_secret": "yyy"
}

7. [idp-server] Access Token + ID Token発行
{
"access_token": "eyJ...",
"id_token": "eyJ...",
"token_type": "Bearer"
}

エンドポイント

# CIBA認証リクエスト
POST /{tenant-id}/v1/backchannel/authentications

# トークン取得(ポーリング)
POST /{tenant-id}/v1/tokens
{
"grant_type": "urn:openid:params:grant-type:ciba",
"auth_req_id": "xxx"
}

実装:


EntryService実装

実装: CibaFlowEntryService.java:86-142

CIBA認証リクエスト処理(10ステップ)

@Transaction
public class CibaFlowEntryService implements CibaFlowApi {

CibaProtocols cibaProtocols;
UserHintResolvers userHintResolvers;
AuthenticationInteractors authenticationInteractors;
CibaRequestAdditionalVerifiers additionalVerifiers;
TenantQueryRepository tenantQueryRepository;
AuthenticationTransactionCommandRepository authenticationTransactionCommandRepository;
AuthenticationPolicyConfigurationQueryRepository authenticationPolicyConfigurationQueryRepository;
CibaFlowEventPublisher eventPublisher;

@Override
public CibaRequestResponse request(
TenantIdentifier tenantIdentifier,
Map<String, String[]> params,
String authorizationHeader,
String clientCert,
RequestAttributes requestAttributes) {

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

// 2. CibaRequest作成
CibaRequest cibaRequest = new CibaRequest(tenant, authorizationHeader, params);
cibaRequest.setClientCert(clientCert);

// 3. CibaProtocol取得
CibaProtocol cibaProtocol = cibaProtocols.get(tenant.authorizationProvider());

// 4. Core層に委譲(UserHintResolver + AdditionalVerifiers使用)
CibaIssueResponse issueResponse =
cibaProtocol.request(cibaRequest, userHintResolvers, additionalVerifiers);

// 5. エラー時は即座に返却
if (!issueResponse.isOK()) {
return issueResponse.toErrorResponse();
}

// 6. イベント発行(CIBA認証リクエスト成功)
eventPublisher.publish(
tenant,
issueResponse.request(),
issueResponse.user(),
DefaultSecurityEventType.backchannel_authentication_request_success.toEventType(),
requestAttributes);

// 7. AuthenticationPolicyConfiguration取得
AuthenticationPolicyConfiguration authenticationPolicyConfiguration =
authenticationPolicyConfigurationQueryRepository.find(
tenant, StandardAuthFlow.CIBA.toAuthFlow());

// 8. AuthenticationTransaction作成
AuthenticationTransaction authenticationTransaction =
CibaAuthenticationTransactionCreator.create(
tenant, issueResponse, authenticationPolicyConfiguration);

// 9. デフォルト認証実行(プッシュ通知送信)
AuthenticationInteractionType authenticationInteractionType =
issueResponse.defaultCibaAuthenticationInteractionType();
AuthenticationInteractor authenticationInteractor =
authenticationInteractors.get(authenticationInteractionType);

AuthenticationInteractionRequestResult interactionRequestResult =
authenticationInteractor.interact(
tenant,
authenticationTransaction,
authenticationInteractionType,
new AuthenticationInteractionRequest(Map.of()),
requestAttributes,
userQueryRepository);

// 10. AuthenticationTransaction保存 + イベント発行
AuthenticationTransaction updatedTransaction =
authenticationTransaction.updateWith(interactionRequestResult);
authenticationTransactionCommandRepository.register(tenant, updatedTransaction);

eventPublisher.publish(
tenant,
issueResponse.request(),
issueResponse.user(),
interactionRequestResult.eventType(),
requestAttributes);

return issueResponse.toResponse();
}
}

ポイント

  • UserHintResolvers: login_hint(email/phone/sub)からUserを解決(Plugin拡張可能)
  • CibaRequestAdditionalVerifiers: CIBA固有の検証(パスワード必須等)
  • デフォルト認証実行: CIBA認証リクエスト時点でプッシュ通知を送信
  • AuthenticationTransaction: 認証状態を管理(ユーザー承認待ち)
  • イベント発行: 2回(リクエスト成功・プッシュ通知送信)

UserHintResolvers(login_hint解決)

実装: UserHintResolvers.java

login_hintの形式

CIBAではlogin_hintパラメータでユーザーを特定します。複数の形式をサポート:

プレフィックス形式検索方法
sub:idp-server内部のユーザーIDsub:user-uuid-12345UserIdentifierで直接取得
email:メールアドレスemail:user@example.comemailで検索
phone:電話番号phone:+81-90-1234-5678phone_numberで検索
ex-sub:外部IdPのsubex-sub:google-user-12345:googleexternal_user_id + provider_idで検索
device:デバイスIDdevice:device-uuid-67890authentication_device.idで検索

LoginHintResolverの実装

実装: LoginHintResolver.java:27-76

public class LoginHintResolver implements UserHintResolver {

@Override
public User resolve(
Tenant tenant,
UserHint userHint,
UserHintRelatedParams userHintRelatedParams,
UserQueryRepository userQueryRepository) {

String loginHint = userHint.value();

// プレフィックスマッチャー(5種類)
List<LoginHintMatcher> matchers = List.of(
// 1. sub:user-uuid
new PrefixMatcher("sub:", hints -> {
UserIdentifier userIdentifier = new UserIdentifier(hints.getLeft());
return userQueryRepository.get(tenant, userIdentifier);
}),

// 2. ex-sub:external-sub:provider-id
new PrefixMatcher("ex-sub:", hints ->
userQueryRepository.findByExternalIdpSubject(
tenant, hints.getLeft(), hints.getRight())),

// 3. device:device-id:provider-id
new PrefixMatcher("device:", hints ->
userQueryRepository.findByDeviceId(
tenant,
new AuthenticationDeviceIdentifier(hints.getLeft()),
hints.getRight())),

// 4. phone:+81-90-1234-5678:provider-id
new PrefixMatcher("phone:", hints ->
userQueryRepository.findByPhone(tenant, hints.getLeft(), hints.getRight())),

// 5. email:user@example.com:provider-id
new PrefixMatcher("email:", hints ->
userQueryRepository.findByEmail(tenant, hints.getLeft(), hints.getRight()))
);

// 最初にマッチしたResolverでUser解決
return matchers.stream()
.filter(matcher -> matcher.matches(loginHint))
.findFirst()
.map(matcher -> {
Pairs<String, String> hints = matcher.extractHints(loginHint);
return matcher.resolve(hints);
})
.orElse(User.notFound());
}
}

使用例

1. emailでユーザー特定

curl -X POST "http://localhost:8080/${TENANT_ID}/v1/backchannel/authentications" \
-H "Content-Type: application/x-www-form-urlencoded" \
-H "Authorization: Basic $(echo -n 'client:secret' | base64)" \
-d "login_hint=email:user@example.com&binding_message=Code: 1234&scope=openid"

処理:

login_hint="email:user@example.com"

PrefixMatcher("email:").matches() → true

extractHints() → ("user@example.com", "")

userQueryRepository.findByEmail(tenant, "user@example.com", "")

User取得

2. phone番号でユーザー特定

curl -X POST "..." \
-d "login_hint=phone:+81-90-1234-5678&binding_message=Code: 1234&scope=openid"

3. デバイスIDでユーザー特定

スマホアプリのデバイスIDを使用:

curl -X POST "..." \
-d "login_hint=device:device-uuid-67890&binding_message=Code: 1234&scope=openid"

4. 外部IdPのsubでユーザー特定

Google等の外部IdPで既に認証済みのユーザー:

curl -X POST "..." \
-d "login_hint=ex-sub:google-user-12345:google&binding_message=Code: 1234&scope=openid"

エラー

ユーザーが見つからない場合:

{
"error": "invalid_request",
"error_description": "User not found for login_hint: email:unknown@example.com"
}

HTTP Status: 400 Bad Request


CIBA Mode

CIBAは3つのモードをサポート:

Mode説明トークン取得方法
Pollポーリングクライアントが定期的にトークンリクエスト
PingPing通知idp-serverがclient_notification_endpointに通知
Pushプッシュidp-serverがclient_notification_endpointにトークン送信

Poll Mode(最も一般的)

1. CIBA Request
↓ レスポンス
{
"auth_req_id": "xxx",
"expires_in": 300,
"interval": 5 // 5秒ごとにポーリング
}

2. クライアントがポーリング(5秒ごと)
POST /{tenant-id}/v1/tokens
{
"grant_type": "urn:openid:params:grant-type:ciba",
"auth_req_id": "xxx"
}
↓ ユーザー未承認時
{
"error": "authorization_pending"
}
↓ ユーザー承認完了後
{
"access_token": "eyJ...",
"id_token": "eyJ..."
}

クライアント認証

CIBA認証リクエストではクライアント認証が必須です。

詳細: 10. Client Authentication実装 - 7つの認証方式の完全ガイド

サポートされる認証方式

認証方式送信方法セキュリティレベル
client_secret_basicBasic認証ヘッダー⭐⭐
client_secret_postPOSTボディ
client_secret_jwtJWT署名(共有鍵)⭐⭐⭐
private_key_jwtJWT署名(秘密鍵)⭐⭐⭐⭐
tls_client_authクライアント証明書(MTLS)⭐⭐⭐⭐⭐

client_secret_basic の例

curl -X POST "http://localhost:8080/${TENANT_ID}/v1/backchannel/authentications" \
-H "Content-Type: application/x-www-form-urlencoded" \
-H "Authorization: Basic $(echo -n 'my-client:my-secret' | base64)" \
-d "login_hint=user@example.com&binding_message=Code: 1234&scope=openid profile"

client_secret_post の例

curl -X POST "http://localhost:8080/${TENANT_ID}/v1/backchannel/authentications" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "login_hint=user@example.com&binding_message=Code: 1234&scope=openid profile&client_id=my-client&client_secret=my-secret"

トークンリクエスト(CIBA Grant Type)

実装: CibaGrantService.java

リクエスト

POST /{tenant-id}/v1/tokens
Content-Type: application/x-www-form-urlencoded
Authorization: Basic base64(client_id:client_secret)

grant_type=urn:openid:params:grant-type:ciba&auth_req_id=8d67dc78-7faa-4d41-aabd-67707b374255

処理フロー

CibaGrantService.create()

┌─────────────────────────────────────────────────────┐
│ 1. Validator(入力形式チェック) │
├─────────────────────────────────────────────────────┤
│ - auth_req_id必須チェック │
│ - grant_type検証 │
└─────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────┐
│ 2. BackchannelAuthenticationRequest取得 │
├─────────────────────────────────────────────────────┤
│ - auth_req_idでBackchannelAuthenticationRequest検索 │
│ - 存在しない → invalid_grant エラー │
└─────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────┐
│ 3. Verifier(ビジネスルール検証) │
├─────────────────────────────────────────────────────┤
│ - 有効期限チェック(5分以内) │
│ - クライアント一致チェック │
│ - ユーザー承認済みチェック │
└─────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────┐
│ 4. 承認状態チェック │
├─────────────────────────────────────────────────────┤
│ - 承認待ち → authorization_pending │
│ - ユーザー拒否 → access_denied │
│ - 承認完了 → トークン発行へ │
└─────────────────────────────────────────────────────┘
↓ 承認完了の場合
┌─────────────────────────────────────────────────────┐
│ 5. トークン生成 │
├─────────────────────────────────────────────────────┤
│ - Access Token生成(JWT、デフォルト1時間有効) │
│ - Refresh Token生成(設定による) │
│ - ID Token生成(nonce/at_hash/c_hash含む) │
│ - BackchannelAuthenticationRequest削除(ワンタイム) │
└─────────────────────────────────────────────────────┘

OAuthToken(Access/Refresh/ID Token)

レスポンスパターン

1. 承認待ち(authorization_pending)

ユーザーがまだスマホで承認していない状態:

{
"error": "authorization_pending",
"error_description": "The authorization request is still pending"
}

HTTP Status: 400 Bad Request

対処: interval秒待機してリトライ(デフォルト5秒)


2. 承認完了(トークン発行)

ユーザーがスマホで承認した後:

{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"id_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"scope": "openid profile email"
}

HTTP Status: 200 OK

重要: auth_req_idは削除される(ワンタイム使用)


3. ユーザー拒否(access_denied)

ユーザーがスマホで拒否ボタンを押した場合:

{
"error": "access_denied",
"error_description": "The resource owner denied the request"
}

HTTP Status: 400 Bad Request

対処: 新しいCIBA認証リクエストを実行


4. タイムアウト(expired_token)

auth_req_idの有効期限(5分)が切れた場合:

{
"error": "expired_token",
"error_description": "The auth_req_id has expired"
}

HTTP Status: 400 Bad Request

対処: 新しいCIBA認証リクエストを実行


ポーリング仕様(OpenID Connect CIBA準拠)

interval(ポーリング間隔)

CIBA認証リクエストのレスポンスで返されるintervalを厳守:

{
"auth_req_id": "xxx",
"expires_in": 300,
"interval": 5 // この秒数ごとにポーリング
}

RFC仕様:

  • クライアントは最低でもinterval待機してからリトライ
  • interval未満でリクエスト → slow_downエラー
  • slow_down受信時 → intervalを5秒延長

: interval=5の場合

0秒: CIBA認証リクエスト
5秒: 1回目のトークンリクエスト → authorization_pending
10秒: 2回目のトークンリクエスト → authorization_pending
15秒: 3回目のトークンリクエスト → トークン発行成功

expires_in(有効期限)

auth_req_idはexpires_in秒間のみ有効:

{
"auth_req_id": "xxx",
"expires_in": 300 // 300秒 = 5分間有効
}

動作:

  • 5分以内にユーザー承認 → トークン発行可能
  • 5分経過後 → expired_tokenエラー
  • トークン発行後 → auth_req_id削除(再利用不可)

AuthorizationGranted 登録タイミング

CIBAフローでのAuthorizationGranted登録

AuthorizationGranted(認可同意記録)はユーザー承認時CibaAuthorizeHandler)に登録されます。

1. CIBA認証リクエスト(auth_req_id発行)

2. プッシュ通知送信

3. ユーザーがスマホで承認 ← この時点でAuthorizationGranted登録

4. Token Request(ポーリング)

用途

  • 同意記録の管理: ユーザーがどのクライアントに同意したかの履歴
  • Grant Management: 同意の取り消し・更新

注意: CIBAフローはブラウザセッションを作成しないため、Authorization Code Flowのようなprompt=noneによるSSOは適用されません。

実装: CibaAuthorizeHandler.java


auth_req_id のライフサイクル

作成 → 使用 → 削除

1. CIBA認証リクエスト
→ BackchannelAuthenticationRequest作成
→ auth_req_id生成(UUID)
→ Redis + DB保存(5分TTL)

2. ユーザー承認待ち
→ ポーリング: authorization_pending
→ AuthenticationTransaction更新(承認状態)

3. トークンリクエスト(承認完了後)
→ BackchannelAuthenticationRequest取得
→ トークン生成
→ BackchannelAuthenticationRequest削除 ← ワンタイム

4. 再度トークンリクエスト
→ BackchannelAuthenticationRequest不存在
→ invalid_grant エラー

重要: Authorization Codeと同じく、使用後即削除される(usedフラグではない)


BackchannelAuthenticationRequest vs AuthenticationTransaction

2つの異なるオブジェクトが使われます:

オブジェクト役割作成タイミング削除タイミング
BackchannelAuthenticationRequestCIBA認証リクエスト情報CIBA認証リクエスト時トークン発行後
AuthenticationTransaction認証状態管理CIBA認証リクエスト時認証完了 or 失敗 or ロック時

BackchannelAuthenticationRequest

保存される情報:

- auth_req_id: UUID
- client_id: クライアントID
- login_hint: ユーザーヒント
- binding_message: "Code: 1234"
- scope: "openid profile email"
- expiresAt: 現在時刻+5分

用途: トークンリクエスト時の検証(クライアント一致・有効期限等)

AuthenticationTransaction

保存される情報:

- identifier: auth-req-12345
- authorizationIdentifier: auth-req-12345
- authenticationPolicy: 認証ポリシー
- interactionResults: {"push_notification": {successCount: 0, attemptCount: 1}}
- request.user: User(sub=user-12345)

用途: ユーザー承認状態の管理(承認待ち・承認完了・拒否)

使い分け

CIBA認証リクエスト:
BackchannelAuthenticationRequest作成(auth_req_id発行)
AuthenticationTransaction作成(認証状態管理)

ユーザー承認:
AuthenticationTransaction更新(interactionResults更新)

トークンリクエスト:
BackchannelAuthenticationRequest検証(有効期限・クライアント)
AuthenticationTransaction確認(承認済みか)
トークン発行
BackchannelAuthenticationRequest削除
AuthenticationTransaction削除

チェックリスト

CIBA実装時の確認項目:

EntryService(UseCase層)

  • CibaRequest作成
  • UserHintResolvers使用(login_hint解決)
  • AuthenticationTransaction作成
  • プッシュ通知送信

Core層(CibaProtocol)

  • login_hint検証
  • binding_message検証
  • BackchannelAuthenticationRequest生成

E2Eテスト

  • Poll modeテスト
  • ユーザー承認テスト
  • ユーザー拒否テスト

よくあるエラー

エラー1: authorization_pending - 認証待ち

原因: ユーザーがまだ承認していない

解決策: ポーリング継続(interval秒待機)

エラー2: access_denied - ユーザーが拒否

原因: ユーザーがスマホで拒否ボタンを押した

解決策: 新しいCIBA認証リクエストを実行


次のステップ

✅ CIBA Flowの実装を理解した!

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

  1. 07. Identity Verification実装 - 身元確認申込み

🔗 詳細情報


情報源: CibaFlowEntryService.java 最終更新: 2025-10-12