В этом руководстве подробно рассказывается, как реализовать аутентификацию вернувшихся пользователей с помощью обнаруживаемых учетных данных, используя как явные триггеры кнопок, так и удобные подсказки автозаполнения в браузере (условный пользовательский интерфейс).
Серверный
Генерирование вариантов
Создайте конечную точку, которая генерирует параметры запроса WebAuthn, используя проверенную библиотеку в соответствии со стандартами.
- Использовать стандартный идентификатор проверяющей стороны. В качестве постоянной строки используется стандартный идентификатор проверяющей стороны.
- Сгенерируйте запрос. Создайте криптографически защищенный случайный буфер с высокой энтропией, надежно сохраните его в сеансе пользователя и закодируйте в формате Base64URL.
- Сопоставление обнаруживаемых учетных данных. Укажите пустой массив
[]дляallowCredentials. Это запрашивает видимые учетные данные, то есть пользователю не нужно сначала вводить свое имя пользователя; поставщик ключа доступа предоставит доступные учетные записи. - Уровень подтверждения пользователя. Установите значение
userVerification: "preferred"(или"required", если это явно предписано корпоративными правилами).- Запрошенный уровень ограничений
userVerificationДОЛЖЕН сохраняться в записи сеанса сервера в конечной точке параметров, а не передаваться обратно от клиента через строки запроса. Это позволяет конечной точке проверки безопасно применять строгие ограничения на сопоставление без риска манипуляций со стороны клиента.
- Запрошенный уровень ограничений
// Options generation example (discoverable flow)
const options = {
challenge: serverGeneratedBase64UrlChallenge, // High-entropy random challenge stored in session
rpId: "example.com",
allowCredentials: [], // Request discoverable passkeys
userVerification: "preferred",
};
// Persist expected UV level to user session
req.session.expectedUserVerification = "preferred";
Конечная точка подтверждения
Надежно проверьте утверждение, возвращенное клиентом, чтобы выполнить аутентификацию пользователя:
- Проверка запроса сеанса. Обеспечивает строгое соответствие между ответом клиента и ожидаемым запросом, сохраненным в сеансе.
- Принудительное применение настроек UV:
- Разрешите аутентификаторы без проверки пользователя (например, с отключенной блокировкой экрана), если в сеансе запрашивается
expectedUserVerification"preferred". Для этого передайтеrequireUserVerification: falseв библиотеку проверки на стороне сервера. Если запрашивается"required", строго требовать биометрические данные или PIN-код.
- Разрешите аутентификаторы без проверки пользователя (например, с отключенной блокировкой экрана), если в сеансе запрашивается
- Ошибка сервера 404. Если идентификатор учетных данных, возвращенный клиентом, не найден в базе данных, возвращается ошибка HTTP
404, чтобы клиент мог активировать Signal API.
Клиентская логика
Аннотация формы HTML
Добавьте аннотации к полям ввода имени пользователя и пароля, чтобы использовать условный интерфейс. Токены автозаполнения объединяют параметры спецификации WebAuthn, а автофокус сразу же активирует всплывающее окно автозаполнения в браузере, когда фокус находится на поле ввода.
<!-- Autocomplete tokens must contain webauthn space-separated -->
<form id="signin-form">
<input
type="text"
name="username"
autocomplete="username webauthn"
autofocus
data-testid="username-field"
/>
<input type="password" name="password" autocomplete="current-password" />
<button type="submit">Sign in</button>
</form>
Процесс с явной кнопкой
Запускайте аутентификацию с помощью ключа доступа, когда пользователь нажимает кнопку "Войти с помощью ключа доступа". Прежде чем показывать запрос ключа доступа, прервите все текущие вызовы условного получения для автозаполнения форм.
Условный процесс медиации (автозаполнение формы)
Чтобы при нажатии на поля входа пользователям предлагалась аутентификация с помощью ключа доступа, включите подсказки автозаполнения форм при загрузке страницы:
- Обнаружение функции. Вызовите
PublicKeyCredential.getClientCapabilities()при загрузке страницы и пропустите вход с помощью ключа доступа, еслиconditionalGetнедоступен. - Декодирование параметров. Декодируйте полученный объект JSON с учетными данными с помощью
PublicKeyCredential.parseRequestOptionsFromJSON(). - Вызов условного метода GET. Вызовите
navigator.credentials.get()с помощьюmediation: "conditional"и передайте сигналAbortController. Это регистрирует автозаполнение без показа диалогового окна ключа доступа. - Разделение исключений Try/Catch. Оберните вызов
navigator.credentials.getв блок try/catch:NotAllowedError– пользователь отменил запрос на вход с помощью ключа доступа или время ожидания истекло.AbortError: запрос на аутентификацию был отменен программно.
- Call Signal API. Оберните вызов
fetch()для проверки сервера в блок try/catch:- Покажите сообщение об ошибке, чтобы пользователь понял, что пошло не так.
- Вызывайте
signalUnknownCredential(), ТОЛЬКО когда сервер явно возвращает статус HTTP404(учетные данные не найдены) и пользователь не аутентифицирован. - Параметр
credentialId, передаваемый вsignalUnknownCredential(), ДОЛЖЕН быть строкой идентификатора учетных данных, закодированной по стандарту Base64URL (например,encoded.id), а НЕ необработанным объектом ArrayBuffercredential.rawId.
- Закодируйте ответ. Закодируйте учетные данные
AuthenticatorAssertionResponseс помощью.toJSON(), прежде чем отправлять их на сервер для проверки.
// optionsFetch and loginVerifyFetch are app-defined HTTP methods
import { optionsFetch, loginVerifyFetch } from "./api.js";
let autofillAbortController = new AbortController();
async function initializeConditionalAutofill() {
// Feature detect Conditional Get autofill support
const capabilities = await PublicKeyCredential.getClientCapabilities();
if (capabilities.conditionalGet === true) {
const loginOptionsJSON = await optionsFetch();
const publicKey =
PublicKeyCredential.parseRequestOptionsFromJSON(loginOptionsJSON);
try {
// Initiate Conditional UI form autofill suggestions
const credential = await navigator.credentials.get({
publicKey,
signal: autofillAbortController.signal,
mediation: "conditional",
});
// Segregated verification fetch
const encoded = credential.toJSON();
const response = await loginVerifyFetch(encoded);
if (!response.ok && response.status === 404) {
// Note: this code path runs pre-authentication, satisfying the unauth precondition
if (PublicKeyCredential.signalUnknownCredential) {
await PublicKeyCredential.signalUnknownCredential({
rpId, // RP ID must match the one defined on the server
credentialId: encoded.id,
});
}
}
} catch (err) {
// Silently swallow expected client WebAuthn exceptions
if (["NotAllowedError", "AbortError"].includes(err.name)) {
return;
}
console.error("Unexpected conditional get error:", err);
}
}
}
async function triggerButtonAuthentication() {
// Abort any pending Conditional Get call to prevent passkey prompt collisions
autofillAbortController.abort();
autofillAbortController = new AbortController(); // Reset controller for next triggers
const loginOptionsJSON = await optionsFetch();
const publicKey =
PublicKeyCredential.parseRequestOptionsFromJSON(loginOptionsJSON);
let credential;
try {
// Passkey explicit prompt trigger
credential = await navigator.credentials.get({
publicKey,
signal: autofillAbortController.signal,
});
} catch (err) {
if (err.name === "NotAllowedError") {
console.log("User cancelled passkey login.");
} else if (err.name === "AbortError") {
console.log("The authentication operation was aborted.");
}
// Re-arm Conditional autofill Suggestions after cancelled explicit button prompts
initializeConditionalAutofill();
return; // Safe exit
}
// Segregated verification try/catch (HTTP 404 trigger)
const encoded = credential.toJSON();
try {
const response = await loginVerifyFetch(encoded);
if (!response.ok && response.status === 404) {
// Note: this code path runs pre-authentication, satisfying the unauth precondition
await PublicKeyCredential.signalUnknownCredential({
rpId, // RP ID must match the one defined on the server
credentialId: encoded.id, // Base64URL-encoded credential ID
});
}
} catch (serverErr) {
console.error("Verification request error:", serverErr);
}
}
// Trigger Conditional Get on load
window.addEventListener("DOMContentLoaded", initializeConditionalAutofill);
Резервные стратегии
Резервный вариант обнаружения функции ключей доступа
Всегда устанавливайте webauthn-polyfills и импортируйте его в контекст.
Учтите, что если поддерживается PublicKeyCredential, то поддерживается и PublicKeyCredential.getClientCapabilities.
import 'webauthn-polyfills';
Резервный вариант синхронизации Signal API
WebAuthn Signal API (webauthn-signals) – это прогрессивная оптимизация, которая используется для синхронизации менеджеров паролей с состоянием учетных данных на сервере.
- Резервный вариант. Доступен через
if (PublicKeyCredential.signalUnknownCredential). Если нет, то фоновая синхронизация проверки пропускается без ошибок браузера.
Простая резервная сериализация JSON
Всегда устанавливайте webauthn-polyfills и импортируйте его в контекст.
Если поддерживается PublicKeyCredential, то поддерживаются и PublicKeyCredential.parseRequestOptionsFromJSON, и PublicKeyCredential.prototype.toJSON.
import 'webauthn-polyfills';