PublicKeyCredential в веб-приложениях
Дата публикации: 2023-05-19Этот пример показывает аутентификацию пользователя через PublicKeyCredential из Web API. Оба слоя демонстрации написаны на JavaScript; исходники доступны в flancer64/demo-webauthn-pubkey, а демонстрация — по адресу pk.auth.demo.teqfw.com.
Участники WebAuthn
Во взаимодействии участвуют пользователь, браузер, backend (Relying Party), хранилище и аутентификатор. Аутентификатор — физическое устройство или программа с асимметричной парой ключей, доступная браузеру: встроенный biometric/PIN-модуль устройства, security key или виртуальный authenticator для разработки.
В Chrome виртуальный authenticator включается в DevTools → More tools → WebAuthn. Он удобен для изучения потока, но не заменяет тестирование на реальных устройствах и браузерах.
Два процесса: attestation и assertion
Attestation регистрирует credential и публичный ключ на сервере:
- браузер запрашивает challenge для регистрации;
- сервер создаёт случайный challenge и сохраняет его;
- браузер просит authenticator создать ключи или использовать существующие;
- пользователь подтверждает действие PIN-кодом, отпечатком или Face ID;
- браузер отправляет attestation-данные серверу;
- сервер проверяет данные и связывает публичный ключ с пользователем.
Assertion подтверждает владение уже зарегистрированным credential:
- браузер запрашивает challenge для входа;
- сервер создаёт и сохраняет challenge;
- authenticator подписывает его приватным ключом после проверки пользователя;
- браузер передаёт assertion серверу;
- сервер проверяет подпись ранее сохранённым публичным ключом.
Приватный ключ не покидает authenticator. Сервер хранит только credential ID, публичный ключ и метаданные, достаточные для проверки.
Регистрация ключа
Сервер генерирует криптографически случайный challenge, например 32 байта через crypto.randomBytes, хранит его с ограниченным временем жизни и передаёт браузеру в base64url-представлении вместе с данными пользователя.
const attestation = await navigator.credentials.create({
publicKey: {
rp: { name: 'WebAuthn Demo' },
user: { id: userIdBytes, name: email, displayName: email },
challenge: challengeBytes,
pubKeyCredParams: [{ type: 'public-key', alg: -7 }],
authenticatorSelection: {
authenticatorAttachment: 'platform',
userVerification: 'preferred',
},
},
});
Бинарные attestationObject и clientDataJSON передаются на сервер в base64url. Сервер обязан проверить challenge, origin, rpId, тип операции, флаги user verification и формат attestation. Из данных извлекается публичный ключ, например в JWK, и сохраняется вместе с credential ID. Библиотеки cbor и parse-cosekey помогают разобрать демонстрационные данные.
Вход по assertion
Для входа backend создаёт новый одноразовый challenge для нужного credential. Браузер вызывает:
const assertion = await navigator.credentials.get({
publicKey: {
challenge: challengeBytes,
allowCredentials: [{ id: credentialIdBytes, type: 'public-key', transports: ['internal'] }],
},
});
На сервер отправляются authenticatorData, clientDataJSON и signature. Проверка должна сопоставить сохранённый challenge, origin и rpId, убедиться в корректности флагов, проверить подпись публичным ключом и обработать signature counter в соответствии с политикой продукта. После этого приложение создаёт собственную сессию — WebAuthn не заменяет управление сессиями и авторизацию.
Практические требования
- WebAuthn требует secure context (HTTPS, кроме localhost) и корректного RP ID.
- Challenges должны быть случайными, одноразовыми, короткоживущими и привязанными к операции.
- Нужно поддержать несколько credential, восстановление доступа и понятный запасной сценарий, иначе потеря устройства заблокирует пользователя.
- Нельзя доверять данным от браузера до полной серверной проверки.
- Для production лучше опираться на актуальную библиотеку WebAuthn и спецификацию, а не копировать учебный парсер.
Вывод
PublicKeyCredential позволяет построить вход без пароля, где секрет остаётся в authenticator, а сервер проверяет криптографическое доказательство. Демонстрация полезна для понимания обмена challenge и credential. В реальном продукте важны также UX регистрации, управление ключами, безопасное восстановление и строгая проверка на сервере. Wiredgeese может спроектировать и внедрить такой поток в существующее веб-приложение.
Дополнительные фрагменты исходного кода
} * Demo_Back_Web_Api_Sign_Up * Fl32_Auth_Back_Mod_PubKey.attestChallengeCreate
/** @type {PublicKeyCredential} */
const attestation = await navigator.credentials.create({publicKey});
{
},
},
{
} ],
} } } * Demo_Front_Ui_Route_Sign_Up * Fl32_Auth_Front_Mod_PubKey.composeOptPkCreate
{
} } * Demo_Front_Ui_Route_Sign_Up * Fl32_Auth_Front_Mod_PubKey.attest * Fl32_Auth_Back_Web_Api_Attest
{
} On the frontend, the attestation ID (DDn…6XA) is stored in local storage for later use in assertions.
{“attestationId”: “DDn8LhxnQB8g7qNKngMy-noDzSDIOyUMGg2soOeS6XA”} * Demo_Front_Ui_Route_Sign_In * Fl32_Auth_Front_Mod_PubKey.assertChallenge * Fl32_Auth_Back_Web_Api_Assert_Challenge
{
} * Fl32_Auth_Back_Web_Api_Assert_Challenge * Fl32_Auth_Back_Mod_PubKey.assertChallengeCreate
/** @type {PublicKeyCredential} */
const assertion = await navigator.credentials.get({publicKey});
{
{
] } ] } } * Demo_Front_Ui_Route_Sign_In * Fl32_Auth_Front_Mod_PubKey.composeOptPkGet
{
} * Demo_Front_Ui_Route_Sign_In * Fl32_Auth_Front_Mod_PubKey.validate * Fl32_Auth_Back_Web_Api_Assert_Validate
{
} Then, retrieve the user’s public key from the database for the corresponding challenge (refer to step 2).