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

FIDO2/WebAuthn パスキー認証

このドキュメントの目的​

認可コードフロー内でFIDO2パスキーを使用して認証するための設定を行うことが目標です。

学べること​

  • 認可コードフロー内でのFIDO2認証の仕組み
  • パスワードレスログインの設定
  • Discoverable / 非Discoverable Credentialの違い

所要時間​

⏱️ 約15分

前提条件​

  • パスキー登録が完了していること
  • ユーザーがFIDO2クレデンシャルを登録済みであること
  • パスワード認証が設定済みであること(パスワードからの移行時)

認可コードフロー内でのFIDO2認証​

認証フローの全体像​

FIDO2認証は、OAuth 2.0認可コードフロー内で行います。パスワード認証の代替として、または追加の認証要素として使用できます。

┌─────────────────────────────────────────────────────────────┐
│ 認可コードフロー │
├─────────────────────────────────────────────────────────────┤
│ │
│ GET /v1/authorizations ────────────────────────────────┐ │
│ ↓ │ │
│ ┌─────────────────────────────────────────────────┐ │ │
│ │ 認証方式の選択 │ │ │
│ │ ├── パスワード認証 │ │ │
│ │ └── FIDO2認証(パスキー)← このドキュメント │ │ │
│ └─────────────────────────────────────────────────┘ │ │
│ ↓ │ │
│ POST /authorizations/{id}/fido2-authentication-challenge│ │
│ ↓ │ │
│ POST /authorizations/{id}/fido2-authentication │ │
│ ↓ │ │
│ POST /authorizations/{id}/authorize ───────────────────┘ │
│ ↓ │
│ 認可コード発行 → トークン取得 │
│ │
└─────────────────────────────────────────────────────────────┘

パスワードレスログイン​

FIDO2認証がsuccess_conditionsに含まれていれば、パスワードなしでログイン可能です。

{
"success_conditions": {
"any_of": [
[{ "path": "$.password-authentication.success_count", "operation": "gte", "value": 1 }],
[{ "path": "$.fido2-authentication.success_count", "operation": "gte", "value": 1 }]
]
}
}

この設定では:

  • パスワード認証のみ → ログイン成功
  • FIDO2認証のみ → ログイン成功(パスワードレス)
  • どちらか一方で十分

認証フローの種類​

1. Discoverable Credential(パスキー)認証​

ユーザー名入力なしで認証できます。認証器がクレデンシャルを保持しており、ユーザーが選択します。

┌────────────────────────────────────────────────────────┐
│ フロントエンド │
│ ┌──────────────────────────────────────────────────┐ │
│ │ 「パスキーでログイン」ボタン │ │
│ │ ↓ │ │
│ │ 認証器がクレデンシャル一覧を表示 │ │
│ │ ↓ │ │
│ │ ユーザーが選択 + 生体認証 │ │
│ └──────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────┘

APIフロー:

POST /authorizations/{id}/fido2-authentication-challenge
Body: {} ← usernameなし

Response:
{
"challenge": "...",
"allowCredentials": [] ← 空配列 = Discoverable
}

2. 非Discoverable Credential認証​

ユーザー名を入力してから認証します。サーバーがクレデンシャルIDを指定します。

┌────────────────────────────────────────────────────────┐
│ フロントエンド │
│ ┌──────────────────────────────────────────────────┐ │
│ │ ユーザー名入力: [user@example.com] │ │
│ │ ↓ │ │
│ │ 「パスキーでログイン」ボタン │ │
│ │ ↓ │ │
│ │ 指定されたクレデンシャルで認証 │ │
│ └──────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────┘

APIフロー:

POST /authorizations/{id}/fido2-authentication-challenge
Body: { "username": "user@example.com" }

Response:
{
"challenge": "...",
"allowCredentials": [
{ "type": "public-key", "id": "credential-id-1" },
{ "type": "public-key", "id": "credential-id-2" }
]
}

設定手順​

FIDO2認証設定(認証部分)​

パスキー登録で作成した設定に、認証用のinteractionが含まれています。

{
"interactions": {
"fido2-authentication-challenge": {
"execution": {
"function": "webauthn4j_authentication_challenge",
"details": {
"rp_id": "example.com",
"origin": "https://example.com",
"user_verification_required": true
}
},
"response": {
"body_mapping_rules": [
{ "from": "$.execution_webauthn4j", "to": "*" }
]
}
},
"fido2-authentication": {
"execution": {
"function": "webauthn4j_authentication",
"details": {
"rp_id": "example.com",
"origin": "https://example.com"
}
},
"response": {
"body_mapping_rules": [
{ "from": "$.execution_webauthn4j", "to": "*" }
]
}
}
}
}

認証ポリシー設定​

FIDO2をログイン方式として許可するには、success_conditionsに追加します。

{
"policies": [
{
"available_methods": ["password", "fido2"],
"success_conditions": {
"any_of": [
[
{
"path": "$.password-authentication.success_count",
"type": "integer",
"operation": "gte",
"value": 1
}
],
[
{
"path": "$.fido2-authentication.success_count",
"type": "integer",
"operation": "gte",
"value": 1
}
]
]
}
}
]
}

認証フローAPI​

全体フロー​

💡 ヒント: 認可リクエストに prompt=login を指定すると、既存のセッションがある場合でも再認証を強制できます。ステップアップ認証など、FIDO2パスキーによる明示的な再認証が必要なシナリオで有用です。

1. GET  /{tenant}/v1/authorizations?response_type=code&...
→ 302 Redirect with authorization_id

2. POST /{tenant}/v1/authorizations/{id}/fido2-authentication-challenge
→ チャレンジ取得

3. [フロントエンド] navigator.credentials.get() で認証器を呼び出し

4. POST /{tenant}/v1/authorizations/{id}/fido2-authentication
→ 認証成功

5. POST /{tenant}/v1/authorizations/{id}/authorize
→ 認可コード発行

FIDO2認証チャレンジ取得​

POST {tenant}/v1/authorizations/{authorization-id}/fido2-authentication-challenge
Content-Type: application/json

リクエスト(Discoverable Credential):

{}

リクエスト(非Discoverable Credential):

{
"username": "user@example.com"
}

レスポンス:

{
"challenge": "base64url-encoded-challenge",
"rpId": "example.com",
"timeout": 60000,
"userVerification": "required",
"allowCredentials": [
{
"type": "public-key",
"id": "base64url-encoded-credential-id",
"transports": ["internal", "hybrid"]
}
]
}
フィールド説明
challengeサーバーが生成したチャレンジ
rpIdRelying Party ID
allowCredentials許可するクレデンシャル(空 = Discoverable)

FIDO2認証完了​

POST {tenant}/v1/authorizations/{authorization-id}/fido2-authentication
Content-Type: application/json

{
"id": "credential-id",
"rawId": "base64url-encoded-raw-id",
"type": "public-key",
"response": {
"clientDataJSON": "base64url-encoded",
"authenticatorData": "base64url-encoded",
"signature": "base64url-encoded",
"userHandle": "base64url-encoded-user-handle"
}
}

レスポンス(成功):

{
"status": "ok"
}

レスポンス(エラー):

HTTPステータスエラーコード説明
400authentication_failed認証処理に失敗
400invalid_signature署名検証失敗
404credential_not_foundクレデンシャルが見つからない

検証フロー​

idp-serverは認証時に以下の検証を行います。

認証器レスポンス
│
├─1. clientDataJSON検証
│ └── origin, challenge, type
│
├─2. authenticatorData検証
│ └── rpIdHash, UP flag, UV flag
│
├─3. 署名検証
│ └── 登録時の公開鍵で検証
│
├─4. signCount検証
│ └── リプレイ攻撃検出
│
└─5. ユーザー特定
├── userHandle(Discoverable)
└── credentialId(非Discoverable)

signCount(署名カウンター)​

認証器は認証のたびにカウンターをインクリメントします。

前回: 10
今回: 11 → OK
今回: 10 → NG(リプレイ攻撃の可能性)

注意: 一部のプラットフォーム認証器はsignCountを常に0で返します。


セキュリティイベント​

イベントタイプ説明
fido2_authentication_challenge_successチャレンジ取得成功
fido2_authentication_success認証成功
fido2_authentication_failure認証失敗

関連ドキュメント​


最終更新: 2025-01-25