Rejestracja klucza dostępu

Z tego przewodnika dowiesz się, jak umożliwić użytkownikom zarejestrowanie klucza dostępu na koncie, co zapewni im wysoce bezpieczną, odporną na phishing alternatywę logowania bez hasła.

Wymagania dotyczące bazy danych

Aby obsługiwać rejestrację kluczy dostępu, tabela danych logowania w bazie danych musi zawierać te pola:

export interface StoredPasskeyCredential {
  id: string; // Base64URL-encoded credential ID (unique lookup key)
  passkeyUserId: string; // Associated application user ID
  credentialPublicKey: string; // Base64URL-encoded public key used to verify assertion signatures
  credentialType: "public-key";
  credentialDeviceType: "singleDevice" | "multiDevice"; // Helps distinguish device-bound vs cloud-synced passkeys
  credentialBackedUp: boolean; // Boolean backup state reported by the authenticator
  aaguid: string; // Authenticator Attestation GUID
  providerIcon?: string; // Provider icon derived from the AAGUID registry (dark or light theme URLs)
  name: string; // Provider name derived from AAGUID registry
  transports: string[]; // Array of transport names (e.g. 'internal', 'hybrid') necessary for exclusion options
  lastUsedAt?: number; // Optional epoch timestamp of last sign-in
  registeredAt: number; // Registration epoch timestamp
  counter: number; // Authenticator sign-in signature counter used to prevent replay attacks
}

Po stronie serwera

Generowanie opcji

Utwórz punkt końcowy, który generuje parametry tworzenia WebAuthn. Zamiast samodzielnie tworzyć kryptografię, korzystaj ze sprawdzonej biblioteki zgodnej ze standardami kategorii.

  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. Utworzenie bezpiecznego wyzwania: wygeneruj na serwerze losowy bufor o wysokiej entropii, który jest bezpieczny kryptograficznie, zapisz go bezpiecznie w sesji użytkownika i zakoduj jako Base64URL na potrzeby dostarczania opcji.
  3. Unikanie duplikowania kluczy dostępu: zmapuj istniejące identyfikatory danych logowania użytkownika zarejestrowane z wyprzedzeniem na tablicę opcji excludeCredentials. Zapobiega to rejestrowaniu przez aplikację uwierzytelniającą duplikatów danych logowania na tym samym koncie dostawcy kluczy dostępu.
  4. Wymuszanie danych logowania możliwych do znalezienia: ustaw requireResidentKey: true i residentKey: "required" w opcjach authenticatorSelection, aby poprosić o dane logowania możliwe do znalezienia, które są niezbędne do logowania się za pomocą danych logowania możliwych do znalezienia.
  5. Skonfiguruj weryfikację użytkownika: określ userVerification: "preferred" lub userVerification: "required". Wiele przypadków użycia związanych ze zgodnością (np. w branży finansowej czy opiece zdrowotnej) wymaga, aby usługa 'required' wymuszała weryfikację użytkownika podczas tworzenia.
  6. Określanie zakresu załącznika:
    • Proces promowania: gdy proponujesz utworzenie klucza dostępu bezpośrednio po standardowym logowaniu się za pomocą hasła lub po rejestracji, ustaw authenticatorAttachment: "platform", aby wymusić użycie uwierzytelniania platformy i pominąć prośby o zewnętrzny klucz bezpieczeństwa.
    • Proces zarządzania: gdy wywołanie następuje z dedykowanego panelu ustawień lub zabezpieczeń, w którym oprócz uwierzytelniania platformy obsługiwane są zewnętrzne klucze bezpieczeństwa, całkowicie pomiń właściwość authenticatorAttachment.
    • Wskazówka: zaakceptuj flagę żądania promotion: boolean, aby warunkowo obsługiwać oba przepływy za pomocą jednego punktu końcowego.
// Options generation example
const options = {
  challenge: serverGeneratedBase64UrlChallenge, // Cryptographically random challenge
  rp: { id: "example.com", name: "Secure Application" },
  user: {
    id: userBase64UrlId, // Unique base64url string identifying the account
    name: "user@example.com",
    displayName: "Jane Doe",
  },
  pubKeyCredParams: [
    {
      type: "public-key",
      alg: -7,
    },
    {
      type: "public-key",
      alg: -257,
    },
  ],
  excludeCredentials: userExistingCredentials.map((cred) => ({
    type: "public-key",
    id: cred.id,
    transports: cred.transports,
  })),
  authenticatorSelection: {
    residentKey: "required",
    requireResidentKey: true,
    userVerification: "preferred",
    ...(isPromotionFlow && { authenticatorAttachment: "platform" }),
  },
};

Weryfikacja

  1. Weryfikacja testu: bezpiecznie weryfikuj test pod kątem oczekiwanego testu powiązanego z sesją.
  2. Weryfikacja obecności użytkownika:
    • Sprawdź, czy flaga obecności użytkownika (UP) zwrócona w przeanalizowanych danych uwierzytelniających ma wartość true, aby potwierdzić fizyczną obecność użytkownika w momencie tworzenia.
  3. Zmniejszenie rygorystyczności weryfikacji w przypadku „preferowanych”:
    • Gdy opcje tworzenia określają userVerification: "preferred", wywołanie weryfikacji po stronie serwera MUSI być skonfigurowane z requireUserVerification: false. W przeciwnym razie uwierzytelnianie, które rejestruje się bez weryfikacji użytkownika (np. wyłączone blokady ekranu), spowoduje fałszywe błędy weryfikacji serwera.

Logika po stronie klienta

  1. Blokowanie interfejsu podczas wczytywania strony:
    • Podczas wczytywania strony wywołaj PublicKeyCredential.getClientCapabilities() i wyłącz przycisk „Utwórz klucz dostępu”, jeśli conditionalGet lub passkeyPlatformAuthenticator nie jest dostępny.
  2. Wywołaj tworzenie i serializację: zdekoduj opcje serwera za pomocą PublicKeyCredential.parseCreationOptionsFromJSON() i przekaż wynikową konfigurację do navigator.credentials.create().
    • Wywołaj credential.toJSON(), aby zakodować AuthenticatorAttestationResponse w prawidłowym obiekcie możliwym do serializacji w formacie JSON przed pobraniem punktu końcowego weryfikacji.
  3. Obsługa wyjątków WebAuthn:
    • InvalidStateError: pasujący klucz dostępu już istnieje (dopasowany przez: excludeCredentials).
    • NotAllowedError: użytkownik anulował okno dialogowe klucza uwierzytelniania lub upłynął limit czasu.
    • AbortError: operacja została przerwana.
    • SecurityError: błędy związane z bezpiecznymi punktami początkowymi (HTTPS) lub niezgodnością identyfikatora RP (problemy z konfiguracją).
  4. Rozdzielenie try/catch w przypadku interfejsu Signal API:
    • Umieść wywołanie weryfikacji serwera fetch() w bloku try/catch. Wywołaj signalUnknownCredential(), gdy nie uda się pobrać weryfikacji serwera (dowolny stan response.ok === false lub błędy sieci).
// optionsFetch and registerVerifyFetch are app-defined HTTP methods
import { optionsFetch, registerVerifyFetch } from "./api.js";

async function registerPasskey(isPromotion = false) {
  // Verify passkey capability and conditional UI are available
  const capabilities = await PublicKeyCredential.getClientCapabilities();
  if (
    !capabilities.passkeyPlatformAuthenticator ||
    !capabilities.conditionalGet
  ) {
    // Hide "Create passkey" buttons and fall back to password flows instead
    showStandardPasswordFallbackUI();
    return;
  }

  const creationOptionsJSON = await optionsFetch({ promotion: isPromotion });
  const publicKey =
    PublicKeyCredential.parseCreationOptionsFromJSON(creationOptionsJSON);

  let credential;
  try {
    // passkey prompt execution
    credential = await navigator.credentials.create({ publicKey });
  } catch (err) {
    if (err.name === "InvalidStateError") {
      console.log("A passkey already exists for this account.");
      alert("A passkey already exists for this account.");
    } else if (err.name === "SecurityError") {
      console.error("Configuration RP ID or Secure Context error.");
      alert("Configuration RP ID or Secure Context error.");
    } else if (err.name === "NotAllowedError") {
      console.log("User cancelled the passkey dialog.");
    } else if (err.name === "AbortError") {
      console.log("The creation operation has been aborted.");
    }
    return; // Safe API exit, do not signal unknown for standard WebAuthn cancels
  }

  // Server Verification phase (Segregated Try/Catch)
  let encodedResponse = credential.toJSON();
  try {
    const response = await registerVerifyFetch(encodedResponse);
    if (!response.ok) {
      // Server verification failed to verify/authenticate the credential (orphaned)
      await PublicKeyCredential.signalUnknownCredential({
        rpId, // RP ID must match the one defined on the server
        credentialId: encodedResponse.id, // Base64URL-encoded credential ID
      });
    }
  } catch (serverErr) {
    console.error("Server verification network failure:", serverErr);
    await publickeycredential.signalunknowncredential({
      rpId, // RP ID must match the one defined on the server
      credentialid: encodedresponse.id, // base64url-encoded credential id
    });
  }
}

Strategie zastępcze

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.

  • Działanie zastępcze: jeśli przeglądarka nie obsługuje PublicKeyCredential.signalUnknownCredential, wywołanie MUSI zostać bezpiecznie pominięte za pomocą bramkowania wykrywania funkcji (if (PublicKeyCredential.signalUnknownCredential)), a weryfikacja po stronie serwera po prostu rejestruje błąd bez wywoływania aktualizacji menedżera.

Ł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.parseCreationOptionsFromJSON i PublicKeyCredential.prototype.toJSON.

import 'webauthn-polyfills';