Обзор
Ниже приведен краткий обзор основных этапов регистрации ключа доступа:
- Определите параметры для создания ключа доступа. Отправьте их клиенту, чтобы затем передать их в вызов для создания ключа доступа: вызов 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
или изучите полный список проверок в спецификации .