Обзор
Вот основные этапы регистрации ключа доступа:

- Задайте настройки для создания ключа доступа. Отправьте их клиенту, чтобы передать в вызов создания ключа доступа: вызов WebAuthn API
navigator.credentials.createна сайте иcredentialManager.createCredentialна устройстве Android. После того как пользователь подтвердит создание ключа доступа, вызов создания ключа доступа будет разрешен и вернет учетные данныеPublicKeyCredential. - Проверьте учетные данные и сохраните их на сервере.
Ниже приведены подробные инструкции для каждого шага.
Создание параметров создания учетных данных
Первое, что нужно сделать на сервере, – создать объект PublicKeyCredentialCreationOptions.
Для этого используйте серверную библиотеку FIDO. Обычно в ней есть служебная функция, которая позволяет создавать такие объекты. Например, SimpleWebAuthn предлагает generateRegistrationOptions.
PublicKeyCredentialCreationOptions должен включать все, что нужно для создания ключа доступа: информацию о пользователе, о проверяющей стороне и конфигурацию свойств создаваемых учетных данных. Определив все эти параметры, передайте их функции в серверной библиотеке FIDO, которая отвечает за создание объекта PublicKeyCredentialCreationOptions.
Некоторые поля PublicKeyCredentialCreationOptions могут быть константами. Другие должны динамически определяться на сервере:
rpIdЧтобы заполнить идентификатор проверяющей стороны на сервере, используйте функции или переменные на стороне сервера, которые предоставляют имя хоста вашего веб-приложения, напримерexample.com.user.nameиuser.displayName. Чтобы заполнить эти поля, используйте информацию о сеансе пользователя, выполнившего вход (или информацию о новом аккаунте пользователя, если он создает ключ доступа при регистрации).user.name– это обычно адрес электронной почты, уникальный для проверяющей стороны.user.displayName– это удобное название. Обратите внимание, что не все платформы будут использоватьdisplayName.user.id– случайная уникальная строка, которая генерируется при создании аккаунта. В отличие от имени пользователя, которое можно изменить, идентификатор должен быть постоянным. Идентификатор пользователя позволяет определить аккаунт, но не должен содержать информацию, позволяющую идентифицировать личность. Скорее всего, в вашей системе уже есть идентификатор пользователя, но при необходимости создайте его специально для ключей доступа, чтобы в нем не было информации, позволяющей идентифицировать личность.excludeCredentials– список идентификаторов существующих учетных данных, который позволяет избежать дублирования ключа доступа от поставщика. Чтобы заполнить это поле, найдите в базе данных существующие учетные данные пользователя. Подробнее о том, как запретить создание нового ключа доступа, если он уже существует…challenge. При регистрации учетных данных запрос не имеет значения, если вы не используете аттестацию – более продвинутый метод проверки личности поставщика ключа доступа и передаваемых им данных. Однако даже если вы не используете подтверждение, запрос все равно является обязательным полем. Инструкции по созданию безопасного запроса для аутентификации приведены в статье Аутентификация с помощью ключа доступа на стороне сервера.
Кодирование и декодирование
PublicKeyCredentialCreationOptions, отправленный сервером. Бинарные поля, такие как challenge и user.id, кодируются на стороне сервера в base64URL, чтобы параметры можно было передавать в формате JSON по протоколу HTTPS.Стандартный объект WebAuthn PublicKeyCredentialCreationOptions включает поля, которые являются ArrayBuffer, и их нельзя напрямую сериализовать с помощью JSON.stringify(). Чтобы передавать эти параметры по протоколу HTTPS, двоичные поля должны быть закодированы как строки base64URL:
- На сервере библиотека FIDO (например, SimpleWebAuthn) автоматически создает сериализуемый объект JSON (например,
PublicKeyCredentialCreationOptionsJSON) с полями, закодированными в Base64URL. - На стороне клиента современные браузеры поддерживают
PublicKeyCredential.parseCreationOptionsFromJSON()для преобразования параметров JSON непосредственно вPublicKeyCredentialCreationOptions. При использовании клиентских библиотек, таких как@simplewebauthn/browser, функции, подобныеstartRegistration({ optionsJSON: options }), выполняют это преобразование автоматически.
Пример кода: создание параметров создания учетных данных
В наших примерах используется библиотека SimpleWebAuthn. В этом случае мы передаем создание параметров учетных данных открытого ключа функции generateRegistrationOptions.
import {
generateRegistrationOptions,
verifyRegistrationResponse,
} from '@simplewebauthn/server';
import { isoBase64URL, isoUint8Array } from '@simplewebauthn/server/helpers';
router.post('/registerRequest', csrfCheck, sessionCheck, async (req, res) => {
const { user } = res.locals;
// Ensure you nest verification function calls in try/catch blocks.
// If something fails, throw an error with a descriptive error message.
// Return that message with an appropriate error code to the client.
try {
// `excludeCredentials` prevents users from re-registering existing
// credentials for a given passkey provider
const excludeCredentials = [];
const credentials = Credentials.findByUserId(user.id);
if (credentials.length > 0) {
for (const cred of credentials) {
excludeCredentials.push({
id: cred.id,
type: 'public-key',
transports: cred.transports,
});
}
}
// Generate registration options for WebAuthn create
const options = await generateRegistrationOptions({
rpName: process.env.RP_NAME,
rpID: process.env.HOSTNAME,
userID: isoUint8Array.fromUTF8String(user.id),
userName: user.username,
userDisplayName: user.displayName || '',
attestationType: 'none',
excludeCredentials,
authenticatorSelection: {
authenticatorAttachment: 'platform',
residentKey: 'required',
userVerification: 'preferred',
},
});
// Keep the challenge in the session
req.session.challenge = options.challenge;
return res.json(options);
} catch (e) {
console.error(e);
return res.status(400).send({ error: e.message });
}
});
Сохраните открытый ключ
navigator.credentials.create возвращает объект PublicKeyCredential.Если метод navigator.credentials.create успешно выполняется на клиенте, это означает, что ключ доступа создан. Возвращается объект PublicKeyCredential.
Объект PublicKeyCredential содержит объект AuthenticatorAttestationResponse, который представляет ответ поставщика ключа доступа на инструкцию клиента по созданию ключа доступа. Она содержит информацию о новых учетных данных, которые нужны вам как проверяющей стороне, чтобы позже аутентифицировать пользователя. Подробнее AuthenticatorAttestationResponse описан в приложенииAuthenticatorAttestationResponse.
Отправьте объект PublicKeyCredential на сервер. Когда вы его получите, подтвердите его.
Передайте этот шаг проверки библиотеке на стороне сервера FIDO. Обычно для этого используется служебная функция. Например, SimpleWebAuthn предлагает verifyRegistrationResponse. Подробнее о том, что происходит за кулисами, читайте в разделе Приложение. Проверка ответа на регистрацию.
После успешной проверки сохраните информацию об учетных данных в базе данных, чтобы пользователь мог позже пройти аутентификацию с помощью ключа доступа, связанного с этими учетными данными.
Используйте отдельную таблицу для учетных данных открытого ключа, связанных с ключами доступа. У пользователя может быть только один пароль, но несколько ключей доступа, например один синхронизированный через Связку ключей Apple iCloud, а другой – через Google Менеджер паролей.
Вот пример схемы, которую можно использовать для хранения учетных данных:

- Таблица Пользователи:
user_id– основной идентификатор пользователя. Случайный, уникальный и постоянный идентификатор пользователя. Используйте его в качестве первичного ключа для таблицы Users.username– имя пользователя, которое он задал сам и может изменить.passkey_user_id– идентификатор пользователя, не содержащий информацию, позволяющую идентифицировать личность, и связанный с ключом доступа. Он представлен в видеuser.idв параметрах регистрации. Когда пользователь попытается выполнить аутентификацию, программа для аутентификации предоставит это значениеpasskey_user_idв ответе на запрос аутентификации вuserHandle. Мы не рекомендуем использоватьpasskey_user_idв качестве первичного ключа. Первичные ключи часто становятся информацией, позволяющей идентифицировать личность, поскольку они широко используются.
- Таблица учетных данных открытого ключа
id– идентификатор учетных данных. Используйте его в качестве первичного ключа для таблицы Public key credentials.public_key– открытый ключ учетных данных.passkey_user_id– используйте это значение в качестве внешнего ключа, чтобы установить связь с таблицей Пользователи.backed_up– для ключа доступа создана резервная копия, если он синхронизирован поставщиком ключей доступа. Сохранение статуса резервного копирования может пригодиться, если в будущем вы захотите отказаться от паролей для пользователей, у которых есть ключи доступаbacked_up. Чтобы проверить, создана ли резервная копия ключа доступа, посмотрите на флаг BE вauthenticatorDataили воспользуйтесь функцией серверной библиотеки FIDO, которая обычно позволяет легко получить доступ к этой информации. Сохранение права на резервное копирование может быть полезно для решения потенциальных запросов пользователей.name– необязательный параметр, который позволяет пользователям задавать собственные отображаемые названия для учетных данных.transports– массив transports. Хранение транспортов полезно для удобства аутентификации пользователей. Когда способы передачи доступны, браузер может вести себя соответствующим образом и показывать интерфейс, соответствующий способу передачи, который поставщик ключей доступа использует для связи с клиентами, в частности для случаев повторной аутентификации, когдаallowCredentialsне пусто.
Для удобства пользователей можно хранить и другую информацию, например поставщика ключа доступа, время создания учетных данных и время последнего использования. Подробнее о дизайне пользовательского интерфейса для ключей доступа…
Пример кода: сохранение учетных данных
В наших примерах используется библиотека SimpleWebAuthn.
Здесь мы передаем проверку ответа на регистрацию функции verifyRegistrationResponse.
import { isoBase64URL } from '@simplewebauthn/server/helpers';
router.post('/registerResponse', csrfCheck, sessionCheck, async (req, res) => {
const expectedChallenge = req.session.challenge;
const expectedOrigin = getOrigin(req.get('User-Agent'));
const expectedRPID = process.env.HOSTNAME;
const response = req.body;
// This sample code is for registering a passkey for an existing,
// signed-in user
// Ensure you nest verification function calls in try/catch blocks.
// If something fails, throw an error with a descriptive error message.
// Return that message with an appropriate error code to the client.
try {
// Verify the credential
const { verified, registrationInfo } = await verifyRegistrationResponse({
response,
expectedChallenge,
expectedOrigin,
expectedRPID,
requireUserVerification: false,
});
if (!verified) {
throw new Error('Verification failed.');
}
const {
aaguid,
credential,
credentialBackedUp
} = registrationInfo;
// Name the credential based on AAGUID
const name =
aaguid === undefined ||
aaguid === '00000000-0000-0000-0000-000000000000' ?
req.useragent?.platform : aaguids[aaguid]?.name;
const base64PublicKey = isoBase64URL.fromBuffer(credential.publicKey);
// Existing, signed-in user
const { user } = res.locals;
// Save the credential
await Credentials.update({
id: credential.id,
passkey_user_id: user.passkey_user_id,
publicKey: base64PublicKey,
name,
aaguid,
transports: credential.transports || response.response.transports,
backed_up: credentialBackedUp,
registered_at: new Date().getTime()
});
// Kill the challenge for this session
delete req.session.challenge;
return res.json(user);
} catch (e) {
delete req.session.challenge;
console.error(e);
return res.status(400).send({ error: e.message });
}
});
Приложение: AuthenticatorAttestationResponse
AuthenticatorAttestationResponse содержит два важных объекта:
response.clientDataJSON– это JSON-версия данных клиента, которые в интернете представляют собой данные, видимые браузеру. Он содержит источник платформы проверяющей стороны, запрос иandroidPackageName, если клиент – приложение для Android. Как проверяющая сторона, вы можете прочитатьclientDataJSON, чтобы получить доступ к информации, которую браузер видел во время запросаcreate.response.attestationObjectсодержит два типа информации:attestationStatement, которое не имеет значения, если вы не используете аттестацию.authenticatorData– это данные, которые видит поставщик ключа доступа. Как проверяющая сторона, вы можете прочитатьauthenticatorData, чтобы получить доступ к данным, которые видит поставщик ключей доступа и которые возвращаются при запросеcreate.
authenticatorData содержит важную информацию об учетных данных открытого ключа, связанных с новым ключом доступа:
- Учетные данные открытого ключа и их уникальный идентификатор.
- Идентификатор стороны с ограничениями, связанный с учетными данными.
- Пометки, описывающие статус пользователя при создании ключа доступа: присутствовал ли пользователь и был ли он успешно подтвержден (подробнее о userVerification).
- AAGUID – это идентификатор поставщика ключей доступа, например Google Менеджера паролей. По AAGUID можно определить поставщика ключа доступа и показать его название на странице управления ключами доступа. (см. раздел Как определить поставщика ключей доступа с помощью AAGUID).
Хотя authenticatorData вложен в attestationObject, содержащаяся в нем информация необходима для реализации ключа доступа независимо от того, используете ли вы аттестацию. authenticatorData закодирован и содержит поля, закодированные в двоичном формате. Обычно синтаксический анализ и декодирование выполняет серверная библиотека. Если вы не используете серверную библиотеку, попробуйте использовать getAuthenticatorData() на стороне клиента, чтобы сэкономить время на синтаксическом анализе и декодировании на стороне сервера.
Приложение. Проверка ответа на запрос регистрации
Проверка ответа на регистрацию состоит из следующих этапов:
- Убедитесь, что идентификатор проверяющей стороны соответствует вашему сайту.
- Убедитесь, что источник запроса является ожидаемым для вашего сайта (URL основного сайта, приложение для Android).
- Если вы требуете подтверждения пользователя, убедитесь, что для параметра
authenticatorData.uvустановлено значениеtrue. - Обычно флаг присутствия пользователя
authenticatorData.upдолжен иметь значениеtrue, но если учетные данные созданы условно, то значение должно бытьfalse. - Убедитесь, что клиент смог предоставить вызов, который вы ему дали. Если вы не используете аттестацию, эта проверка не имеет значения. Однако мы рекомендуем реализовать эту проверку, чтобы ваш код был готов к использованию подтверждения в будущем.
- Убедитесь, что идентификатор учетных данных ещё не зарегистрирован ни для одного пользователя.
- Убедитесь, что алгоритм, который поставщик ключа доступа использует для создания учетных данных, указан в списке (в каждом поле
algвpublicKeyCredentialCreationOptions.pubKeyCredParams, которое обычно определяется в библиотеке на стороне сервера и не видно вам). Это гарантирует, что пользователи смогут регистрироваться только с помощью разрешенных вами алгоритмов.
Чтобы узнать больше, изучите исходный код SimpleWebAuthn для verifyRegistrationResponse или ознакомьтесь с полным списком проверок в спецификации.