パスキーの管理

このガイドでは、Signal API を使用して、保存された認証情報とユーザー プロフィールの詳細をサーバーとユーザーのパスワード マネージャー間で同期しながら、ユーザーが登録済みのパスキーを表示、名前変更、削除できるようにする方法について説明します。

サーバーサイド オペレーション

バックエンド データベース レイヤとエンドポイントは、登録された認証情報の一般的な CRUD アクションと、ユーザー プロフィールの詳細の更新をサポートする必要があります。フレームワーク固有のライブラリから切り離されたサーバーは、次のエンドポイントを公開します。

  1. すべてのユーザー認証情報を一覧表示する: ログインしているユーザーの ID に一致するすべての StoredPasskeyCredential レコードを取得します。
  2. 認証情報名を更新する: 特定の認証情報 ID の新しいカスタム ニックネームを受け入れ、サーバーで更新を保持します。
  3. 認証情報を削除する: データベースから特定の認証情報 ID を削除します。
  4. ユーザー アカウントの詳細を更新する: ログインしているユーザーのプロファイルの更新された name(ユーザー名)と displayName の値を受け入れ、クライアントがそれらをパスキー プロバイダと同期できるようにします。
// Node.js routing example for credential CRUD and user profile rename
router.get('/api/credentials', checkUserAuthenticated, async (req, res) => {
  const list = await db.findCredentialsByUserId(req.user.id);
  return res.json(list);
});

router.put('/api/credential/:id', checkUserAuthenticated, async (req, res) => {
  const { id } = req.params;
  const { name } = req.body;
  const cred = await db.findCredentialById(id);
  if (!cred || cred.passkeyUserId !== req.user.id) {
    return res.status(404).json({ error: 'Credential not found.' });
  }
  cred.name = name;
  await db.saveCredential(cred);
  return res.json(cred);
});

router.delete('/api/credential/:id', checkUserAuthenticated, async (req, res) => {
  const { id } = req.params;
  const cred = await db.findCredentialById(id);
  if (!cred || cred.passkeyUserId !== req.user.id) {
    return res.status(404).json({ error: 'Credential not found.' });
  }
  await db.deleteCredential(id);
  return res.json({ success: true });
});

router.put('/api/user', checkUserAuthenticated, async (req, res) => {
  const { name, displayName } = req.body;
  const updatedUser = await db.updateUser(req.user.id, { name, displayName });
  return res.json(updatedUser);
});

クライアントサイド管理 UI

専用の設定パネルをレンダリングして、ユーザーが登録済みの認証オプションとアカウント プロフィールの詳細を簡単に監査および管理できるようにします。

  1. 保存済みリストを表示する: エンドポイントからリストを取得し、個々の認証情報の行をレンダリングします。レスポンスが空の場合は、空の状態を示す有用なメッセージ(「パスキーが見つかりません」など)を表示します。
  2. AAGUID メタデータをマッピングする: 各パスキーについて、ローカル レジストリで aaguid プロパティを検索し、プロバイダの詳細をレンダリングします。詳しくは、AAGUID からパスキー プロバイダを特定するをご覧ください。
  3. アイテムごとの UI 要件: リストコンテナ内のすべての行は、以下をレンダリングしなければなりません。
    • プロバイダ アイコン: AAGUID から派生した画像またはデータ URI。
    • プロバイダ / カスタム名: AAGUID から派生した名前、またはユーザーが変更した文字列。
    • 登録日: データベースに保持された未加工のエポック タイムスタンプ registeredAt を、クライアントに表示するために人が読める日付に変換したものです。
    • 最終使用日: データベースに保持された未加工のエポック タイムスタンプ lastUsedAt を、クライアントに表示するために人が読める形式に変換した日付(存在する場合)。
    • 名前変更ボタン: 名前変更アクションをトリガーして、サーバー上の認証情報のカスタム ニックネームを更新します(PUT /api/credential/:id)。注: 個々のパスキーのニックネームの名前変更は、サーバーサイドのラベル変更であり、signalCurrentUserDetails() は呼び出されません。
    • 削除ボタン: 認証情報(DELETE /api/credential/:id)の削除をトリガーし、残りの認証情報 ID を signalAllAcceptedCredentials() 経由で同期します。
  4. ユーザー アカウントの名前変更 UI:
    • ユーザーがアカウントのユーザー名(name)または displayName(PUT /api/user)を更新できるようにするコントロールを提供し、更新されたユーザーの詳細を PublicKeyCredential.signalCurrentUserDetails() を介してパスワード マネージャーとすぐに同期します。
  5. 条件付きの [パスキーを作成] ボタン:
    • 管理ページに、目立つ「パスキーを作成」登録トリガー ボタンを表示します。この UI 要素をレンダリングする前に、ページは PublicKeyCredential.getClientCapabilities() を使用して機能検出を行い、プラットフォーム認証システムがサポートされていることを確認する必要があります。パスキーがサポートされていない場合は、このボタンを非表示にして、代わりに標準の MFA 登録を促します。
    • navigator.credentials.create() 呼び出しで authenticatorSelection.authenticatorAttachment を省略して、セキュリティ キーを登録できるようにしました。

Signal API の同期

Signal API を使用すると、アプリケーションは認証情報の状態をパスワード マネージャーに伝達し、ユーザーの同期されたボルトとバックエンド データベースを同期させることができます。

  • 特徴検出とパラメータ エンコード ルール:
    • Signal API メソッド(if (PublicKeyCredential.signalAllAcceptedCredentials) と if (PublicKeyCredential.signalCurrentUserDetails))を呼び出す前に、必ず機能検出を行います。
    • Signal API メソッド(signalAllAcceptedCredentials、signalCurrentUserDetails)に渡されるすべての userId パラメータと認証情報 ID パラメータは、Base64URL エンコードされた文字列である必要があります。
  • ページ読み込みの同期を開始する:
    • アプリケーションは、管理ページが読み込まれるときに signalAllAcceptedCredentials() を自動的に呼び出す必要があります(例: DOMContentLoaded イベント リスナー、モジュールの初期化、useEffect などのフレームワーク コンポーネント マウントフック内)。
  • 管理更新の同期:
    • アプリケーションは、認証情報の削除クリック ハンドラの取得後に、直ちに signalAllAcceptedCredentials() を呼び出す必要があります。
    • アプリは、ユーザー アカウントの名前変更(name または displayName)のクリック ハンドラで、フェッチ後に直ちに signalCurrentUserDetails() を呼び出さなければなりません。
// Client-side management synchronization ES module
import { listFetch, renameCredentialFetch, updateUserFetch, deleteFetch } from './api.js';

// Base64URL-encoded User ID string (illustration only)
const base64UrlUserId = "M2YPl-KGnA8";

async function syncAcceptedCredentials(currentCredentialsList) {
  if (!window.PublicKeyCredential || !PublicKeyCredential.signalAllAcceptedCredentials) return;
  try {
    const credentialIds = currentCredentialsList.map(c => c.id); // Array of Base64URL credential ID strings
    
    await PublicKeyCredential.signalAllAcceptedCredentials({
      rpId, // RP ID must match the one defined on the server
      userId: base64UrlUserId, // User ID Base64URL-encoded string
      allAcceptedCredentialIds: credentialIds
    });
  } catch (e) {
    console.error('SignalAllAcceptedCredentials sync failure:', e);
  }
}

async function loadManagementPanel() {
  const response = await listFetch();
  const list = await response.json();
  
  renderUI(list);
  // Sync accepted credentials on initial load
  await syncAcceptedCredentials(list);
}

async function performDelete(credentialId) {
  const response = await deleteFetch(credentialId);
  if (response.ok) {
    const updatedResponse = await listFetch();
    const updatedList = await updatedResponse.json();
    
    renderUI(updatedList);
    // Sync remaining accepted credentials after deletion
    await syncAcceptedCredentials(updatedList);
  }
}

// Renaming a passkey's nickname updates the server record only
async function performCredentialRename(credentialId, newCredentialName) {
  const response = await renameCredentialFetch(credentialId, { name: newCredentialName });
  if (response.ok) {
    const updatedResponse = await listFetch();
    renderUI(await updatedResponse.json());
  }
}

// Renaming the user's account username or displayName triggers signalCurrentUserDetails
async function performUserRename(rpId, userId, updatedName, updatedDisplayName) {
  const response = await updateUserFetch({ name: updatedName, displayName: updatedDisplayName });
  if (response.ok && window.PublicKeyCredential && PublicKeyCredential.signalCurrentUserDetails) {
    try {
      await PublicKeyCredential.signalCurrentUserDetails({
        rpId, // RP ID must match the one defined on the server
        userId, // Base64URL-encoded user ID
        name: updatedName, // Updated username
        displayName: updatedDisplayName // Updated display name
      });
    } catch (e) {
      console.error('SignalCurrentUserDetails sync failure:', e);
    }
  }
}

window.addEventListener('DOMContentLoaded', loadManagementPanel);

AAGUID からパスキー プロバイダを特定する

AAGUID(Authenticator Attestation Globally Unique Identifier)は、特定のインスタンスではなく、認証器のモデルを表す 128 ビットの固有識別子です。これは、パスキーの登録時に認証器データに含まれ、どのパスキー プロバイダ(Google パスワード マネージャー、iCloud キーチェーン、1Password など)が認証情報を作成したかを判断するために使用できます。

AAGUID は、ユーザーがパスキーを管理できるようにするためにのみ使用してください。暗号的に証明されていない限り、変更できます。プラットフォーム パスキーは現在、暗号的に証明されていません。

1. AAGUID レジストリ

コミュニティが管理している AAGUID とプロバイダ名およびアイコンの JSON マッピングは、次の場所で入手できます。

https://raw.githubusercontent.com/passkeydeveloper/passkey-authenticator-aaguids/refs/heads/main/combined_aaguid.json

各エントリのスキーマは次のとおりです。

{
  "<aaguid-uuid>": {
    "name": "Provider Name",
    "icon_light": "data:image/png;base64,...",
    "icon_dark": "data:image/png;base64,..."
  }
}

2. 登録後の AAGUID の使用

登録レスポンスを検証したら、登録結果から aaguid を読み取り、レジストリと照合して、認証情報の name と providerIcon を入力します。

レジストリで AAGUID を検索する前に、'00000000-0000-0000-0000-000000000000' と等しいかどうかを確認します。その場合は、レジストリ検索をスキップし、name をフォールバック(ユーザー エージェントのデバイス名や「不明なパスキー プロバイダ」など)に設定し、providerIcon を undefined に設定します。ゼロ以外の AAGUID のレジストリのみを検索します。

import aaguids from './aaguids.json' with { type: 'json' };

const { aaguid } = registrationInfo;
if (aaguid === '00000000-0000-0000-0000-000000000000') {
  // use the device name as the passkey provider based on
  // the information derived from the user agent string,
  // or just say "Unknown passkey provider"
} else {
  const provider = aaguids[aaguid];
  const credential = {
    // ...other fields
    aaguid,
    name: provider?.name || 'Unknown passkey provider',
    providerIcon: provider?.icon_light,
  };
}

代替戦略

パスキー機能検出のフォールバック

常に「webauthn-polyfills」をインストールし、コンテキストにインポートします。PublicKeyCredential がサポートされている限り、PublicKeyCredential.getClientCapabilities もサポートされます。

import 'webauthn-polyfills';

Signal API の同期フォールバック

ブラウザが PublicKeyCredential.parseRequestOptionsFromJSON をサポートしていない場合は、「webauthn-polyfills」を使用します。

<script type="module"> 
  if (!PublicKeyCredential.parseRequestOptionsFromJSON) { 
     await import('https://unpkg.com/webauthn-polyfills'); 
   } 
 </script> 

これにより、PublicKeyCredential.prototype.toJSON のサポートも追加されます。