In deze handleiding staat hoe je gebruikers een toegangssleutel voor hun account laat registreren. Zo kunnen ze inloggen zonder wachtwoord, wat een zeer veilige, phishingbestendige methode is.
Databasevereisten
Als je registraties van toegangssleutels wilt ondersteunen, moet je tabel met databasetoegangscodes de volgende velden opslaan:
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
}
Aan de serverzijde
Opties genereren
Maak een eindpunt dat WebAuthn-parameters voor maken genereert. Vertrouw op een gecontroleerde bibliotheek volgens de categorienormen in plaats van handmatig cryptografie te schrijven.
- De vooraf gedefinieerde RP-ID gebruiken: Gebruik de vooraf gedefinieerde juiste RP-ID als een constante tekenreeks.
- Maak een beveiligde challenge: Genereer een cryptografisch beveiligde willekeurige buffer met hoge entropie op de server, sla deze beveiligd op in de sessie van de gebruiker en codeer deze als Base64URL voor de levering van opties.
- Dubbele toegangssleutels voorkomen: Wijs de bestaande vooraf geregistreerde inloggegevens-ID's van de gebruiker toe aan de matrix met
excludeCredentials-opties. Zo voorkom je dat de authenticator dubbele inloggegevens registreert voor hetzelfde account van de toegangssleutelprovider. - Vindbare inloggegevens afdwingen: Stel
requireResidentKey: trueenresidentKey: "required"in de opties vanauthenticatorSelectionin om vindbare inloggegevens aan te vragen. Dit is nodig voor vindbare logins. - Gebruikersverificatie instellen: Geef
userVerification: "preferred"ofuserVerification: "required"op. Voor veel use cases voor naleving (bijvoorbeeld financiën, gezondheidszorg) is'required'vereist om gebruikersverificatie af te dwingen bij het maken. - Het bereik van de bijlage bepalen:
- Promotieflow: Als je voorstelt om een toegangssleutel te maken direct na standaard inloggen met een wachtwoord of promoties na aanmelding, stel je
authenticatorAttachment: "platform"in om de platform-authenticator af te dwingen en prompts voor externe beveiligingssleutels over te slaan. - Beheerproces: Als de aanroep wordt uitgevoerd vanuit een speciaal instellingen- of beveiligingsvenster waarin externe beveiligingssleutels worden ondersteund naast de platformverificator, laat je de property
authenticatorAttachmenthelemaal weg. - Tip: Accepteer een
promotion: boolean-verzoekmarkering om beide stromen voorwaardelijk te verwerken met één eindpunt.
- Promotieflow: Als je voorstelt om een toegangssleutel te maken direct na standaard inloggen met een wachtwoord of promoties na aanmelding, stel je
// 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" }),
},
};
Verificatie
- Challengeverificatie: Verifieer de challenge op een beveiligde manier aan de hand van de verwachte sessiegebonden challenge.
- Aanwezigheid van gebruiker verifiëren:
- Zorg dat de UP-markering (User Present) die wordt geretourneerd in de geparseerde verificatorgegevens
trueis om de fysieke aanwezigheid van de gebruiker op het moment van maken te bevestigen.
- Zorg dat de UP-markering (User Present) die wordt geretourneerd in de geparseerde verificatorgegevens
- Verificatie versoepelen voor 'preferred':
- Als de aanmaakopties
userVerification: "preferred"hebben opgegeven, MOET de aanroep voor verificatie aan de serverzijde worden ingesteld metrequireUserVerification: false. Anders veroorzaken authenticators die zich registreren zonder gebruikersverificatie (bijvoorbeeld als schermvergrendeling uitstaat) onterechte fouten bij de serververificatie.
- Als de aanmaakopties
Logica aan de clientzijde
- De UI blokkeren bij het laden van de pagina:
- Roep bij het laden van de pagina
PublicKeyCredential.getClientCapabilities()aan en zet de knop Toegangssleutel maken uit alsconditionalGetofpasskeyPlatformAuthenticatorniet beschikbaar is.
- Roep bij het laden van de pagina
- Aanroepen en serialiseren: Decodeer serveropties met
PublicKeyCredential.parseCreationOptionsFromJSON()en geef de resulterende configuratie door aannavigator.credentials.create().- Roep
credential.toJSON()aan om deAuthenticatorAttestationResponsete coderen in een geldig, json-serialiseerbaar object voordat u het verificatie-eindpunt ophaalt.
- Roep
- WebAuthn-uitzonderingen verwerken:
InvalidStateError: Er bestaat al een overeenkomende toegangssleutel (overeenkomst op basis vanexcludeCredentials).NotAllowedError: De gebruiker heeft het dialoogvenster voor de verificatietoegangssleutel geannuleerd of er is een time-out opgetreden.AbortError: De bewerking is afgebroken.SecurityError: Fouten met beveiligde herkomsten (HTTPS) of niet-overeenkomende RP-ID's (configuratieproblemen).
- Try/Catch-scheiding voor Signal API:
- Plaats de aanroep voor serververificatie
fetch()in een try/catch-blok. RoepsignalUnknownCredential()aan als het ophalen van de serververificatie mislukt (elke statusresponse.ok === falseof netwerkfout).
- Plaats de aanroep voor serververificatie
// 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
});
}
}
Reservestrategieën
Terugvaloptie voor synchronisatie van Signal API
De WebAuthn Signal API (webauthn-signals) is een progressieve optimalisatie die wordt gebruikt om wachtwoordmanagers gesynchroniseerd te houden met de status van de servergegevens.
- Fallback-functionaliteit: Als
PublicKeyCredential.signalUnknownCredentialniet wordt ondersteund door de browser, MOET het gesprek veilig worden omzeild via functieherkenning (if (PublicKeyCredential.signalUnknownCredential)). De verificatie aan de serverzijde registreert de fout dan zonder managerupdates te activeren.
Easy JSON Serialization Fallback
Installeer altijd 'webauthn-polyfills' en importeer dit in de context.
Zolang PublicKeyCredential wordt ondersteund, worden PublicKeyCredential.parseCreationOptionsFromJSON en PublicKeyCredential.prototype.toJSON ook ondersteund.
import 'webauthn-polyfills';