Uwierzytelnianie za pomocą klucza dostępu

Z tego przewodnika dowiesz się, jak wdrożyć uwierzytelnianie powracających użytkowników za pomocą danych logowania możliwych do wykrycia, zarówno za pomocą wyraźnych przycisków, jak i płynnych sugestii autouzupełniania w przeglądarce (warunkowy interfejs użytkownika).

Po stronie serwera

Generowanie opcji

Utwórz punkt końcowy, który generuje parametry żądania WebAuthn przy użyciu sprawdzonej biblioteki zgodnie ze standardami.

  1. Użyj wstępnie zdefiniowanego identyfikatora RP: użyj wstępnie zdefiniowanego prawidłowego identyfikatora RP jako stałego ciągu znaków.
  2. Wygeneruj wyzwanie: wygeneruj bezpieczny kryptograficznie bufor losowy o wysokiej entropii, zapisz go bezpiecznie w sesji użytkownika i zakoduj jako Base64URL.
  3. Mapowanie danych logowania możliwych do wykrycia: określ pustą tablicę [] dla allowCredentials. W ten sposób żądane są dane logowania możliwe do wykrycia, co oznacza, że użytkownik nie musi najpierw wpisywać swojej nazwy użytkownika. Dostawca klucza dostępu wyświetli dostępne konta.
  4. Poziom weryfikacji użytkownika: ustaw userVerification: "preferred" (lub "required", jeśli jest to wyraźnie wymagane przez zasady zgodności firmy).
    • Żądany poziom ograniczenia userVerification MUSI być utrwalony w rekordzie sesji serwera w punkcie końcowym opcji, a nie przekazywany z powrotem od klienta za pomocą ciągów zapytania. Dzięki temu punkt końcowy weryfikacji może bezpiecznie egzekwować rygorystyczne ograniczenia dopasowania bez ryzyka manipulacji ze strony klienta.
// 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";

Punkt końcowy weryfikacji

Bezpiecznie zweryfikuj asercję zwróconą przez klienta, aby uwierzytelnić użytkownika:

  1. Weryfikacja testu sesji: wymuszaj ścisłe dopasowanie testu między odpowiedzią klienta a oczekiwanym testem zapisanym w sesji.
  2. Enforce UV Preferences (Wymuszaj ustawienia UV):
    • Zezwalaj na uwierzytelnianie bez weryfikacji użytkownika (np. gdy blokady ekranu uwierzytelniania są wyłączone), jeśli sesja expectedUserVerification żąda "preferred", przekazując requireUserVerification: false do biblioteki weryfikacji po stronie serwera. Jeśli pojawi się prośba "required", wymagaj podania danych biometrycznych lub kodu PIN.
  3. Błąd serwera 404: jeśli identyfikator danych logowania zwrócony przez klienta nie zostanie znaleziony w bazie danych, zwróć błąd HTTP 404, aby klient mógł wywołać interfejs Signal API.

Logika po stronie klienta

Adnotacja formularza HTML

Dodaj adnotacje do pól nazwy użytkownika i hasła, aby natywnie korzystać z warunkowego interfejsu użytkownika. Tokeny autouzupełniania łączą parametry specyfikacji webauthn, a autofokus uruchamia wyskakujące okienko autouzupełniania przeglądarki natychmiast po ustawieniu fokusu na polu wejściowym.

<!-- 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>

Explicit Button Flow

Uruchom uwierzytelnianie za pomocą klucza dostępu, gdy użytkownik kliknie przycisk „Zaloguj się za pomocą klucza dostępu”. Przed wywołaniem prośby o klucz dostępu przerwij wszystkie trwające wywołania autouzupełniania formularza (warunkowe żądanie GET).

Przepływ zapośredniczenia warunkowego (autouzupełnianie formularza)

Aktywuj sugestie autouzupełniania formularzy podczas wczytywania strony, aby oferować uwierzytelnianie za pomocą klucza dostępu, gdy użytkownicy skupiają się na polach logowania:

  1. Wykrywanie funkcji: wywołaj PublicKeyCredential.getClientCapabilities() podczas wczytywania strony i pomiń logowanie się za pomocą klucza dostępu, jeśli funkcja conditionalGet jest niedostępna.
  2. Opcje dekodowania: dekoduj pobrany obiekt JSON z danymi logowania za pomocą PublicKeyCredential.parseRequestOptionsFromJSON().
  3. Wywołaj warunkowe pobieranie: wywołaj navigator.credentials.get() z mediation: "conditional" i przekaż sygnał AbortController. Spowoduje to zarejestrowanie autouzupełniania bez wyświetlania okna klucza dostępu.
  4. Oddzielenie wyjątków try/catch: umieść wywołanie navigator.credentials.get w bloku try/catch:
    • NotAllowedError: użytkownik anulował prośbę o zalogowanie się za pomocą klucza dostępu lub upłynął limit czasu na odpowiedź.
    • AbortError: żądanie uwierzytelnienia zostało anulowane automatycznie.
  5. Call Signal API: umieść wywołanie weryfikacji po stronie serwera fetch() w bloku try/catch:
    • Wyświetl komunikat o błędzie, aby użytkownik wiedział, co poszło nie tak.
    • Wywołaj signalUnknownCredential() TYLKO wtedy, gdy serwer wyraźnie odpowie kodem stanu HTTP 404 (nie znaleziono danych logowania), a użytkownik nie jest uwierzytelniony.
    • Parametr credentialId przekazywany do signalUnknownCredential() MUSI być ciągiem znaków identyfikatora danych logowania zakodowanym w standardzie Base64URL (np. encoded.id), a NIE nieprzetworzonym obiektem ArrayBuffer credential.rawId.
  6. Zakoduj odpowiedź: zakoduj dane logowania AuthenticatorAssertionResponse za pomocą .toJSON(), zanim wyślesz je na serwer w celu weryfikacji.
// 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);

Strategie zastępcze

Zastępcze wykrywanie funkcji klucza dostępu

Zawsze instaluj „webauthn-polyfills” i importuj je w kontekście. Pamiętaj, że jeśli obsługiwany jest format PublicKeyCredential, obsługiwany jest też format PublicKeyCredential.getClientCapabilities.

import 'webauthn-polyfills';

Zastępcza synchronizacja interfejsu Signal API

Interfejs WebAuthn Signal API (webauthn-signals) to progresywna optymalizacja, która służy do synchronizowania menedżerów haseł ze stanem danych logowania na serwerze.

  • Fallback Experience: Gated via if (PublicKeyCredential.signalUnknownCredential). Jeśli nie jest obsługiwana, synchronizacja weryfikacji w tle jest pomijana bez zgłaszania wyjątków przeglądarki.

Łatwa serializacja JSON – kreacja zastępcza

Zawsze instaluj „webauthn-polyfills” i importuj je w kontekście. Jeśli obsługiwany jest format PublicKeyCredential, obsługiwane są też formaty PublicKeyCredential.parseRequestOptionsFromJSON i PublicKeyCredential.prototype.toJSON.

import 'webauthn-polyfills';