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.
- Użyj wstępnie zdefiniowanego identyfikatora RP: użyj wstępnie zdefiniowanego prawidłowego identyfikatora RP jako stałego ciągu znaków.
- 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.
- 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. - Wymuszanie danych logowania możliwych do znalezienia: ustaw
requireResidentKey: trueiresidentKey: "required"w opcjachauthenticatorSelection, 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. - Skonfiguruj weryfikację użytkownika: określ
userVerification: "preferred"lubuserVerification: "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. - 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.
- Proces promowania: gdy proponujesz utworzenie klucza dostępu bezpośrednio po standardowym logowaniu się za pomocą hasła lub po rejestracji, ustaw
// 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
- Weryfikacja testu: bezpiecznie weryfikuj test pod kątem oczekiwanego testu powiązanego z sesją.
- 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.
- Sprawdź, czy flaga obecności użytkownika (UP) zwrócona w przeanalizowanych danych uwierzytelniających ma wartość
- Zmniejszenie rygorystyczności weryfikacji w przypadku „preferowanych”:
- Gdy opcje tworzenia określają
userVerification: "preferred", wywołanie weryfikacji po stronie serwera MUSI być skonfigurowane zrequireUserVerification: 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.
- Gdy opcje tworzenia określają
Logika po stronie klienta
- Blokowanie interfejsu podczas wczytywania strony:
- Podczas wczytywania strony wywołaj
PublicKeyCredential.getClientCapabilities()i wyłącz przycisk „Utwórz klucz dostępu”, jeśliconditionalGetlubpasskeyPlatformAuthenticatornie jest dostępny.
- Podczas wczytywania strony wywołaj
- Wywołaj tworzenie i serializację: zdekoduj opcje serwera za pomocą
PublicKeyCredential.parseCreationOptionsFromJSON()i przekaż wynikową konfigurację donavigator.credentials.create().- Wywołaj
credential.toJSON(), aby zakodowaćAuthenticatorAttestationResponsew prawidłowym obiekcie możliwym do serializacji w formacie JSON przed pobraniem punktu końcowego weryfikacji.
- Wywołaj
- 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ą).
- Rozdzielenie try/catch w przypadku interfejsu Signal API:
- Umieść wywołanie weryfikacji serwera
fetch()w bloku try/catch. WywołajsignalUnknownCredential(), gdy nie uda się pobrać weryfikacji serwera (dowolny stanresponse.ok === falselub błędy sieci).
- Umieść wywołanie weryfikacji serwera
// 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';