Szybkie logowanie w trybie natychmiastowego interfejsu

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.

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 witrynie NotAllowedError.
  • 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łąd NotAllowedError, 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' z autocomplete="username webauthn") w polach formularza połączonych ze standardowym przyciskiem modalnym navigator.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 listy allowCredentials, którą tryb natychmiastowego interfejsu odrzuca z błędem NotAllowedError. Pominięcie allowCredentials pozwolił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. detektor click w 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 pola publicKey.allowCredentials, gdy określono parametr uiMode: 'immediate'. Przeglądarki natychmiast odrzucają każde żądanie trybu natychmiastowego interfejsu z niepustą listą allowCredentials, zwracając błąd NotAllowedError, 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ści signal w navigator.credentials.get(), gdy określono uiMode: '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łaj abort() w tym warunkowym interfejsie użytkownika AbortController przed wywołaniem trybu natychmiastowego interfejsu użytkownika i pomiń signal w 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:

  1. Wygeneruj wyzwanie powiązane z sesją: utwórz kryptograficznie losowy bufor wyzwania, zapisz go w sesji serwera i zakoduj w formacie Base64URL.
  2. Utrzymywanie preferencji weryfikacji użytkownika: ustaw userVerification: 'preferred' i zapisz expectedUserVerification: 'preferred' w sesji.
  3. Pomiń allowCredentials: nie uwzględniaj niepustej listy allowCredentials w opcjach trybu natychmiastowego interfejsu, ponieważ przeglądarki odrzucają niepuste listy dozwolonych z 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);
});

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:

  1. 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 HTTP 404, aby klient mógł powiadomić menedżera haseł za pomocą PublicKeyCredential.signalUnknownCredential().
  2. Weryfikacja hasła (/api/login/password): sprawdź username (cred.id) i password (cred.password) w bazie danych użytkowników i utwórz uwierzytelnioną sesję.

Implementacja po stronie klienta

Ujednolicona prośba o dane logowania

  1. Żądanie kluczy dostępu i haseł: zdekoduj opcje serwera za pomocą PublicKeyCredential.parseRequestOptionsFromJSON() i wywołaj navigator.credentials.get() w ramach modułu obsługi kliknięć zainicjowanego przez użytkownika za pomocą password: true, publicKey i uiMode: 'immediate' (bez przekazywania signal).
  2. 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 HTTP 404, wywołaj PublicKeyCredential.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.
  3. Obsługa błędu NotAllowedError, gdy nie ma lokalnych danych logowania: przechwyć błąd NotAllowedError (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 /signin z 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 wykonawczego capabilities.immediateGet za pomocą PublicKeyCredential.getClientCapabilities(). OBOWIĄZKOWO: zawsze sprawdzaj capabilities.immediateGet przed wywołaniem navigator.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śli capabilities.immediateGet jest fałszem lub gdy navigator.credentials.get() odrzuca z NotAllowedError, 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: true lub po sprawdzeniu funkcji capabilities.immediateGet wracają 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.