В этом руководстве подробно рассказывается, как разрешить пользователям регистрировать ключ доступа для своего аккаунта, чтобы обеспечить высокозащищенную, устойчивую к фишингу альтернативу входу без пароля.
Требования к базе данных
Чтобы поддерживать регистрацию ключей доступа, таблица учетных данных в базе данных должна содержать следующие поля:
export interface StoredPasskeyCredential {
id: string; // Base64URL-encoded credential ID (unique lookup key)
passkeyUserId: string; // Associated application user ID
credentialPublicKey: string; // Base64URL-encoded public key used to verify assertion signatures
credentialType: "public-key";
credentialDeviceType: "singleDevice" | "multiDevice"; // Helps distinguish device-bound vs cloud-synced passkeys
credentialBackedUp: boolean; // Boolean backup state reported by the authenticator
aaguid: string; // Authenticator Attestation GUID
providerIcon?: string; // Provider icon derived from the AAGUID registry (dark or light theme URLs)
name: string; // Provider name derived from AAGUID registry
transports: string[]; // Array of transport names (e.g. 'internal', 'hybrid') necessary for exclusion options
lastUsedAt?: number; // Optional epoch timestamp of last sign-in
registeredAt: number; // Registration epoch timestamp
counter: number; // Authenticator sign-in signature counter used to prevent replay attacks
}
Серверный
Генерирование вариантов
Создайте конечную точку, которая генерирует параметры создания WebAuthn. Используйте проверенные библиотеки, соответствующие стандартам категорий, а не создавайте криптографические решения самостоятельно.
- Использовать стандартный идентификатор проверяющей стороны. В качестве постоянной строки используется стандартный идентификатор проверяющей стороны.
- Создайте безопасный запрос. Сгенерируйте на сервере криптографически безопасный случайный буфер с высокой энтропией, надежно сохраните его в сеансе пользователя и закодируйте в формате Base64URL для передачи параметров.
- Избегайте дублирования ключей доступа. Сопоставьте существующие идентификаторы предварительно зарегистрированных учетных данных пользователя с массивом параметров
excludeCredentials. Это предотвращает регистрацию дублирующихся учетных данных в одном аккаунте поставщика ключей доступа. - Принудительное использование обнаруживаемых учетных данных. Задайте для параметров
requireResidentKey: trueиresidentKey: "required"вauthenticatorSelectionзначение, которое запрашивает обнаруживаемые учетные данные. Это необходимо для входа с помощью обнаруживаемых учетных данных. - Настройте проверку пользователей. Укажите
userVerification: "preferred"илиuserVerification: "required". Во многих случаях, когда требуется соблюдение нормативных требований (например, в сфере финансов или здравоохранения),'required'требует подтверждения личности пользователя при создании. - Определите область действия прикрепления
- Продвижение. Если вы предлагаете создать ключ доступа сразу после стандартного входа с паролем или регистрации, задайте для
authenticatorAttachment: "platform"принудительное использование аутентификатора платформы и пропуск запросов внешнего электронного ключа. - Процесс управления. Если вызов выполняется из специальной панели настроек или безопасности, где помимо аутентификатора платформы поддерживаются внешние электронные ключи, полностью опустите свойство
authenticatorAttachment. - Совет. Чтобы условно обрабатывать оба потока с помощью одной конечной точки, примите флаг запроса
promotion: boolean.
- Продвижение. Если вы предлагаете создать ключ доступа сразу после стандартного входа с паролем или регистрации, задайте для
// Options generation example
const options = {
challenge: serverGeneratedBase64UrlChallenge, // Cryptographically random challenge
rp: { id: "example.com", name: "Secure Application" },
user: {
id: userBase64UrlId, // Unique base64url string identifying the account
name: "user@example.com",
displayName: "Jane Doe",
},
pubKeyCredParams: [
{
type: "public-key",
alg: -7,
},
{
type: "public-key",
alg: -257,
},
],
excludeCredentials: userExistingCredentials.map((cred) => ({
type: "public-key",
id: cred.id,
transports: cred.transports,
})),
authenticatorSelection: {
residentKey: "required",
requireResidentKey: true,
userVerification: "preferred",
...(isPromotionFlow && { authenticatorAttachment: "platform" }),
},
};
Проверка
- Проверка запроса. Безопасная проверка запроса на соответствие ожидаемому запросу, связанному с сеансом.
- Проверка присутствия пользователя
- Убедитесь, что флаг User Present (UP), возвращенный в обработанных данных аутентификатора, имеет значение
true. Это подтверждает физическое присутствие пользователя в момент создания.
- Убедитесь, что флаг User Present (UP), возвращенный в обработанных данных аутентификатора, имеет значение
- Упрощенная проверка для предпочтительных доменов
- Если в параметрах создания указано
userVerification: "preferred", то вызов проверки на стороне сервера ДОЛЖЕН быть настроен с помощьюrequireUserVerification: false. В противном случае аутентификаторы, которые регистрируются без подтверждения пользователя (например, если отключена блокировка экрана), будут вызывать ложные сбои проверки сервера.
- Если в параметрах создания указано
Клиентская логика
- Ограничение доступа к интерфейсу при загрузке страницы
- При загрузке страницы вызовите
PublicKeyCredential.getClientCapabilities()и отключите кнопку "Создать ключ доступа", еслиconditionalGetилиpasskeyPlatformAuthenticatorнедоступны.
- При загрузке страницы вызовите
- Вызов создания и сериализация. Декодируйте параметры сервера с помощью
PublicKeyCredential.parseCreationOptionsFromJSON()и передайте полученную конфигурацию вnavigator.credentials.create().- Вызовите
credential.toJSON(), чтобы закодироватьAuthenticatorAttestationResponseв действительный сериализуемый объект JSON, прежде чем запрашивать конечную точку подтверждения.
- Вызовите
- Обработка исключений WebAuthn
InvalidStateError: соответствующий ключ доступа уже существует (совпадение поexcludeCredentials).NotAllowedError: пользователь закрыл диалоговое окно аутентификации с помощью ключа доступа или время ожидания истекло.AbortError. Операция прервана.SecurityError– ошибки, связанные с защищенными источниками (HTTPS) или несоответствием идентификатора проверяющей стороны (проблемы с конфигурацией).
- Разделение Try/Catch для Signal API:
- Оберните вызов проверки сервера
fetch()в блок try/catch. ВызовитеsignalUnknownCredential(), если не удалось получить подтверждение от сервера (любой статусresponse.ok === falseили сетевые ошибки).
- Оберните вызов проверки сервера
// optionsFetch and registerVerifyFetch are app-defined HTTP methods
import { optionsFetch, registerVerifyFetch } from "./api.js";
async function registerPasskey(isPromotion = false) {
// Verify passkey capability and conditional UI are available
const capabilities = await PublicKeyCredential.getClientCapabilities();
if (
!capabilities.passkeyPlatformAuthenticator ||
!capabilities.conditionalGet
) {
// Hide "Create passkey" buttons and fall back to password flows instead
showStandardPasswordFallbackUI();
return;
}
const creationOptionsJSON = await optionsFetch({ promotion: isPromotion });
const publicKey =
PublicKeyCredential.parseCreationOptionsFromJSON(creationOptionsJSON);
let credential;
try {
// passkey prompt execution
credential = await navigator.credentials.create({ publicKey });
} catch (err) {
if (err.name === "InvalidStateError") {
console.log("A passkey already exists for this account.");
alert("A passkey already exists for this account.");
} else if (err.name === "SecurityError") {
console.error("Configuration RP ID or Secure Context error.");
alert("Configuration RP ID or Secure Context error.");
} else if (err.name === "NotAllowedError") {
console.log("User cancelled the passkey dialog.");
} else if (err.name === "AbortError") {
console.log("The creation operation has been aborted.");
}
return; // Safe API exit, do not signal unknown for standard WebAuthn cancels
}
// Server Verification phase (Segregated Try/Catch)
let encodedResponse = credential.toJSON();
try {
const response = await registerVerifyFetch(encodedResponse);
if (!response.ok) {
// Server verification failed to verify/authenticate the credential (orphaned)
await PublicKeyCredential.signalUnknownCredential({
rpId, // RP ID must match the one defined on the server
credentialId: encodedResponse.id, // Base64URL-encoded credential ID
});
}
} catch (serverErr) {
console.error("Server verification network failure:", serverErr);
await publickeycredential.signalunknowncredential({
rpId, // RP ID must match the one defined on the server
credentialid: encodedresponse.id, // base64url-encoded credential id
});
}
}
Резервные стратегии
Резервный вариант синхронизации Signal API
WebAuthn Signal API (webauthn-signals) – это прогрессивная оптимизация, которая используется для синхронизации менеджеров паролей с состоянием учетных данных на сервере.
- Резервный вариант. Если браузер не поддерживает
PublicKeyCredential.signalUnknownCredential, вызов ДОЛЖЕН быть безопасно обойден с помощью шлюза обнаружения функций (if (PublicKeyCredential.signalUnknownCredential)), а проверка на стороне сервера просто регистрирует сбой, не запуская обновления менеджера.
Простая резервная сериализация JSON
Всегда устанавливайте webauthn-polyfills и импортируйте его в контекст.
Если поддерживается PublicKeyCredential, то поддерживаются и PublicKeyCredential.parseCreationOptionsFromJSON, и PublicKeyCredential.prototype.toJSON.
import 'webauthn-polyfills';