В этом руководстве рассказывается, как разрешить пользователям просматривать, переименовывать и удалять зарегистрированные ключи доступа, синхронизируя сохраненные учетные данные и сведения профиля пользователя между сервером и менеджерами паролей пользователя с помощью Signal API.
Операции на стороне сервера
Ваш уровень базы данных и конечные точки ДОЛЖНЫ поддерживать стандартные действия CRUD для зарегистрированных учетных данных, а также обновление сведений в профиле пользователя. Сервер не зависит от библиотек фреймворков и предоставляет конечные точки для следующих действий:
- Получение списка всех учетных данных пользователя. Получите все записи
StoredPasskeyCredential, соответствующие идентификатору пользователя, выполнившего вход. - Обновление названий учетных данных. Принимайте новые псевдонимы для определенных идентификаторов учетных данных и сохраняйте изменения на сервере.
- Удалить учетные данные. Удаление из базы данных определенного идентификатора учетных данных.
- Обновление данных аккаунта пользователя. Принимайте обновленные значения
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);
});
Клиентский интерфейс управления
Создайте специальную панель настроек, чтобы пользователи могли легко проверять и изменять зарегистрированные способы аутентификации и данные профиля аккаунта.
- Показ сохраненного списка. Получение списка из конечной точки и отображение отдельных строк с учетными данными. Если ответ пустой, покажите полезное сообщение (например, "Ключи доступа не найдены").
- Сопоставление метаданных AAGUID. Для каждого ключа доступа найдите его свойство
aaguidв локальном реестре, чтобы отобразить сведения о поставщике. Подробнее о том, как определить поставщика ключа доступа по AAGUID… - Требования к интерфейсу для каждого элемента. В каждой строке внутри контейнера списка ДОЛЖНЫ отображаться:
- Значок поставщика. Изображение, полученное на основе AAGUID, или URI данных.
- Поставщик/специальное название. Название, полученное на основе AAGUID, или строка, переименованная пользователем.
- Дата регистрации: Необработанная временная метка
registeredAt, сохраненная в базе данных и преобразованная в человекочитаемый формат для отображения на стороне клиента. - Дата последнего использования. Необработанная временная метка эпохи
lastUsedAt, сохраненная в базе данных и преобразованная в человекочитаемый формат (если доступна) для показа клиенту. - Кнопка "Переименовать". Запускает действие по переименованию, чтобы обновить псевдоним учетных данных на сервере (
PUT /api/credential/:id). Примечание. Изменение псевдонима отдельного ключа доступа – это изменение ярлыка на стороне сервера, которое НЕ вызываетsignalCurrentUserDetails(). - Кнопка удаления. Запускает удаление учетных данных (
DELETE /api/credential/:id) и синхронизирует оставшиеся идентификаторы учетных данных черезsignalAllAcceptedCredentials().
- Интерфейс переименования аккаунта пользователя:
- Предоставьте пользователю возможность изменить имя пользователя (
name) илиdisplayName(PUT /api/user) и сразу же синхронизируйте обновленные данные с менеджером паролей черезPublicKeyCredential.signalCurrentUserDetails().
- Предоставьте пользователю возможность изменить имя пользователя (
- Условная кнопка "Создать ключ доступа"
- Добавьте на страницу управления заметную кнопку "Создать ключ доступа". Прежде чем показывать этот элемент интерфейса, страница ДОЛЖНА определить возможности с помощью
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.
- Прежде чем вызывать методы Signal API, всегда проверяйте, поддерживаются ли они (
- Запуск синхронизации при загрузке страницы
- Приложение ДОЛЖНО автоматически вызывать
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.