Обзор
Вот общий обзор ключевых шагов, связанных с регистрацией ключа доступа:
- Определите параметры для создания ключа доступа. Отправьте их клиенту, чтобы вы могли передать их в вызов создания ключа доступа: вызов 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
: для регистрации учетных данных запрос не имеет значения, если вы не используете аттестацию, более продвинутый метод проверки личности поставщика ключей доступа и данных, которые он передает. Однако даже если вы не используете аттестацию, запрос по-прежнему является обязательным полем. В этом случае для простоты вы можете установить для этого задания один0
. Инструкции по созданию безопасного запроса на аутентификацию доступны в разделе Аутентификация с использованием пароля на стороне сервера .
Кодирование и декодирование
![PublicKeyCredentialCreationOptions, отправленный сервером](https://google-developers.gonglchuangl.net/identity/passkeys/developer-guides/server-registration/images/server-publicKeyCredentialCreationOptions.png?authuser=0&hl=ru)
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 = 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 });
}
});
Сохраните открытый ключ
![PublicKeyCredentialCreationOptions, отправленный сервером](https://google-developers.gonglchuangl.net/identity/passkeys/developer-guides/server-registration/images/server-publicKeyCredentialCreationOptions.png?authuser=0&hl=ru)
navigator.credentials.create
возвращает объект PublicKeyCredential
. Когда navigator.credentials.create
успешно разрешается на клиенте, это означает, что ключ доступа был успешно создан. Возвращается объект PublicKeyCredential
.
Объект PublicKeyCredential
содержит объект AuthenticatorAttestationResponse
, который представляет собой ответ поставщика ключа доступа на инструкцию клиента по созданию ключа доступа. Он содержит информацию о новых учетных данных, которые вам понадобятся в качестве RP для последующей аутентификации пользователя. Дополнительные сведения об 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
: используйте его как внешний ключ для установления связи с таблицей «Пользователи» . -
backed_up
: резервная копия ключа доступа сохраняется , если он синхронизирован поставщиком ключа доступа. Сохранение состояния резервной копии полезно, если вы хотите рассмотреть возможность удаления паролей в будущем для пользователей, у которых есть ключи доступаbacked_up
. Вы можете проверить, создана ли резервная копия ключа доступа, проверив флаги вauthenticatorData
или используя функцию серверной библиотеки FIDO, которая обычно доступна для облегчения доступа к этой информации. Сохранение соответствия критериям резервного копирования может быть полезно для ответа на запросы потенциальных пользователей. -
name
: опционально отображаемое имя учетных данных, позволяющее пользователям присваивать учетным данным собственные имена. -
transports
: Массив транспортов . Хранение транспортов полезно для аутентификации пользователя. Когда транспорты доступны, браузер может вести себя соответствующим образом и отображать пользовательский интерфейс, соответствующий транспорту, который поставщик ключей доступа использует для связи с клиентами — в частности, для случаев повторной аутентификации, когдаallowCredentials
не пуст.
-
Другая информация может быть полезна для хранения в целях удобства пользователя, включая такие элементы, как поставщик ключа доступа, время создания учетных данных и время последнего использования. Подробнее читайте в разделе «Дизайн пользовательского интерфейса Passkeys» .
Пример кода: сохранить учетные данные
В наших примерах мы используем библиотеку 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 { credentialPublicKey, credentialID } = registrationInfo;
// Existing, signed-in user
const { user } = res.locals;
// Save the credential
await Credentials.update({
id: base64CredentialID,
publicKey: base64PublicKey,
// Optional: set the platform as a default name for the credential
// (example: "Pixel 7")
name: req.useragent.platform,
transports: response.response.transports,
passkey_user_id: user.passkey_user_id,
backed_up: registrationInfo.credentialBackedUp
});
// 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
, который не имеет значения, если вы не используете аттестацию. -
authenticatorData
— это данные, видимые поставщиком ключа доступа. В качестве RP чтениеauthenticatorData
дает вам доступ к данным, видимым поставщиком ключей доступа и возвращаемым во время запросаcreate
.
-
authenticatorData
содержит важную информацию об учетных данных открытого ключа, связанных с вновь созданным ключом доступа:
- Учетные данные открытого ключа и уникальный идентификатор учетных данных для него.
- Идентификатор RP, связанный с учетными данными.
- Флаги, которые описывают статус пользователя на момент создания ключа доступа: присутствовал ли пользователь на самом деле и был ли пользователь успешно проверен (см.
userVerification
). - AAGUID , который идентифицирует поставщика ключа доступа. Отображение поставщика ключей доступа может быть полезно для ваших пользователей, особенно если у них есть ключ доступа, зарегистрированный для вашей службы у нескольких поставщиков ключей доступа.
Несмотря на то, что authenticatorData
вложен в attestationObject
, содержащаяся в нем информация необходима для реализации вашего пароля независимо от того, используете ли вы аттестацию или нет. authenticatorData
закодирован и содержит поля, закодированные в двоичном формате. Ваша серверная библиотека обычно занимается синтаксическим анализом и декодированием. Если вы не используете серверную библиотеку, рассмотрите возможность использования клиентской части getAuthenticatorData()
чтобы сэкономить на работе по синтаксическому анализу и декодированию на стороне сервера.
Приложение: проверка регистрационного ответа
По сути, проверка ответа на регистрацию состоит из следующих проверок:
- Убедитесь, что идентификатор RP соответствует вашему сайту.
- Убедитесь, что источник запроса соответствует ожидаемому источнику вашего сайта (URL основного сайта, приложение Android).
- Если вам требуется проверка пользователя, убедитесь, что флаг проверки
authenticatorData.uv
имеетtrue
. Убедитесь, что флаг присутствияauthenticatorData.up
имеет значениеtrue
, поскольку присутствие пользователя всегда требуется для ключей доступа. - Убедитесь, что клиент смог выполнить поставленную вами задачу. Если вы не используете аттестацию, эта проверка не имеет значения. Однако реализация этой проверки является рекомендуемой практикой: она гарантирует, что ваш код будет готов, если вы решите использовать аттестацию в будущем.
- Убедитесь, что идентификатор учетных данных еще не зарегистрирован ни для одного пользователя.
- Убедитесь, что алгоритм, используемый поставщиком ключа доступа для создания учетных данных, является алгоритмом, который вы указали (в каждом поле
alg
publicKeyCredentialCreationOptions.pubKeyCredParams
, который обычно определяется в вашей серверной библиотеке и не виден вам). Это гарантирует, что пользователи смогут регистрироваться только с теми алгоритмами, которые вы разрешили.
Чтобы узнать больше, проверьте исходный код SimpleWebAuthn на verifyRegistrationResponse
или погрузитесь в полный список проверок в спецификации .