Przekierowanie niezalogowanego użytkownika na specjalną stronę logowania, gdy kliknie on link „Zaloguj się”, rozpocznie proces płatności, zapisze produkt w ulubionych lub natrafi na paywall, powoduje utratę kontekstu i zwiększa liczbę użytkowników, którzy rezygnują z dalszych działań. Tryb natychmiastowego interfejsu (uiMode: 'immediate' w navigator.credentials.get()) umożliwia szybkie logowanie kontekstowe, wyświetlając ujednolicony selektor konta przeglądarki, gdy na urządzeniu są dostępne lokalne klucze dostępu (publicKey) lub zapisane hasła (password: true).
W przeciwieństwie do standardowych modalnych promptów WebAuthn, które zawsze wyświetlają interfejs przeglądarki i korzystają z kodów QR na różnych urządzeniach lub zewnętrznych kluczy bezpieczeństwa, tryb natychmiastowego interfejsu szybko kończy się błędem NotAllowedError, jeśli na urządzeniu nie są dostępne lokalnie żadne odpowiednie dane logowania. Dzięki temu aplikacja może uwierzytelniać powracających użytkowników na miejscu za pomocą jednego kliknięcia, a użytkowników bez lokalnych danych logowania płynnie przekierowywać na standardową stronę logowania, do płatności bez logowania lub do formularza logowania w aplikacji.
Zalecane wzorce i antywzorce UX
Uruchamiaj tryb natychmiastowego interfejsu w module obsługi zdarzeń inicjowanych przez użytkownika (wymagającym tymczasowej aktywacji przez użytkownika) w momentach, gdy intencja użytkownika jest jasna, tuż przed opuszczeniem bieżącej strony. Jeśli natychmiastowe uwierzytelnienie się powiedzie, od razu zrealizuj zamiar użytkownika (aktualizując bieżącą stronę lub przechodząc bezpośrednio do uwierzytelnionego miejsca docelowego). Jeśli nie powiedzie się z błędem NotAllowedError, wyświetl proces logowania lub rejestracji ze ścieżką do realizacji zamiaru użytkownika po zakończeniu.
Możliwe przypadki użycia:
- Przechwytywanie kliknięcia linku lub przycisku logowania w nagłówku: przechwyć kliknięcie globalnego linku „Zaloguj się” (
event.preventDefault()), aby natychmiast uwierzytelnić powracających użytkowników, lub kontynuuj nawigację do specjalnej strony logowania w witrynieNotAllowedError. - Dynamiczne logowanie przed płatnością: uwierzytelniaj powracających kupujących jednym kliknięciem, gdy klikną „Przejdź do kasy”, aby trafili bezpośrednio do kroku dostawy i płatności, lub przejdź do standardowego ekranu logowania lub płatności bez logowania w przypadku niepowodzenia.
- Logowanie wbudowane w przypadku mikroakcji (ulubione, polubienie, zakładka, zapisanie): uwierzytelnianie na miejscu, gdy niezalogowany użytkownik kliknie przycisk mikroakcji i natychmiast wykona działanie (np. przełączy
aria-pressed="true") lub otworzy zastępcze okno logowania albo stronę logowania z parametrem adresu URL powrotu. - Elastyczne paywalle w artykule: uwierzytelniaj czytelników na miejscu, gdy klikną „Czytaj dalej na swoim koncie” na banerze paywalla, lub rozwiń wbudowany formularz logowania lub rejestracji w przypadku awarii.
Negatywne wzorce, których należy unikać:
- NIE używaj trybu natychmiastowego interfejsu na dedykowanej stronie logowania: nigdy nie dołączaj elementu
uiMode: 'immediate'do głównego przycisku „Zaloguj się” lub „Zaloguj się za pomocą klucza dostępu” na stronie logowania w miejscu docelowym. Użytkownicy trafiają na specjalną stronę logowania, gdy nie mają lokalnych danych logowania na bieżącym urządzeniu lub gdy chcą użyć klucza dostępu na różnych urządzeniach (skanując kod QR telefonem) albo zewnętrznego fizycznego klucza bezpieczeństwa.uiMode: 'immediate'blokuje opcje kodu QR i klucza bezpieczeństwa na różnych urządzeniach i natychmiast wyświetla błądNotAllowedError, gdy nie ma lokalnych danych logowania. Użycie tej metody na specjalnej stronie logowania powoduje, że użytkownik trafia w ślepy zaułek, w którym kliknięcie przycisku nic nie daje. Na dedykowanej stronie logowania użyj warunkowego interfejsu użytkownika (mediation: 'conditional'zautocomplete="username webauthn") w polach formularza połączonych ze standardowym przyciskiem modalnymnavigator.credentials.get({ publicKey })(patrz Uwierzytelnianie za pomocą klucza dostępu). - NIE używaj trybu natychmiastowego interfejsu do ponownego uwierzytelniania: nigdy nie używaj metody
uiMode: 'immediate'do ponownego uwierzytelniania zalogowanego już użytkownika przed wykonaniem działań związanych z poufnymi danymi (takich jak zmiana hasła, aktualizacja adresu dostawy, edytowanie zapisanych form płatności czy dodanie nowego klucza dostępu). Ponowne uwierzytelnianie musi ograniczać prośbę do danych logowania zalogowanego użytkownika za pomocą niepustej listyallowCredentials, którą tryb natychmiastowego interfejsu odrzuca z błędemNotAllowedError. PominięcieallowCredentialspozwoliłoby użytkownikowi nieumyślnie wybrać inne konto niż to, na które jest obecnie zalogowany. W przypadku ponownego uwierzytelniania z podwyższonym poziomem uprawnień postępuj zgodnie z instrukcjami podanymi w sekcji Ponowne uwierzytelnianie za pomocą klucza dostępu.
Ograniczenia dotyczące prywatności i bezpieczeństwa
Przeglądarki wymuszają rygorystyczne zasady dotyczące prywatności i bezpieczeństwa, gdy żądany jest parametr uiMode: 'immediate':
- Wymagana jest tymczasowa aktywacja użytkownika: MUSISZ wywołać
navigator.credentials.get({ uiMode: 'immediate' })w module obsługi zdarzeń zainicjowanych przez użytkownika (np. detektorclickw elemencie<button>lub<a>), aby zapobiec cichemu sprawdzaniu danych logowania. Nigdy nie wywołuj go automatycznie podczas wczytywania strony (DOMContentLoaded). Wywołanie interfejsu API nie powoduje zużycia aktywacji tymczasowej. - Nie przekazuj niepustej listy
allowCredentials: NIE wypełniaj polapublicKey.allowCredentials, gdy określono parametruiMode: 'immediate'. Przeglądarki natychmiast odrzucają każde żądanie trybu natychmiastowego interfejsu z niepustą listąallowCredentials, zwracając błądNotAllowedError, dzięki czemu strony uzależnione nie mogą sprawdzać konkretnych identyfikatorów danych logowania, aby wnioskować o wcześniejszych wizytach lub śledzić użytkowników w różnych sesjach. - Nie przekazuj właściwości
AbortSignal(signal): NIE ustawiaj właściwościsignalwnavigator.credentials.get(), gdy określonouiMode: 'immediate', ponieważ przeglądarki nie zezwalają na automatyczne odrzucanie okna natychmiastowego logowania. Jeśli na stronie działa też żądanie warunkowego interfejsu użytkownika (mediation: 'conditional') w tle, wywołajabort()w tym warunkowym interfejsie użytkownikaAbortControllerprzed wywołaniem trybu natychmiastowego interfejsu użytkownika i pomińsignalw samym żądaniu natychmiastowym. - Sesje przeglądania w trybie incognito i prywatnym: żądania wysyłane w oknach przeglądania w trybie incognito lub prywatnym są zawsze odrzucane z błędem
NotAllowedError, dzięki czemu sesje prywatne nie różnią się od sesji bez danych logowania lokalnego.
Konfiguracja po stronie serwera
Punkt końcowy generowania opcji
Udostępnij punkt końcowy, który generuje wykrywalne opcje żądań WebAuthn dla nieuwierzytelnionych użytkowników:
- Wygeneruj wyzwanie powiązane z sesją: utwórz kryptograficznie losowy bufor wyzwania, zapisz go w sesji serwera i zakoduj w formacie Base64URL.
- Utrzymywanie preferencji weryfikacji użytkownika: ustaw
userVerification: 'preferred'i zapiszexpectedUserVerification: 'preferred'w sesji. - Pomiń
allowCredentials: nie uwzględniaj niepustej listyallowCredentialsw opcjach trybu natychmiastowego interfejsu, ponieważ przeglądarki odrzucają niepuste listy dozwolonych zNotAllowedError.
// 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);
});
Punkty końcowe weryfikacji podwójnych danych logowania
Ponieważ navigator.credentials.get({ password: true, publicKey, uiMode: 'immediate' }) może zostać rozwiązany za pomocą klucza dostępu (PublicKeyCredential) lub zapisanego hasła (PasswordCredential), podaj moduły obsługi weryfikacji dla obu typów danych logowania:
- Weryfikacja asercji WebAuthn (
/api/login/verify): sprawdź asercję pod kątem wyzwania sesji i zapisanego klucza publicznego. Jeśli identyfikatora danych logowania nie ma w bazie danych, zwróć stan HTTP404, aby klient mógł powiadomić menedżera haseł za pomocąPublicKeyCredential.signalUnknownCredential(). - Weryfikacja hasła (
/api/login/password): sprawdźusername(cred.id) ipassword(cred.password) w bazie danych użytkowników i utwórz uwierzytelnioną sesję.
Implementacja po stronie klienta
Ujednolicona prośba o dane logowania
- Żądanie kluczy dostępu i haseł: zdekoduj opcje serwera za pomocą
PublicKeyCredential.parseRequestOptionsFromJSON()i wywołajnavigator.credentials.get()w ramach modułu obsługi kliknięć zainicjowanego przez użytkownika za pomocąpassword: true,publicKeyiuiMode: 'immediate'(bez przekazywaniasignal). - Weryfikacja oddziału przez firmę
cred.type:- Jeśli
cred.type === 'public-key', zserializuj asercję za pomocącred.toJSON()i wyślij ją do punktu końcowego weryfikacji WebAuthn za pomocą żądania POST. Gdy serwer odpowie kodem HTTP404, wywołajPublicKeyCredential.signalUnknownCredential({ rpId: publicKey.rpId, credentialId: encoded.id }). - Jeśli
cred.type === 'password', wyślij żądanie POST{ username: cred.id, password: cred.password }do punktu końcowego weryfikacji hasła.
- Jeśli
- Obsługa błędu
NotAllowedError, gdy nie ma lokalnych danych logowania: przechwyć błądNotAllowedError(zgłaszany natychmiast, gdy na urządzeniu nie ma lokalnych kluczy dostępu ani zapisanych haseł lub gdy użytkownik zamknie selektor) i przejdź do standardowego procesu logowania (np. otwórz wbudowane okno logowania lub przejdź do/signinz adresem URL powrotu).
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;
}
});
Strategie zastępcze
Natychmiastowe przełączenie na tryb interfejsu
- Progresywne ulepszanie: traktuj
uiMode: 'immediate'jako progresywne ulepszanie. Nie wnioskuj o obsłudze na podstawie nazwy lub wersji przeglądarki. Jedynym wiarygodnym sygnałem obsługi jest sprawdzenie środowiska wykonawczegocapabilities.immediateGetza pomocąPublicKeyCredential.getClientCapabilities(). OBOWIĄZKOWO: zawsze sprawdzajcapabilities.immediateGetprzed wywołaniemnavigator.credentials.get({ uiMode: 'immediate' })– przeglądarki, które nie rozpoznająuiMode: 'immediate', ignorują nieznany klucz słownika i otwierają blokujące modalne okno dialogowe klucza dostępu i kodu QR. Jeślicapabilities.immediateGetjest fałszem lub gdynavigator.credentials.get()odrzuca zNotAllowedError, wróć do standardowego sposobu logowania się w aplikacji (np. otwórz wbudowane okno logowania lub przejdź do specjalnej strony logowania z parametrem adresu URL powrotu), aby użytkownicy nieobsługiwanych przeglądarek widzieli dotychczasowy proces logowania bez zmian:
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' })
}
Zastępcze wykrywanie możliwości klienta
Zawsze instaluj 'webauthn-polyfills' i importuj go w module klienta, aby PublicKeyCredential.getClientCapabilities() był dostępny, gdy PublicKeyCredential jest obsługiwany:
import 'webauthn-polyfills';
Zastępcze dane logowania za pomocą hasła
Przekazywanie password: true wraz z publicKey w navigator.credentials.get() umożliwia obsługiwanym przeglądarkom uwzględnianie haseł zapisanych w menedżerze haseł przeglądarki w ujednoliconym selektorze konta.
- Działanie zastępcze: przeglądarki, które nie obsługują
PasswordCredential, ignorują opcjępassword: truelub po sprawdzeniu funkcjicapabilities.immediateGetwracają do standardowego formularza logowania, w którym<input autocomplete="username webauthn">i<input type="password" autocomplete="current-password">zapewniają natywne autouzupełnianie haseł.
Łatwa serializacja JSON w przypadku błędu
Zawsze instaluj 'webauthn-polyfills' i importuj go w pakiecie klienta, aby funkcje PublicKeyCredential.parseRequestOptionsFromJSON() i PublicKeyCredential.prototype.toJSON() działały spójnie w przeglądarkach obsługujących PublicKeyCredential:
import 'webauthn-polyfills';
Zastępcza synchronizacja interfejsu Signal API
Interfejs WebAuthn Signal API (webauthn-signals) to progresywne ulepszenie, które usuwa nieaktualne klucze dostępu z menedżera haseł użytkownika, gdy serwer odpowiada kodem HTTP 404.
- Działanie zastępcze: połączenia są blokowane przez
if (PublicKeyCredential.signalUnknownCredential). Jeśli nie jest obsługiwany, pomiń wywołanie sygnału i wyświetl jasny komunikat, który pomoże użytkownikowi zalogować się za pomocą innej metody.