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

- Определите параметры для создания ключа доступа. Отправьте их клиенту, чтобы затем передать их в вызов для создания ключа доступа: вызов API WebAuthn
navigator.credentials.createв веб-версии иcredentialManager.createCredentialв Android. После того, как пользователь подтвердит создание ключа доступа, вызов для создания ключа доступа будет выполнен и вернет учётные данныеPublicKeyCredential. - Проверьте учетные данные и сохраните их на сервере.
В следующих разделах подробно рассматриваются особенности каждого этапа.
Создать параметры создания учетных данных
Первый шаг, который необходимо сделать на сервере, — создать объект PublicKeyCredentialCreationOptions .
Для этого воспользуйтесь серверной библиотекой FIDO. Обычно она предлагает вспомогательную функцию, которая может создать эти параметры автоматически. SimpleWebAuthn предлагает, например, generateRegistrationOptions .
PublicKeyCredentialCreationOptions должен включать всё необходимое для создания ключа доступа: информацию о пользователе, о проверяющей стороне (RP) и конфигурацию свойств создаваемых учётных данных. После определения всех этих параметров передайте их при необходимости функции в серверной библиотеке FIDO, которая отвечает за создание объекта PublicKeyCredentialCreationOptions .
Некоторые поля PublicKeyCredentialCreationOptions могут быть константами. Другие должны динамически определяться на сервере:
-
rpId: Чтобы заполнить идентификатор RP на сервере, используйте серверные функции или переменные, которые предоставляют вам имя хоста вашего веб-приложения, напримерexample.com. -
user.nameиuser.displayName: для заполнения этих полей используйте данные сеанса вошедшего в систему пользователя (или данные новой учётной записи, если пользователь создаёт ключ доступа при регистрации).user.name— это обычно адрес электронной почты, уникальный для RP.user.displayName— удобное для пользователя имя. Обратите внимание, чтоdisplayNameподдерживается не на всех платформах. -
user.id— случайная уникальная строка, генерируемая при создании учётной записи. Она должна быть постоянной, в отличие от имени пользователя, которое можно редактировать. Идентификатор пользователя идентифицирует учётную запись, но не должен содержать никакой персональной информации (PII) . Вероятно, в вашей системе уже есть идентификатор пользователя, но при необходимости создайте его специально для паролей, чтобы защитить его от любых персональных данных. -
excludeCredentials: список идентификаторов существующих учётных данных для предотвращения дублирования ключа доступа от поставщика ключа доступа. Чтобы заполнить это поле, найдите в базе данных существующие учётные данные этого пользователя. Подробнее см. в разделе «Запретить создание нового ключа доступа, если он уже существует» . -
challenge: для регистрации учётных данных запрос не имеет значения, если вы не используете аттестацию — более продвинутый метод проверки личности поставщика ключа доступа и передаваемых им данных. Однако даже без аттестации запрос остаётся обязательным. Инструкции по созданию безопасного запроса для аутентификации см. в разделе «Аутентификация с помощью ключа доступа на стороне сервера» .
Кодирование и декодирование

PublicKeyCredentialCreationOptions отправляются сервером. challenge , user.id и excludeCredentials.credentials должны быть закодированы на стороне сервера в base64URL , чтобы PublicKeyCredentialCreationOptions можно было доставить по протоколу HTTPS. PublicKeyCredentialCreationOptions включают поля типа ArrayBuffer , поэтому они не поддерживаются методом JSON.stringify() . Это означает, что в настоящее время для доставки PublicKeyCredentialCreationOptions по HTTPS некоторые поля необходимо вручную кодировать на сервере с помощью base64URL , а затем декодировать на клиенте.
- На сервере кодирование и декодирование обычно выполняется серверной библиотекой FIDO.
- На клиенте кодирование и декодирование сейчас необходимо выполнять вручную. В будущем это станет проще: будет доступен метод преобразования параметров из JSON в
PublicKeyCredentialCreationOptions. Проверьте статус реализации в Chrome.
Пример кода: создание параметров создания учетных данных
В наших примерах мы используем библиотеку SimpleWebAuthn . Здесь мы передаем создание параметров учётных данных открытого ключа её функции generateRegistrationOptions .
import {
generateRegistrationOptions,
verifyRegistrationResponse,
generateAuthenticationOptions,
verifyAuthenticationResponse
} from '@simplewebauthn/server';
import { isoBase64URL } 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: isoBase64URL.toBuffer(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: user.id,
userName: user.username,
userDisplayName: user.displayName || '',
attestationType: 'none',
excludeCredentials,
authenticatorSelection: {
authenticatorAttachment: 'platform',
requireResidentKey: true
},
});
// 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 Keychain, и один, синхронизированный через Google Password Manager.
Вот пример схемы, которую можно использовать для хранения учетных данных:

- Таблица пользователей :
-
user_id: основной идентификатор пользователя. Случайный, уникальный и постоянный идентификатор пользователя. Используйте его в качестве первичного ключа для таблицы «Пользователи» . -
username. Имя пользователя, определяемое пользователем и потенциально доступное для редактирования. -
passkey_user_id: идентификатор пользователя, не содержащий персональных данных, указанный вuser.idв параметрах регистрации . При последующей попытке аутентификации пользователя аутентификатор предоставитpasskey_user_idв ответе на запрос аутентификации вuserHandle. Мы рекомендуем не использоватьpasskey_user_idв качестве первичного ключа. Первичные ключи, как правило, фактически становятся персональными данными в системах, поскольку они широко используются.
-
- Таблица учетных данных открытого ключа :
-
id: идентификатор учётных данных. Используйте его в качестве первичного ключа для таблицы учётных данных открытого ключа . -
public_key: Открытый ключ учетных данных. -
passkey_user_id: используйте это как внешний ключ для установления связи с таблицей Users . -
backed_up: Резервная копия ключа доступа создается, если он синхронизирован поставщиком ключей доступа. Сохранение состояния резервной копии полезно, если вы планируете в будущем отказаться от паролей пользователей, хранящих ключи доступа,backed_up. Вы можете проверить наличие резервной копии ключа доступа, проверив флаг BE вauthenticatorDataили воспользовавшись функцией серверной библиотеки FIDO, которая обычно доступна для легкого доступа к этой информации. Сохранение информации о наличии резервной копии может быть полезно для ответа на потенциальные запросы пользователей. -
name: При необходимости отображаемое имя для учетных данных, позволяющее пользователям давать учетным данным собственные имена. -
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,
credentialPublicKey,
credentialID,
credentialBackedUp
} = registrationInfo;
// Name the credential based on AAGUID
const name =
aaguid === undefined ||
aaguid === '000000-0000-0000-0000-00000000' ?
req.useragent?.platform : aaguids[aaguid].name;
const base64CredentialID = isoBase64URL.fromBuffer(credentialID);
const base64PublicKey = isoBase64URL.fromBuffer(credentialPublicKey);
// Existing, signed-in user
const { user } = res.locals;
// Save the credential
await Credentials.update({
id: base64CredentialID,
passkey_user_id: user.passkey_user_id,
publicKey: base64PublicKey,
name,
aaguid,
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-версия клиентских данных , которые в вебе отображаются в браузере. Они содержат источник RP, запрос иandroidPackageName, если клиент — приложение Android. В качестве RP чтениеclientDataJSONдаёт доступ к информации, которую браузер видел во время запросаcreate. -
response.attestationObjectсодержит два фрагмента информации:-
attestationStatement, который не имеет значения, если вы не используете attestation. -
authenticatorData— это данные, которые видит поставщик ключа доступа. Как RP, чтениеauthenticatorDataдаёт вам доступ к данным, которые видит поставщик ключа доступа и которые возвращаются при запросеcreate.
-
authenticatorData содержит важную информацию об учетных данных открытого ключа, связанных с вновь созданным ключом доступа:
- Собственно учетные данные открытого ключа и уникальный идентификатор учетных данных для него.
- Идентификатор RP, связанный с учетными данными.
- Флаги, описывающие статус пользователя при создании ключа доступа: присутствовал ли пользователь на самом деле и был ли пользователь успешно проверен (см. подробное описание userVerification ).
- AAGUID — это идентификатор поставщика ключа доступа, например, Google Password Manager. На основе AAGUID можно определить поставщика ключа доступа и отобразить его имя на странице управления ключами доступа . (См. раздел Определение поставщика ключа доступа с помощью AAGUID ).
Несмотря на то, что authenticatorData вложен в attestationObject , содержащаяся в нём информация необходима для реализации вашего ключа доступа независимо от того, используете ли вы аттестацию. authenticatorData закодирован и содержит поля, закодированные в двоичном формате. Обычно разбором и декодированием занимается ваша серверная библиотека. Если вы не используете серверную библиотеку, рассмотрите возможность использования getAuthenticatorData() на стороне клиента, чтобы сэкономить время на разбор и декодирование на стороне сервера.
Приложение: проверка ответа на регистрацию
Под капотом проверка ответа на регистрацию состоит из следующих проверок:
- Убедитесь, что идентификатор RP соответствует вашему сайту.
- Убедитесь, что источник запроса соответствует ожидаемому источнику для вашего сайта (URL основного сайта, приложение Android).
- Если вам требуется проверка пользователя, убедитесь, что флаг проверки пользователя
authenticatorData.uvимеетtrue. - Обычно ожидается, что флаг присутствия пользователя
authenticatorData.upбудет иметьtrue, но если учетные данные созданы условно , ожидается, что он будет иметь значениеfalse. - Проверьте, смог ли клиент выполнить поставленную вами задачу. Если вы не используете аттестацию, эта проверка не важна. Тем не менее, её реализация — это наилучшая практика: она гарантирует готовность вашего кода, если вы решите использовать аттестацию в будущем.
- Убедитесь, что идентификатор учетной записи еще не зарегистрирован ни для одного пользователя.
- Убедитесь, что алгоритм, используемый поставщиком ключа доступа для создания учётных данных, — это алгоритм, указанный вами (в каждом поле
algфайлаpublicKeyCredentialCreationOptions.pubKeyCredParams, который обычно определяется в вашей серверной библиотеке и не виден вам). Это гарантирует, что пользователи смогут регистрироваться только с помощью разрешённых вами алгоритмов.
Чтобы узнать больше, проверьте исходный код SimpleWebAuthn на наличие verifyRegistrationResponse или изучите полный список проверок в спецификации .