Управление ключами доступа

В этом руководстве рассказывается, как разрешить пользователям просматривать, переименовывать и удалять зарегистрированные ключи доступа, синхронизируя сохраненные учетные данные и сведения профиля пользователя между сервером и менеджерами паролей пользователя с помощью Signal API.

Операции на стороне сервера

Ваш уровень базы данных и конечные точки ДОЛЖНЫ поддерживать стандартные действия CRUD для зарегистрированных учетных данных, а также обновление сведений в профиле пользователя. Сервер не зависит от библиотек фреймворков и предоставляет конечные точки для следующих действий:

  1. Получение списка всех учетных данных пользователя. Получите все записи StoredPasskeyCredential, соответствующие идентификатору пользователя, выполнившего вход.
  2. Обновление названий учетных данных. Принимайте новые псевдонимы для определенных идентификаторов учетных данных и сохраняйте изменения на сервере.
  3. Удалить учетные данные. Удаление из базы данных определенного идентификатора учетных данных.
  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);
});

Клиентский интерфейс управления

Создайте специальную панель настроек, чтобы пользователи могли легко проверять и изменять зарегистрированные способы аутентификации и данные профиля аккаунта.

  1. Показ сохраненного списка. Получение списка из конечной точки и отображение отдельных строк с учетными данными. Если ответ пустой, покажите полезное сообщение (например, "Ключи доступа не найдены").
  2. Сопоставление метаданных AAGUID. Для каждого ключа доступа найдите его свойство aaguid в локальном реестре, чтобы отобразить сведения о поставщике. Подробнее о том, как определить поставщика ключа доступа по AAGUID…
  3. Требования к интерфейсу для каждого элемента. В каждой строке внутри контейнера списка ДОЛЖНЫ отображаться:
    • Значок поставщика. Изображение, полученное на основе AAGUID, или URI данных.
    • Поставщик/специальное название. Название, полученное на основе AAGUID, или строка, переименованная пользователем.
    • Дата регистрации: Необработанная временная метка registeredAt, сохраненная в базе данных и преобразованная в человекочитаемый формат для отображения на стороне клиента.
    • Дата последнего использования. Необработанная временная метка эпохи lastUsedAt, сохраненная в базе данных и преобразованная в человекочитаемый формат (если доступна) для показа клиенту.
    • Кнопка "Переименовать". Запускает действие по переименованию, чтобы обновить псевдоним учетных данных на сервере (PUT /api/credential/:id). Примечание. Изменение псевдонима отдельного ключа доступа – это изменение ярлыка на стороне сервера, которое НЕ вызывает signalCurrentUserDetails().
    • Кнопка удаления. Запускает удаление учетных данных (DELETE /api/credential/:id) и синхронизирует оставшиеся идентификаторы учетных данных через signalAllAcceptedCredentials().
  4. Интерфейс переименования аккаунта пользователя:
    • Предоставьте пользователю возможность изменить имя пользователя (name) или displayName (PUT /api/user) и сразу же синхронизируйте обновленные данные с менеджером паролей через PublicKeyCredential.signalCurrentUserDetails().
  5. Условная кнопка "Создать ключ доступа"
    • Добавьте на страницу управления заметную кнопку "Создать ключ доступа". Прежде чем показывать этот элемент интерфейса, страница ДОЛЖНА определить возможности с помощью PublicKeyCredential.getClientCapabilities(), чтобы проверить, поддерживается ли аутентификатор платформы. Если ключи доступа не поддерживаются, скройте эту кнопку и предложите пользователю стандартные способы регистрации для многофакторной аутентификации.
    • Разрешить регистрацию электронного ключа, пропустив authenticatorSelection.authenticatorAttachment при вызове navigator.credentials.create().

Синхронизация Signal API

Signal API позволяет приложению передавать менеджерам паролей информацию о статусе учетных данных, чтобы синхронизировать хранилища пользователя и вашу базу данных.

  • Правило распознавания функций и кодирования параметров:
    • Прежде чем вызывать методы Signal API, всегда проверяйте, поддерживаются ли они (if (PublicKeyCredential.signalAllAcceptedCredentials) и if (PublicKeyCredential.signalCurrentUserDetails)).
    • Все параметры userId и идентификаторы учетных данных, передаваемые методам Signal API (signalAllAcceptedCredentials, signalCurrentUserDetails), ДОЛЖНЫ быть строками, закодированными по стандарту Base64URL.
  • Запуск синхронизации при загрузке страницы
    • Приложение ДОЛЖНО автоматически вызывать signalAllAcceptedCredentials() при загрузке страницы управления (например, внутри прослушивателя событий DOMContentLoaded, инициализации модуля или хука монтирования компонента фреймворка, такого как useEffect).
  • Синхронизация обновлений управления:
    • Приложение ДОЛЖНО вызвать signalAllAcceptedCredentials() сразу после получения ответа в обработчике нажатия кнопки удаления учетных данных.
    • Приложение ДОЛЖНО вызывать signalCurrentUserDetails() сразу после получения данных в обработчике кликов для переименования аккаунта пользователя (name или displayName).
// 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 (глобальный уникальный идентификатор аттестации аутентификатора) – это 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.