Если неаутентифицированный пользователь нажимает кнопку "Войти", начинает оформление заказа, добавляет товар в избранное или сталкивается с платным доступом к статье, а вы перенаправляете его на отдельную страницу входа, то он теряет контекст и может уйти с сайта. Режим немедленного показа интерфейса (uiMode: 'immediate' в navigator.credentials.get()) позволяет быстро войти в аккаунт, не теряя контекст. Если на устройстве есть локальные ключи доступа (publicKey) или сохраненные пароли (password: true), пользователь сразу увидит единое окно выбора аккаунта.
В отличие от стандартных модальных запросов WebAuthn, которые всегда показывают интерфейс браузера и возвращаются к QR-кодам на разных устройствах или внешним ключам безопасности, режим Immediate UI быстро завершается с ошибкой NotAllowedError, если на устройстве нет подходящих учетных данных. Это позволяет вашему приложению выполнять аутентификацию вернувшихся пользователей одним нажатием, а пользователей без локальных учетных данных легко перенаправлять на стандартную страницу входа, страницу оплаты как гость или встроенную форму входа.
Рекомендуемые шаблоны и антишаблоны UX
Запускайте режим немедленного показа интерфейса в обработчике событий, инициированных пользователем (требуется временная активация пользователем), когда пользователь явно выражает намерение непосредственно перед уходом с текущей страницы. Если немедленная аутентификация прошла успешно, сразу же выполните намерение пользователя (обновите текущую страницу или перейдите непосредственно к месту назначения, требующему аутентификации). Если возвращается ошибка NotAllowedError, покажите пользователю процесс входа или регистрации, а затем выполните его запрос.
Примеры использования:
- Перехват нажатия кнопки или ссылки для входа в заголовке. Перехватывайте нажатие кнопки "Войти" (
event.preventDefault()), чтобы сразу выполнять аутентификацию вернувшихся пользователей или перенаправлять их на страницу входа на сайтеNotAllowedError. - Динамический вход перед оплатой. Позволяет аутентифицировать вернувшихся покупателей одним нажатием, когда они нажимают "Перейти к оплате", чтобы они сразу переходили к шагу доставки и оплаты, или переходить к стандартному экрану входа или оплаты в качестве гостя в случае сбоя.
- Встроенный вход для микродействий (добавление в избранное, лайк, закладка, сохранение). Выполняйте аутентификацию на месте, когда пользователь, не вошедший в аккаунт, нажимает кнопку микродействия, и сразу же завершайте действие (например, переключайте
aria-pressed="true") или открывайте резервное модальное окно входа или страницу входа с параметром URL возврата. - Ограниченный бесплатный доступ к статьям. Читатели могут пройти аутентификацию, нажав "Продолжить чтение, используя аккаунт" на баннере платного доступа или развернув встроенную форму входа или регистрации.
Чего следует избегать:
- НЕ используйте режим немедленного показа интерфейса на специальной странице входа. Никогда не прикрепляйте
uiMode: 'immediate'к основной кнопке "Войти" или "Войти с помощью ключа доступа" на странице входа. Пользователи попадают на специальную страницу входа, только если на текущем устройстве нет локальных учетных данных или если они хотят использовать ключ доступа на нескольких устройствах (отсканировав QR-код с помощью телефона) или внешний аппаратный электронный ключ. ПосколькуuiMode: 'immediate'подавляет варианты с QR-кодом и электронным ключом на разных устройствах и сразу же выдаетNotAllowedError, если локальные учетные данные отсутствуют, использование этого метода на специальной странице входа заводит пользователя в тупик, когда нажатие кнопки ни к чему не приводит. На отдельной странице входа используйте условный интерфейс (mediation: 'conditional'сautocomplete="username webauthn") для полей ввода формы в сочетании со стандартной модальной кнопкойnavigator.credentials.get({ publicKey })(см. раздел Аутентификация с помощью ключа доступа). - НЕ используйте режим Immediate UI для повторной аутентификации. Никогда не используйте
uiMode: 'immediate'для повторной аутентификации уже вошедшего в аккаунт пользователя перед выполнением важных действий, таких как смена пароля, обновление адреса доставки, изменение сохраненных способов оплаты или добавление нового ключа доступа. При повторной аутентификации запрос должен быть ограничен учетными данными вошедшего пользователя с помощью непустого спискаallowCredentials, который режим Immediate UI отклоняет с ошибкойNotAllowedError. Если не указатьallowCredentials, пользователь может случайно выбрать аккаунт, отличный от того, в который он вошел. Для повторной аутентификации с повышением уровня доступа следуйте инструкциям в разделе Повторная аутентификация с помощью ключа доступа.
Ограничения, связанные с конфиденциальностью и безопасностью
При запросе uiMode: 'immediate' браузеры применяют строгие правила конфиденциальности и безопасности:
- Требуется временная активация пользователя. Чтобы предотвратить скрытую проверку учетных данных, вы ДОЛЖНЫ вызвать
navigator.credentials.get({ uiMode: 'immediate' })внутри обработчика событий, инициированных пользователем (например, прослушивательclickдля элемента<button>или<a>). Никогда не вызывайте его автоматически при загрузке страницы (DOMContentLoaded). Вызов API не расходует временную активацию. - Не передавайте непустой список
allowCredentials. Не заполняйтеpublicKey.allowCredentials, если указаноuiMode: 'immediate'. Браузеры сразу отклоняют любой запрос в режиме Immediate UI с непустым спискомallowCredentials, выдавая ошибкуNotAllowedError. Это не позволяет проверяющим сторонам искать определенные идентификаторы учетных данных, чтобы узнать о предыдущих посещениях или отслеживать пользователей в разных сеансах. - Не передавайте
AbortSignal(signal). Не задавайте свойствоsignalдляnavigator.credentials.get(), если указаноuiMode: 'immediate', поскольку браузеры запрещают программное закрытие диалогового окна мгновенного входа. Если на вашей странице также выполняется фоновый запрос условного интерфейса (mediation: 'conditional'), вызовитеabort()для этого условного интерфейсаAbortControllerперед вызовом режима немедленного интерфейса и опуститеsignalиз самого немедленного запроса. - Режим инкогнито и приватный просмотр. Запросы, отправленные в режиме инкогнито или приватного просмотра, всегда отклоняются с ошибкой
NotAllowedError, чтобы приватные сеансы не отличались от сеансов без локальных учетных данных.
Настройка на стороне сервера
Конечная точка генерации вариантов
Предоставьте конечную точку, которая генерирует обнаруживаемые параметры запроса WebAuthn для неаутентифицированных посетителей:
- Сгенерируйте запрос, связанный с сеансом. Создайте криптографически случайный буфер запроса, сохраните его в сеансе сервера и закодируйте как Base64URL.
- Сохранение настроек подтверждения пользователя. Задайте
userVerification: 'preferred'и сохранитеexpectedUserVerification: 'preferred'в сеансе. - Не указывайте
allowCredentials. Не включайте непустой списокallowCredentialsв параметры для режима Immediate UI, так как браузеры отклоняют непустые белые списки сNotAllowedError.
// Server-side options generation example (/api/login/options)
router.post('/api/login/options', (req, res) => {
const options = {
challenge: serverGeneratedBase64UrlChallenge, // High-entropy random challenge stored in session
rpId: 'example.com',
userVerification: 'preferred',
};
req.session.challenge = options.challenge;
req.session.expectedUserVerification = 'preferred';
return res.json(options);
});
Конечные точки для проверки двух наборов учетных данных
Поскольку navigator.credentials.get({ password: true, publicKey, uiMode: 'immediate' }) может разрешаться как с помощью ключа доступа (PublicKeyCredential), так и с помощью сохраненного пароля (PasswordCredential), предоставьте обработчики проверки для обоих типов учетных данных:
- Проверка утверждения WebAuthn (
/api/login/verify). Утверждение проверяется на соответствие запросу сеанса и сохраненному открытому ключу. Если идентификатор учетных данных не найден в базе данных, верните статус HTTP404, чтобы клиент мог уведомить менеджер паролей черезPublicKeyCredential.signalUnknownCredential(). - Проверка пароля (
/api/login/password). Проверьтеusername(cred.id) иpassword(cred.password) по базе данных пользователей и установите сеанс с аутентификацией.
Клиентская реализация
Единый запрос учетных данных
- Запрос ключей доступа и паролей. Декодируйте параметры сервера с помощью
PublicKeyCredential.parseRequestOptionsFromJSON()и вызовитеnavigator.credentials.get()в обработчике кликов, инициированных пользователем, сpassword: true,publicKeyиuiMode: 'immediate'(без передачиsignal). - Проверка филиала компанией
cred.type:- Если
cred.type === 'public-key', сериализуйте утверждение с помощьюcred.toJSON()и отправьте его в конечную точку проверки WebAuthn. Если сервер возвращает код HTTP404, вызовитеPublicKeyCredential.signalUnknownCredential({ rpId: publicKey.rpId, credentialId: encoded.id }). - Если
cred.type === 'password', отправьте запрос POST{ username: cred.id, password: cred.password }на конечную точку проверки пароля.
- Если
- Обрабатывайте ошибку
NotAllowedError, если на устройстве нет локальных учетных данных. ОшибкаNotAllowedErrorвозникает, когда на устройстве нет локальных ключей доступа или сохраненных паролей или когда пользователь закрывает окно выбора. В этом случае перенаправляйте пользователя на стандартную страницу входа, например открывайте встроенное диалоговое окно входа или перенаправляйте на страницу/signinс URL возврата.
let isAuthenticating = false;
async function authenticateWithImmediateUi() {
const optionsResponse = await fetch('/api/login/options', { method: 'POST' });
if (!optionsResponse.ok) {
throw new Error('Failed to fetch authentication options');
}
const optionsJSON = await optionsResponse.json();
const publicKey = PublicKeyCredential.parseRequestOptionsFromJSON(optionsJSON);
// MANDATORY: Request both password and publicKey with uiMode: 'immediate' and NO signal parameter
const cred = await navigator.credentials.get({
password: true,
publicKey,
uiMode: 'immediate',
});
if (!cred) {
throw new DOMException('No credential returned', 'NotAllowedError');
}
if (cred.type === 'public-key') {
const encoded = cred.toJSON();
const verifyResponse = await fetch('/api/login/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(encoded),
});
if (verifyResponse.status === 404) {
await PublicKeyCredential.signalUnknownCredential({
rpId: publicKey.rpId || window.location.hostname,
credentialId: encoded.id, // Base64URL-encoded credential ID string
});
throw new Error('Passkey credential is no longer recognized by the server');
}
if (!verifyResponse.ok) {
throw new Error('Passkey verification failed');
}
return await verifyResponse.json();
}
if (cred.type === 'password') {
const verifyResponse = await fetch('/api/login/password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username: cred.id,
password: cred.password,
}),
});
if (!verifyResponse.ok) {
throw new Error('Password verification failed');
}
return await verifyResponse.json();
}
throw new Error(`Unsupported credential type: ${cred.type}`);
}
// Example: Intercepting an inline micro-action (Favorite button) or Header Sign-in click
const favoriteButton = document.getElementById('favorite-btn');
favoriteButton.addEventListener('click', async (event) => {
event.preventDefault();
if (isAuthenticating) return;
// If already signed in, toggle the micro-action state immediately
if (document.body.dataset.signedIn === 'true') {
const nextPressed = favoriteButton.getAttribute('aria-pressed') !== 'true';
favoriteButton.setAttribute('aria-pressed', String(nextPressed));
return;
}
isAuthenticating = true;
try {
const user = await authenticateWithImmediateUi();
// Complete the user's intended action immediately
document.body.dataset.signedIn = 'true';
favoriteButton.setAttribute('aria-pressed', 'true');
updateHeaderUserState(user);
} catch (err) {
if (err.name === 'NotAllowedError') {
// No local credentials available or user dismissed dialog:
// transition smoothly to fallback sign-in UI or redirect with return URL
showFallbackSignInUi({ returnAction: 'favorite' });
return;
}
console.error('Authentication error:', err);
} finally {
isAuthenticating = false;
}
});
Резервные стратегии
Немедленный переход в резервный режим интерфейса
- Прогрессивное улучшение. Относитесь к
uiMode: 'immediate'как к прогрессивному улучшению. Не делайте выводов о поддержке на основе названия или версии браузера. Единственный надежный сигнал поддержки – это проверка среды выполненияcapabilities.immediateGetс помощьюPublicKeyCredential.getClientCapabilities(). ОБЯЗАТЕЛЬНО. Всегда проверяйтеcapabilities.immediateGetперед вызовомnavigator.credentials.get({ uiMode: 'immediate' }). Браузеры, которые не распознаютuiMode: 'immediate', игнорируют неизвестный ключ словаря и открывают блокирующее модальное диалоговое окно с ключом доступа и QR-кодом. Еслиcapabilities.immediateGetимеет ложное значение илиnavigator.credentials.get()отклоняется сNotAllowedError, вернитесь к стандартному процессу входа в приложение (например, откройте встроенное модальное окно входа или перейдите на специальную страницу входа с параметром URL возврата), чтобы пользователи неподдерживаемых браузеров видели существующий процесс входа без изменений:
async function isImmediateUiSupported() {
if (!window.PublicKeyCredential || !PublicKeyCredential.getClientCapabilities) {
return false;
}
try {
const capabilities = await PublicKeyCredential.getClientCapabilities();
return Boolean(capabilities?.immediateGet);
} catch {
return false;
}
}
async function authenticateWithImmediateUi() {
// MANDATORY: Check immediateGet support so unsupported browsers do not open a modal QR dialog
const supported = await isImmediateUiSupported();
if (!supported) {
// Throw NotAllowedError so the caller's single catch path treats "unsupported" like "no local credentials"
throw new DOMException('Immediate UI mode is not supported', 'NotAllowedError');
}
// Proceed with fetching options and calling navigator.credentials.get({ password: true, publicKey, uiMode: 'immediate' })
}
Резервный вариант определения возможностей клиента
Всегда устанавливайте 'webauthn-polyfills' и импортируйте его в клиентский модуль, чтобы PublicKeyCredential.getClientCapabilities() был доступен, когда поддерживается PublicKeyCredential:
import 'webauthn-polyfills';
Резервные учетные данные пароля
Если передать password: true вместе с publicKey в navigator.credentials.get(), поддерживаемые браузеры смогут включать сохраненные пароли из менеджера паролей браузера в единое окно выбора аккаунта.
- Резервный вариант. Браузеры, которые не поддерживают
PasswordCredential, игнорируют параметрpassword: trueили возвращаются к стандартной форме входа, используя проверку функцииcapabilities.immediateGet. В этом случае<input autocomplete="username webauthn">и<input type="password" autocomplete="current-password">обеспечивают встроенное автозаполнение паролей.
Простая резервная сериализация JSON
Всегда устанавливайте 'webauthn-polyfills' и импортируйте его в клиентский пакет, чтобы PublicKeyCredential.parseRequestOptionsFromJSON() и PublicKeyCredential.prototype.toJSON() работали одинаково во всех браузерах, поддерживающих PublicKeyCredential:
import 'webauthn-polyfills';
Резервная синхронизация Signal API
WebAuthn Signal API (webauthn-signals) – это прогрессивное улучшение, которое удаляет устаревшие ключи доступа из менеджера паролей пользователя, когда сервер отвечает HTTP 404.
- Резервный вариант. Звонки доступны только в
if (PublicKeyCredential.signalUnknownCredential). Если поддержка отсутствует, пропустите вызов сигнала и покажите пользователю понятное сообщение с предложением войти другим способом.