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

FIDO2 識別子とIdP統合 - userId, username, credentialId の関係


概要

FIDO2/WebAuthn では複数の識別子が登場し、それぞれが異なる役割を持ちます。このドキュメントでは、これらの識別子の関係性と、IdP(Identity Provider)との統合における注意点を解説します。

このドキュメントで学べること:

  • FIDO2 における3つの主要な識別子(userId, username, credentialId)
  • 各識別子の発行者と保存先
  • 登録・認証フローでの識別子の流れ
  • IdP と FIDOサーバーの役割分担
  • 実装パターンと設計上の考慮事項

FIDO2 の識別子一覧

3つの主要な識別子

┌─────────────────────────────────────────────────────────────────────────────┐
│ FIDO2 の識別子 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ userId │ │ username │ │ credentialId │ │
│ │ (user.id) │ │ (user.name) │ │ │ │
│ ├─────────────────┤ ├─────────────────┤ ├─────────────────┤ │
│ │ バイト列 │ │ 文字列 │ │ バイト列 │ │
│ │ (Base64URL) │ │ │ │ (Base64URL) │ │
│ ├─────────────────┤ ├─────────────────┤ ├─────────────────┤ │
│ │ RP が発行 │ │ RP が決定 │ │ 認証器が生成 │ │
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘

各識別子の詳細

識別子WebAuthn での名称発行者最大サイズ用途
userIduser.id / userHandleRP(IdP)64バイトユーザーの一意識別
usernameuser.nameRP(IdP)任意人間が読めるユーザー識別子
credentialIdcredential.id / id認証器1023バイト公開鍵/秘密鍵ペアの識別

登録フローでの識別子の流れ

シーケンス図

┌──────────────────────────────────────────────────────────────────────────────┐
│ 登録フロー │
├──────────────────────────────────────────────────────────────────────────────┤
│ │
│ IdP (RP Server) クライアント (RP) ブラウザ 認証器 │
│ │ │ │ │ │
│ │ │ │ │ │
│ ┌────┴────┐ │ │ │ │
│ │ userId │ │ │ │ │
│ │ username│ ← IdP が決定 │ │ │ │
│ └────┬────┘ │ │ │ │
│ │ │ │ │ │
│ │ ① Challenge + │ │ │ │
│ │ user: { │ │ │ │
│ │ id: userId, │ │ │ │
│ │ name: username, │ │ │ │
│ │ displayName │ │ │ │
│ │ } │ │ │ │
│ │─────────────────────────>│ │ │ │
│ │ │ │ │ │
│ │ │ ② create() │ │ │
│ │ │─────────────────────>│ │ │
│ │ │ │ │ │
│ │ │ │ ③ 認証器へ │ │
│ │ │ │────────────>│ │
│ │ │ │ │ │
│ │ │ │ ┌──────┴─────┐│
│ │ │ │ │ 鍵ペア生成 ││
│ │ │ │ │credentialId││
│ │ │ │ │ 生成 ││
│ │ │ │ │ ││
│ │ │ │ │ userId保存 ││
│ │ │ │ │ (※Passkey) ││
│ │ │ │ └──────┬─────┘│
│ │ │ │ │ │
│ │ │ │ ④ 結果 │ │
│ │ │ │<────────────│ │
│ │ │ │ │ │
│ │ │ ⑤ 結果 │ │ │
│ │ │<─────────────────────│ │ │
│ │ │ │ │ │
│ │ ⑥ id: credentialId, │ │ │ │
│ │ attestationObject, │ │ │ │
│ │ clientDataJSON │ │ │ │
│ │<─────────────────────────│ │ │ │
│ │ │ │ │ │
│ ┌────┴────┐ │ │ │ │
│ │検証+保存│ │ │ │ │
│ │userId │ │ │ │ │
│ │ ↓ │ │ │ │ │
│ │credential │ │ │ │
│ │ Id │ │ │ │ │
│ └─────────┘ │ │ │ │
│ │
└──────────────────────────────────────────────────────────────────────────────┘

登録時のデータフロー

① RP Server → クライアント(Challenge レスポンス)

{
"challenge": "ランダムなチャレンジ(Base64URL)",
"rp": {
"id": "example.com",
"name": "Example Corporation"
},
"user": {
"id": "dXNlci0xMjM0NTY3ODkw", // ← IdP が発行した userId (Base64URL)
"name": "user@example.com", // ← username
"displayName": "田中 太郎" // ← 表示名
},
"pubKeyCredParams": [...],
"timeout": 60000,
"attestation": "none"
}

重要: user.iduser.name は IdP(RP Server)が決定します。

⑥ クライアント → RP Server(登録レスポンス)

{
"id": "Y3JlZGVudGlhbElkLTEyMzQ1Njc4OTA...", // ← credentialId(認証器が生成)
"type": "public-key",
"response": {
"clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIi...",
"attestationObject": "o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRo..."
}
}

重要: id(credentialId)は認証器が生成した値です。クライアントから送られてきます。


認証フローでの識別子の流れ

シーケンス図

┌──────────────────────────────────────────────────────────────────────────────┐
│ 認証フロー │
├──────────────────────────────────────────────────────────────────────────────┤
│ │
│ IdP (RP Server) クライアント (RP) ブラウザ 認証器 │
│ │ │ │ │ │
│ │ ① Challenge + │ │ │ │
│ │ allowCredentials │ │ │ │
│ │─────────────────────────>│ │ │ │
│ │ │ │ │ │
│ │ │ ② get() │ │ │
│ │ │─────────────────────>│ │ │
│ │ │ │ │ │
│ │ │ │ ③ 認証器へ │ │
│ │ │ │────────────>│ │
│ │ │ │ │ │
│ │ │ │ ┌──────┴─────┐│
│ │ │ │ │ 署名生成 ││
│ │ │ │ │ ││
│ │ │ │ │ userHandle ││
│ │ │ │ │ 返却 ││
│ │ │ │ │ (※Passkey) ││
│ │ │ │ └──────┬─────┘│
│ │ │ │ │ │
│ │ │ │ ④ 結果 │ │
│ │ │ │<────────────│ │
│ │ │ │ │ │
│ │ │ ⑤ 結果 │ │ │
│ │ │<─────────────────────│ │ │
│ │ │ │ │ │
│ │ ⑥ id: credentialId, │ │ │ │
│ │ signature, │ │ │ │
│ │ userHandle, │ │ │ │
│ │ authenticatorData │ │ │ │
│ │<─────────────────────────│ │ │ │
│ │ │ │ │ │
│ ┌────┴────┐ │ │ │ │
│ │署名検証 │ │ │ │ │
│ │ │ │ │ │ │
│ │credentialId │ │ │ │
│ │ → 公開鍵特定 │ │ │ │
│ │ │ │ │ │ │
│ │userHandle │ │ │ │
│ │ → ユーザー特定 │ │ │ │
│ │ (※Passkey) │ │ │ │
│ └─────────┘ │ │ │ │
│ │
└──────────────────────────────────────────────────────────────────────────────┘

認証時の識別子の役割

識別子認証時の役割使用シナリオ
credentialId公開鍵の特定常に使用
userHandleユーザーの特定Passkey(Discoverable Credential)の場合
username(使用しない)認証前にユーザー名入力がある場合のみ

ユーザー特定の方式

方式1: 2要素認証(ユーザー既知)

1. ユーザーがユーザー名/パスワードで認証
2. IdP がユーザーを特定
3. そのユーザーの credentialId リストを allowCredentials に設定
4. FIDO2 認証を実行
5. 署名検証のみ(ユーザーは既に特定済み)

方式2: Passkey(Discoverable Credential)

1. ユーザー名入力なしで認証開始
2. allowCredentials を空で送信
3. 認証器がユーザーを選択(UI表示)
4. 認証器が userHandle(userId)を返却
5. IdP が userHandle でユーザーを特定

識別子によるユーザー特定

┌─────────────────────────────────────────────────────────────────────────────┐
│ ユーザー特定の流れ │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 【2要素認証の場合】 │
│ │
│ ユーザー名入力 → IdP でユーザー特定 → credentialId で公開鍵特定 │
│ ~~~~~~~~~~~~~~~~~~~ │
│ 事前にユーザー確定 │
│ │
│ 【Passkey の場合】 │
│ │
│ 認証器の応答 → userHandle で IdP ユーザー特定 → credentialId で公開鍵特定│
│ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ │
│ userHandle = 登録時の userId │
│ │
└─────────────────────────────────────────────────────────────────────────────┘

IdP と FIDOサーバーの役割分担

アーキテクチャパターン

FIDO2 の実装には、主に2つのアーキテクチャパターンがあります。

パターン1: FIDOサーバー内蔵型

┌─────────────────────────────────────────────────────────────────────────────┐
│ IdP (FIDOサーバー内蔵) │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────────────────────────────────────────────────────────┐ │
│ │ IdP Server │ │
│ │ ┌─────────────────┐ ┌─────────────────────────────────────────────┐│ │
│ │ │ User 管理 │ │ FIDO ライブラリ ││ │
│ │ │ │ │ (webauthn4j 等) ││ │
│ │ │ - userId │ │ ││ │
│ │ │ - username │ │ - Attestation 検証 ││ │
│ │ │ - credentials │ │ - Assertion 検証 ││ │
│ │ │ │ │ - 公開鍵管理 ││ │
│ │ └─────────────────┘ └─────────────────────────────────────────────┘│ │
│ └───────────────────────────────────────────────────────────────────────┘ │
│ │
│ 例: Keycloak, Auth0 (内蔵モード) │
│ │
└─────────────────────────────────────────────────────────────────────────────┘

特徴:

  • userId と credentialId の紐づけが IdP 内部で完結
  • シンプルな構成
  • FIDOサーバーのレスポンス形式を気にする必要がない

パターン2: 外部FIDOサーバー連携型

┌─────────────────────────────────────────────────────────────────────────────┐
│ IdP + 外部 FIDOサーバー │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────────┐ ┌───────────────────────────────────────┐│
│ │ IdP Server │ │ FIDO Server ││
│ │ │ │ (LINE FIDO2 等) ││
│ │ - User 管理 │ HTTP │ ││
│ │ - userId │<──────>│ - Attestation 検証 ││
│ │ - username │ │ - Assertion 検証 ││
│ │ - credentialId 参照 │ │ - 公開鍵管理 ││
│ │ (紐づけ用) │ │ - credentialId 管理 ││
│ └───────────────────────┘ └───────────────────────────────────────┘│
│ │
│ 例: idp-server + LINE FIDO2 Server │
│ │
└─────────────────────────────────────────────────────────────────────────────┘

特徴:

  • IdP と FIDOサーバーが分離
  • credentialId の紐づけ管理が必要
  • FIDOサーバーの API 仕様を考慮する必要がある

credentialId の紐づけ

登録時の紐づけ

外部FIDOサーバー連携型では、登録時に credentialId を IdP 側で保存する必要があります。

┌─────────────────────────────────────────────────────────────────────────────┐
│ 登録時の credentialId 紐づけ │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ クライアント IdP FIDOサーバー │
│ │ │ │ │
│ │ 登録リクエスト │ │ │
│ │ id: credentialId │ │ │
│ │ response: {...} │ │ │
│ │──────────────────────>│ │ │
│ │ │ │ │
│ │ │ Attestation 検証リクエスト│ │
│ │ │ id: credentialId │ │
│ │ │ response: {...} │ │
│ │ │──────────────────────────>│ │
│ │ │ │ │
│ │ │ 検証結果 │ │
│ │ │ status: "ok" │ │
│ │ │<──────────────────────────│ │
│ │ │ │ │
│ │ ┌─────┴─────┐ │ │
│ │ │ credentialId を │ │
│ │ │ User に紐づけて保存 │ │
│ │ │ │ │
│ │ │ ※ credentialId は │ │
│ │ │ リクエストから取得 │ │
│ │ │ (FIDOサーバーの │ │
│ │ │ レスポンスではない) │ │
│ │ └─────┬─────┘ │ │
│ │ │ │ │
│ │ 登録成功 │ │ │
│ │<──────────────────────│ │ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘

重要なポイント:

  • FIDO 認定テスト API では、FIDOサーバーの登録レスポンスは status のみ
  • credentialId はクライアントからのリクエストに含まれている
  • IdP は リクエストの id フィールドから credentialId を取得して保存する

IdP でのデータモデル例

User
├── sub: "user-123" // IdP の内部ユーザーID
├── preferredUsername: "user@example.com"
├── authenticationDevices[]
│ └── AuthenticationDevice
│ ├── deviceId: "device-456"
│ └── credentials[]
│ └── DeviceCredential
│ ├── type: "fido2"
│ ├── credentialId: "Y3JlZGVudGlhbC..." // 認証器が生成した ID
│ ├── rpId: "example.com"
│ └── fidoServerId: "webauthn4j-001"

FIDO2 userId と IdP ユーザーID の関係

マッピング戦略

FIDO2 の user.id(userId)と IdP のユーザーID(sub)は、必ずしも同じである必要はありません。

戦略説明
同一値IdP の sub をそのまま使用user.id = User.sub
エンコードIdP の sub を Base64URL エンコードuser.id = Base64URL(User.sub)
ハッシュIdP の sub をハッシュ化user.id = SHA256(User.sub)
別値専用の FIDO ユーザーID を生成user.id = UUID()

Keycloak の実装例

// KeycloakユーザーIDをFIDO2のuser.idに変換
String userId = Base64Url.encode(userModel.getId().getBytes(StandardCharsets.UTF_8));

考慮事項

  • user.id は最大64バイト
  • Passkey の場合、認証器が user.id を保存・返却する
  • user.id から IdP ユーザーを逆引きできる必要がある(Passkey の場合)

まとめ

識別子の発行者と用途

識別子発行者保存先登録時認証時
userIdIdPIdP + 認証器(※)RP→認証器認証器→RP(※)
usernameIdPIdPRP→認証器-
credentialId認証器認証器 + FIDOサーバー + IdP認証器→RP認証器→RP

※ Passkey(Discoverable Credential)の場合

設計上のベストプラクティス

  1. credentialId はリクエストから取得

    • FIDOサーバーのレスポンスに依存しない
    • FIDO 標準に準拠
  2. userId は IdP ユーザーから派生

    • 逆引き可能な形式を選択
    • Passkey 対応を考慮
  3. FIDOサーバーの役割を明確化

    • 暗号検証のみを担当
    • ユーザー管理は IdP の責務

参考リンク