Skip to content
Published on

Passkeys エンタープライズ導入ガイド — WebAuthn/FIDO2 から Keycloak 統合まで

Authors

はじめに — パスワードの終焉 2026

2026年現在、パスワードは名実ともに「レガシー認証手段」となりました。FIDO Alliance によると、主要なコンシューマーサービス(Google、Apple、Microsoft、Amazon、TikTok など)でのパスキー対応は当たり前になり、すでに数十億のアカウントがパスキーを登録しています。Verizon DBIR レポートが毎年繰り返し指摘しているように、侵害インシデントの大多数は依然として窃取されたクレデンシャル(stolen credentials)とフィッシングから始まります。パスワードが存在する限り、フィッシングはなくなりません。

エンタープライズ環境での計算式はコンシューマーとは少し異なります。単に「ログインが楽になる」のではなく、次の3つが中核的なドライバーです。

  1. フィッシング耐性(phishing resistance): 米国 OMB の Zero Trust 戦略(M-22-09)が連邦機関にフィッシング耐性 MFA を義務化して以降、金融・医療・公共セクターの規制が同じ方向に収束しています。OTP やプッシュ通知型 MFA は中間者フィッシングキット(Evilginx の類)に無力であることが実証されています。
  2. ヘルプデスクコスト: パスワードリセットはヘルプデスクチケットの20〜50%を占める古典的なコスト項目です。パスワードレス化はこのコストを構造的に排除します。
  3. ユーザー体験とコンバージョン: ログイン成功率の向上とログイン時間の短縮は、社内の生産性と顧客のコンバージョンの両方に直接影響します。

本記事では、まず WebAuthn/FIDO2 プロトコルの内部動作を解剖し、Keycloak 26 のパスキー統合設定、段階的ロールアウト戦略、そしてエンタープライズで必ず直面するアカウント復旧と attestation ポリシーの問題まで、実務の観点から扱います。

WebAuthn/FIDO2 アーキテクチャの全体像

FIDO2 は2つの仕様で構成されています。

  • WebAuthn (W3C): ブラウザ/プラットフォームが Web アプリケーションに公開する JavaScript API とデータ構造を定義します。現在の最新は WebAuthn Level 3 です。
  • CTAP2 (FIDO Alliance): クライアント(ブラウザ/OS)と外部認証器(セキュリティキー、スマートフォン)間の通信プロトコルです。

全コンポーネントの関係は次のとおりです。

+------------------+        +---------------------+        +------------------+
|  Relying Party   |        |   Client/Platform   |        |  Authenticator   |
|  (Web サーバー、 | <----> |  (ブラウザ + OS)    | <----> |  (Touch ID、     |
|   Keycloak など) |  HTTPS |                     |  CTAP2 |   YubiKey、      |
|                  |        |  WebAuthn API       |  又は  |   スマートフォン)|
|  challenge 生成  |        |  navigator.         |  内蔵  |                  |
|  署名の検証      |        |  credentials.*      |  API   |  秘密鍵を保管    |
+------------------+        +---------------------+        +------------------+

中核となるアイデアはシンプルです。非対称鍵ペアに基づく challenge-response 認証です。

  • 登録時に認証器が鍵ペアを生成し、公開鍵のみがサーバー(Relying Party、RP)に渡されます。
  • 秘密鍵は認証器(Secure Enclave、TPM、セキュリティキーの secure element)を決して離れません。
  • ログイン時にサーバーがランダムな challenge を送り、認証器が秘密鍵で署名すると、サーバーは公開鍵で検証します。

サーバーには公開鍵しか保存されないため、サーバーの DB が丸ごと流出しても、攻撃者が得られるのは「検証用の公開鍵」だけです。パスワードハッシュの流出とは脅威モデルそのものが異なります。

Registration Ceremony — 登録セレモニーの詳細

WebAuthn 仕様は登録プロセスを ceremony と呼びます。手順が厳格に定義されているためです。

ユーザー        ブラウザ                  RP サーバー           認証器
  |                |                        |                    |
  |  登録開始      |                        |                    |
  |--------------->|  POST /webauthn/begin  |                    |
  |                |----------------------->|                    |
  |                |   PublicKeyCredential  |                    |
  |                |   CreationOptions      |                    |
  |                |   (challenge, rp.id,   |                    |
  |                |    user.id, pubKey     |                    |
  |                |    CredParams ...)     |                    |
  |                |<-----------------------|                    |
  |                |  navigator.credentials.create()             |
  |                |-------------------------------------------->|
  |  生体認証/PIN  |                        |                    |
  |<--------------------------------------------------------------|
  |  ユーザー確認  |                        |                    |
  |--------------------------------------------------------------->|
  |                |   attestationObject + clientDataJSON        |
  |                |<--------------------------------------------|
  |                |  POST /webauthn/finish |                    |
  |                |----------------------->|                    |
  |                |   検証後、公開鍵を保存 |                    |

サーバーが返すオプションオブジェクトの実際の形は次のとおりです。

{
  "challenge": "Y2hhbGxlbmdlLXJhbmRvbS0zMmJ5dGVz",
  "rp": {
    "id": "example.com",
    "name": "Example Corp"
  },
  "user": {
    "id": "dXNlci1pZC1vcGFxdWU",
    "name": "jdoe@example.com",
    "displayName": "Jane Doe"
  },
  "pubKeyCredParams": [
    { "type": "public-key", "alg": -7 },
    { "type": "public-key", "alg": -257 }
  ],
  "authenticatorSelection": {
    "residentKey": "required",
    "userVerification": "required",
    "authenticatorAttachment": "platform"
  },
  "attestation": "none",
  "timeout": 60000,
  "excludeCredentials": []
}

各フィールドの意味を確認していきます。

  • challenge: サーバーが生成する最低16バイト以上の暗号学的乱数です。リプレイ攻撃を防ぐ要であり、必ずサーバーセッションに保存しておき、レスポンス検証時に比較しなければなりません。
  • rp.id: Relying Party 識別子。登録されるクレデンシャルがどのドメインに紐付くかを決定します。フィッシング耐性の根幹なので、後ほど詳しく扱います。
  • user.id: ユーザーを識別する不透明(opaque)なバイト列です。メールアドレスのような PII を入れず、ランダムな UUID を推奨します。一度決めると変更が困難です。
  • pubKeyCredParams: 許可アルゴリズムのリスト。COSE アルゴリズム識別子で、-7 は ES256(ECDSA P-256)、-257 は RS256 です。ES256 を最優先に置くのが標準的な慣行です。
  • residentKey: discoverable credential(旧 resident key)を要求するかどうか。パスキー体験(ID 入力なしのログイン)を望むなら required に設定する必要があります。
  • userVerification: 生体認証/PIN などの「ユーザー本人確認」を要求するかどうか。パスワードレスの1要素ログインであれば required が必須です。
  • excludeCredentials: すでに登録済みのクレデンシャル ID のリスト。同じ認証器の重複登録を防ぎます。

Attestation — 認証器の出自証明

attestation は「この公開鍵が本当に特定のメーカー/モデルの認証器で生成された」ことを暗号学的に証明するメカニズムです。登録レスポンスの attestationObject の中に attestation statement が含まれてきます。

attestation の要求レベルは4種類です。

意味推奨シナリオ
noneattestation 不要(デフォルト)コンシューマーサービス、一般的な社内アプリ
indirect匿名化された attestation を許可ほぼ使われない
directメーカー attestation の原文を要求規制業界、認証器モデルの統制が必要な場合
enterprise個別デバイスの識別が可能(事前合意済み環境)管理デバイス限定の展開

重要な現実をひとつ。synced passkey(iCloud キーチェーン、Google パスワードマネージャー)はほとんどの場合 attestation を提供しません。 direct attestation を強制すると、事実上プラットフォームパスキーをブロックする効果になるため、ポリシー設計時に必ず考慮すべきです。

attestation 検証時、サーバーは attestation statement の証明書チェーンを FIDO Metadata Service(MDS) のメタデータと照合し、認証器の AAGUID(Authenticator Attestation GUID — 認証器モデル識別子)とセキュリティ認証レベル(FIDO L1/L2 など)を確認できます。

Authentication Ceremony — ログインセレモニーの詳細

ログイン手順は登録と対称的です。

ユーザー        ブラウザ                  RP サーバー           認証器
  |                |                        |                    |
  |  ログイン開始  |  POST /webauthn/login/begin                 |
  |--------------->|----------------------->|                    |
  |                |   PublicKeyCredential  |                    |
  |                |   RequestOptions       |                    |
  |                |   (challenge, rpId,    |                    |
  |                |    allowCredentials)   |                    |
  |                |<-----------------------|                    |
  |                |  navigator.credentials.get()                |
  |                |-------------------------------------------->|
  |  生体認証/PIN  |                        |                    |
  |<------------------------------------------------------------|
  |                |   authenticatorData + signature             |
  |                |   + clientDataJSON                          |
  |                |<--------------------------------------------|
  |                |  POST /webauthn/login/finish                |
  |                |----------------------->|                    |
  |                |   署名検証 → セッション|                    |

サーバー側の検証で必ず確認すべき項目です。

  1. challenge の一致: clientDataJSON 内の challenge がサーバーの発行した値と一致するか。
  2. origin の一致: clientDataJSON 内の origin が期待するオリジン(例: 本番ドメインの HTTPS オリジン)と一致するか。
  3. rpIdHash の一致: authenticatorData の先頭32バイトが rp.id の SHA-256 ハッシュと一致するか。
  4. UP/UV フラグ: User Present、User Verified フラグがポリシーに適合するか。
  5. 署名の検証: 登録時に保存した公開鍵で署名が有効か。
  6. signCount: 署名カウンターが前回値より増加しているか(複製認証器の検知)。ただし synced passkey はカウンターが常に0という実装が多いため、「増加しなければブロック」ではなく「異常シグナルとしてログ」程度に扱うのが現実的です。

Synced Passkey vs Device-bound — 何が違うのか

「パスキー」という用語はマーケティング的には discoverable WebAuthn credential 全般を指しますが、実務では同期の有無によってセキュリティ特性が大きく分かれます。

区分Synced PasskeyDevice-bound Credential
保存場所クラウドキーチェーン(iCloud、Google PM、1Password など)単一デバイスのセキュアハードウェア
デバイス紛失時他のデバイスから復旧可能クレデンシャル永久喪失
attestation一般的になしメーカー attestation が可能
複製可能性クラウドアカウントのセキュリティに依存ハードウェア的に不可能
代表例iPhone/Android のパスキーYubiKey、管理デバイスの TPM 鍵
適合シナリオ一般従業員、コンシューマー特権アカウント、規制ワークロード

もうひとつの軸は認証器の形態です。

区分Platform AuthenticatorRoaming Authenticator
形態デバイス内蔵(Touch ID、Windows Hello、Android)外付け(USB/NFC/BLE セキュリティキー)
CTAP 通信OS 内部 APICTAP2 over USB/NFC/BLE
クロスデバイスhybrid transport(QR + BLE 近接確認)で可能キーを物理的に挿せばよい
コスト0円(デバイスに含まれる)キー購入コスト

エンタープライズポリシーの典型的な組み合わせは次のとおりです。

  • 一般従業員: synced passkey を許可(UX と復旧の容易さを優先)
  • 管理者/特権アカウント: device-bound セキュリティキー、または管理デバイスの platform authenticator のみ許可(attestation + AAGUID ポリシーで強制)

フィッシング耐性の原理 — Origin Binding

パスキーが OTP やプッシュ MFA と決定的に異なるのが origin binding です。

OTP フィッシングのシナリオを思い浮かべてください。攻撃者が本物そっくりの偽サイトを作り、ユーザーが入力したパスワードと OTP をリアルタイムで本物のサイトに中継(relay)します。ユーザーには6桁の数字が「どのサイトのためのものか」を区別する手段がないため、騙されるしかありません。

WebAuthn では、この中継がプロトコルレベルで遮断されます。

  1. クレデンシャルは登録時に rp.id(ドメイン)にバインドされます。evil-example.com から example.com のクレデンシャルを要求しても、ブラウザが rp.id 検証の段階で拒否します。偽サイトでは、そもそも署名自体が生成されません。
  2. 署名対象に clientDataJSON が含まれ、その中にブラウザが直接記録した origin が入ります。仮に何らかの経路で署名を入手できても、サーバーが origin フィールドを検証した瞬間に偽の出所は露呈します。
  3. challenge が署名に含まれるため、リプレイも不可能です。

つまり「ユーザーが注意して」フィッシングを防ぐのではなく、ブラウザとプロトコルが数学的に防いでくれます。これがフィッシング耐性 MFA の定義であり、NIST SP 800-63B の AAL3 の議論で hardware-based phishing-resistant authenticator が言及される理由です。

Keycloak 26 のパスキー統合

Keycloak は以前から WebAuthn をサポートしてきましたが、26.x に入ってパスキー体験がファーストクラス市民になりました。特に 26.4 からログインフォームにパスキーが統合され、26.6(2026年時点の最新は 26.6.2)では conditional UI ベースの滑らかなフローが標準提供されています。

2つの UI モード

  • Conditional UI(オートフィル): ID 入力欄にフォーカスが当たると、ブラウザがそのサイトに登録されたパスキーをオートフィルの形で提案します。ユーザーは ID/パスワードを入力する必要がなく、リストから選択するだけです。WebAuthn の conditional mediation 機能を使用します。
  • Modal UI: 「パスキーでログイン」ボタンを押すと、モーダルダイアログで認証器選択画面が表示される明示的なフローです。

Realm の設定

Admin Console 上のパスは次のとおりです。

  1. Authentication → Policies → WebAuthn Passwordless Policy でパスワードレス用ポリシーを設定します。(通常の WebAuthn Policy は2要素用で、Passwordless Policy がパスキー用です。)
  2. Realm Settings → Login でパスキー関連オプションを有効化します。

kcadm CLI でポリシーを設定する例です。

# WebAuthn Passwordless ポリシーの設定
/opt/keycloak/bin/kcadm.sh update realms/myrealm \
  -s 'webAuthnPolicyPasswordlessRpEntityName=Example Corp' \
  -s 'webAuthnPolicyPasswordlessRpId=example.com' \
  -s 'webAuthnPolicyPasswordlessRequireResidentKey=Yes' \
  -s 'webAuthnPolicyPasswordlessUserVerificationRequirement=required' \
  -s 'webAuthnPolicyPasswordlessAttestationConveyancePreference=none' \
  -s 'webAuthnPolicyPasswordlessSignatureAlgorithms=["ES256","RS256"]' \
  -s 'webAuthnPolicyPasswordlessAuthenticatorAttachment=not specified' \
  -s 'webAuthnPolicyPasswordlessCreateTimeout=60'

ポイントは3つです。

  • RequireResidentKey = Yes: discoverable credential を強制しないと、conditional UI でクレデンシャルが検索されません。
  • UserVerificationRequirement = required: 1要素のパスワードレスなので本人確認が必須です。
  • RpId: 最上位ドメイン(example.com)に設定すると、サブドメイン(sso.example.com、app.example.com)全体でクレデンシャルを共有できます。ただし一度決めると、変更時に既存クレデンシャルがすべて無効化されるため、慎重に決定すべきです。

認証フローの構成

passkey-first のログインフローを作るには、browser flow を複製して次のように構成します。

Browser Flow (複製: browser-passkeys)
├── Cookie                                  [Alternative]
├── Identity Provider Redirector            [Alternative]
└── Passkeys Forms (subflow)                [Alternative]
    ├── Username/WebAuthn Form              [Required]
    │     └─ conditional UI でパスキーをオートフィル提案
    └── Password + OTP (subflow)            [Conditional - フォールバック]
          ├── Password Form                 [Required]
          └── OTP Form                      [Conditional]

26.6 では標準提供のパスキー統合ログインフォームのおかげで、特別なカスタマイズなしに上記と同様の体験を有効化できます。詳細は Keycloak リリースノートを参照してください。

ユーザー登録フロー

既存ユーザーにパスキーを登録させるには Required Action を使います。

# 特定ユーザーにパスキー登録の required action を付与
/opt/keycloak/bin/kcadm.sh update users/USER-ID -r myrealm \
  -s 'requiredActions=["webauthn-register-passwordless"]'

または Account Console(最近のデフォルトテーマ)で、ユーザー自身が Signing in → Passkeys メニューから登録できます。

フロントエンドコード例 — 自前で RP を実装する場合

Keycloak を使えば以下のコードを自分で書くことはほぼありませんが、自前の RP を実装する場合や動作を理解するためには知っておく必要があります。

登録(Registration)

async function registerPasskey() {
  // 1. サーバーからオプションを取得
  const resp = await fetch('/webauthn/register/begin', { method: 'POST' });
  const options = await resp.json();

  // 2. base64url → ArrayBuffer 変換
  options.challenge = base64urlToBuffer(options.challenge);
  options.user.id = base64urlToBuffer(options.user.id);
  options.excludeCredentials = (options.excludeCredentials || []).map((c) => ({
    ...c,
    id: base64urlToBuffer(c.id),
  }));

  // 3. 認証器を呼び出し — OS の生体認証 UI が表示される
  const credential = await navigator.credentials.create({ publicKey: options });

  // 4. 結果をサーバーへ送信
  await fetch('/webauthn/register/finish', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      id: credential.id,
      rawId: bufferToBase64url(credential.rawId),
      type: credential.type,
      response: {
        clientDataJSON: bufferToBase64url(credential.response.clientDataJSON),
        attestationObject: bufferToBase64url(credential.response.attestationObject),
      },
    }),
  });
}

Conditional UI ログイン

async function conditionalLogin() {
  // ブラウザが conditional mediation をサポートしているか確認
  if (
    !window.PublicKeyCredential ||
    !(await PublicKeyCredential.isConditionalMediationAvailable())
  ) {
    return; // フォールバック: 通常のフォームログイン
  }

  const resp = await fetch('/webauthn/login/begin', { method: 'POST' });
  const options = await resp.json();
  options.challenge = base64urlToBuffer(options.challenge);

  // mediation: 'conditional' — モーダルではなくオートフィルでパスキーを提案
  const assertion = await navigator.credentials.get({
    publicKey: options,
    mediation: 'conditional',
  });

  await fetch('/webauthn/login/finish', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      id: assertion.id,
      rawId: bufferToBase64url(assertion.rawId),
      response: {
        clientDataJSON: bufferToBase64url(assertion.response.clientDataJSON),
        authenticatorData: bufferToBase64url(assertion.response.authenticatorData),
        signature: bufferToBase64url(assertion.response.signature),
        userHandle: assertion.response.userHandle
          ? bufferToBase64url(assertion.response.userHandle)
          : null,
      },
    }),
  });
}

conditional UI を使うには、HTML の入力欄に autocomplete ヒントが必要です。

<input type="text" name="username" autocomplete="username webauthn" />

段階的ロールアウト戦略 — MFA からパスワードレスへ

ビッグバン移行は必ず失敗します。推奨する段階別戦略は次のとおりです。

Phase 0          Phase 1            Phase 2              Phase 3
パイロット       2要素として導入   passkey-first        passwordless
─────────        ─────────────      ──────────────       ─────────────
IT/セキュリティ  パスワード +       conditional UI で    パスワードを
チームと志願者   passkey(2FA)、     パスキーを優先提案   削除または無効化
グループ         OTP の置換開始     パスワードは         フォールバックは
                                    フォールバック       復旧フローのみ
  • Phase 0(パイロット、2〜4週間): IT/セキュリティチームと志願者グループから始めます。ブラウザ/OS マトリクス(古い Windows、仮想デスクトップ、キオスク環境)で詰まるポイントを見つけるのが目的です。
  • Phase 1(2要素として共存): OTP を置き換える2つ目の要素としてパスキーを導入します。ユーザーが慣れる時間を与えながら登録率(enrollment rate)を上げます。この段階の主要指標はアクティブユーザーに対するパスキー登録率です。
  • Phase 2(passkey-first): ログイン画面でパスキーをデフォルト経路にし、パスワードをフォールバックにします。Keycloak の conditional UI がこの段階で真価を発揮します。
  • Phase 3(passwordless): 登録率が十分に高くなったら(経験則では90%以上)、パスワード認証を無効化します。この時点で復旧フローの堅牢さが全体のセキュリティレベルを決定します。

各段階でモニタリングすべき指標: 登録率、パスキーログイン成功率、フォールバック使用率、ヘルプデスクチケットの推移。

アカウント復旧 — パスワードレスのアキレス腱

パスワードをなくすと「パスワードを忘れました」は消えますが、「デバイスをなくしました」がその座を占めます。復旧経路がフィッシング可能であれば、システム全体のセキュリティは復旧経路のレベルまで低下します。攻撃者は常に最も弱い輪を狙います。

推奨する原則は次のとおりです。

  1. 複数クレデンシャル登録をデフォルトに: オンボーディング時に最低2つ(例: ノート PC の platform authenticator + スマートフォン、またはパスキー + バックアップセキュリティキー)の登録をポリシーで強制します。synced passkey はそれ自体が復旧手段になります。
  2. 復旧もフィッシング耐性を持たせる: メールのマジックリンクや SMS で復旧を許可すると、フィッシング耐性の全体が崩壊します。社内環境であれば、ヘルプデスクでの対面/ビデオによる本人確認 + 一時登録トークンの発行が定石です。
  3. 高価値アカウントはより厳格に: 管理者アカウントの復旧には2人承認(four-eyes)手続きを推奨します。
  4. オフボーディングと連動: 退職/デバイス返却時に該当クレデンシャルを即座に無効化するプロセスを IGA(Identity Governance)とつなぎます。

Keycloak では、復旧シナリオのために一時的な required action の付与 + 既存クレデンシャルの削除を API で自動化できます。

# 紛失デバイスのクレデンシャルを削除
/opt/keycloak/bin/kcadm.sh delete \
  users/USER-ID/credentials/CREDENTIAL-ID -r myrealm

# 再登録の required action を付与
/opt/keycloak/bin/kcadm.sh update users/USER-ID -r myrealm \
  -s 'requiredActions=["webauthn-register-passwordless"]'

企業ポリシー — Attestation 検証と AAGUID 許可リスト

規制業界や特権アカウントには「どの認証器でも OK」は通用しません。そこで登場するのが attestation 検証と AAGUID フィルタリングです。

AAGUID は認証器モデルごとの128ビット識別子です。たとえば特定のハードウェアキー製品群や特定のプラットフォームパスキー提供者を AAGUID で区別できます。FIDO MDS から AAGUID ごとのメタデータ(認証レベル、既知の脆弱性の有無)を取得し、検証パイプラインに統合します。

ポリシー設計の例です。

ユーザーグループattestation許可する認証器備考
一般従業員noneすべてのパスキーUX 優先
開発者(本番アクセス)direct会社支給キー + 管理デバイスの platformAAGUID 許可リスト
インフラ管理者directFIDO L2 認証のハードウェアキーのみMDS メタデータ検証

Keycloak の WebAuthn Passwordless Policy で Acceptable AAGUIDs に許可リストを登録すると、リスト外の認証器の登録が拒否されます。注意点として、attestation conveyance を none にすると AAGUID がゼロで送られてくることがあり、フィルタリングが無力化されます。AAGUID ポリシーを使うなら、attestation を direct に併せて引き上げる必要があります。

導入時のよくある失敗(アンチパターン)

  1. rp.id を狭く設定して開始: sso.example.com で登録を始めて、後から example.com 全体に拡張しようとすると、すべてのクレデンシャルを再登録しなければなりません。最初から登録可能な最も広い有効ドメインを検討してください。
  2. userVerification を discouraged のままパスワードレス運用: UP(存在確認)だけで1要素ログインを許可すると、「デバイスを拾った人」がログインできてしまいます。パスワードレスには必ず required。
  3. synced passkey に direct attestation を強制: プラットフォームパスキーがすべて登録失敗し、導入自体が頓挫します。ユーザーグループごとにポリシーを分離してください。
  4. signCount 不一致で無条件ブロック: synced passkey はカウンターを0で送る実装が多いです。ブロックではなくリスクシグナルとしてログし、他のシグナルと組み合わせてください。
  5. 復旧経路を SMS/メールで開放: フィッシング耐性 MFA を導入しておきながら、復旧をフィッシング可能なチャネルで開けておくのは、鍵をかけて窓を開けておくようなものです。
  6. excludeCredentials の未設定: 同じ認証器を重複登録できてしまい、ユーザーのクレデンシャル一覧が乱雑になって混乱が生じます。
  7. challenge の再利用または検証の省略: challenge をステートレスに処理しようとして検証をスキップすると、リプレイ攻撃に無防備になります。必ずサーバー側の状態(セッション/キャッシュ)と照合してください。
  8. iframe/cross-origin コンテキストの未考慮: 埋め込み型ログインウィジェットで WebAuthn 呼び出しがブロックされることがあります。Permissions Policy(publickey-credentials-get)を点検してください。
  9. 登録率の指標なしで Phase 3 に突入: 登録率60%でパスワードを切ると、ヘルプデスクが麻痺します。データに基づいて切り替え時期を決めてください。

おわりに

パスキー導入は技術的には「WebAuthn API の連携」ですが、実際にはポリシー設計と変更管理のプロジェクトです。プロトコル自体(origin binding、challenge-response、attestation)はすでに十分に成熟しており、Keycloak 26 のようなオープンソース IdP が conditional UI まで標準提供する2026年の時点で、技術的な障壁はほぼ消えました。

残された課題は組織のものです。どのユーザーグループにどの認証器を許可するか、復旧手続きをどうフィッシング耐性のあるものに設計するか、登録率をどう引き上げるか — 本記事の段階別戦略とアンチパターンのリストが、その旅路の地図になれば幸いです。

次の記事では、認証の次のステップである認可(authorization)モデルの進化 — RBAC、ABAC、ReBAC と OpenFGA を扱います。

参考資料