Questa guida spiega in dettaglio come consentire agli utenti di registrare una passkey per il proprio account, fornendo un'alternativa di accesso senza password altamente sicura e resistente al phishing.
Requisiti del database
Per supportare le registrazioni delle passkey, la tabella delle credenziali del database deve memorizzare i seguenti campi:
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
}
Lato server
Generazione di opzioni
Crea un endpoint che genera i parametri di creazione di WebAuthn. Affidati a una libreria verificata in base agli standard di categoria anziché alla crittografia manuale.
- Utilizza l'ID RP predefinito: utilizza l'ID RP corretto predefinito come stringa costante.
- Crea una verifica sicura: genera un buffer casuale ad alta entropia e crittograficamente sicuro sul server, memorizzalo in modo sicuro nella sessione dell'utente e codificalo come Base64URL per la distribuzione delle opzioni.
- Evita passkey duplicate: mappa gli ID delle credenziali preregistrate esistenti dell'utente all'array di opzioni
excludeCredentials. In questo modo, l'autenticatore non registra credenziali duplicate sullo stesso account del provider di passkey. - Forza le credenziali rilevabili: imposta
requireResidentKey: trueeresidentKey: "required"nelle opzioniauthenticatorSelectionper richiedere una credenziale rilevabile, necessaria per gli accessi rilevabili. - Configura la verifica dell'utente: specifica
userVerification: "preferred"ouserVerification: "required". Molti casi d'uso di conformità (ad es. finanza, sanità) richiedono che'required'applichi la verifica dell'utente al momento della creazione. - Determina l'ambito dell'allegato:
- Flusso di promozione: quando proponi la creazione di passkey subito dopo gli accessi con password standard o le promozioni post-registrazione, imposta
authenticatorAttachment: "platform"per applicare l'autenticatore della piattaforma e ignorare i prompt dei token di sicurezza esterni. - Flusso di gestione: se la chiamata viene effettuata da un pannello di sicurezza o di impostazioni dedicato in cui sono supportati i token di sicurezza esterni oltre all'autenticatore della piattaforma, ometti completamente la proprietà
authenticatorAttachment. - Suggerimento: accetta un flag di richiesta
promotion: booleanper gestire in modo condizionale entrambi i flussi con un singolo endpoint.
- Flusso di promozione: quando proponi la creazione di passkey subito dopo gli accessi con password standard o le promozioni post-registrazione, imposta
// 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" }),
},
};
Verifica
- Verifica della verifica: verifica in modo sicuro la verifica rispetto alla verifica prevista associata alla sessione.
- Verifica la presenza dell'utente:
- Assicurati che il flag User Present (UP) restituito nei dati dell'autenticatore analizzati sia
trueper confermare la presenza fisica dell'utente al momento della creazione.
- Assicurati che il flag User Present (UP) restituito nei dati dell'autenticatore analizzati sia
- Verifica meno rigida per "preferred":
- Quando le opzioni di creazione specificano
userVerification: "preferred", la chiamata di verifica lato server DEVE essere configurata conrequireUserVerification: false. In caso contrario, gli autenticatori che si registrano senza verifica dell'utente (ad es. con il blocco schermo disattivato) attiveranno errori di verifica del server fittizi.
- Quando le opzioni di creazione specificano
Logica lato client
- Blocca l'UI al caricamento pagina:
- Al caricamento della pagina, chiama
PublicKeyCredential.getClientCapabilities()e disattiva il pulsante "Crea passkey" seconditionalGetopasskeyPlatformAuthenticatornon sono disponibili.
- Al caricamento della pagina, chiama
- Richiama la creazione e la serializzazione: decodifica le opzioni del server con
PublicKeyCredential.parseCreationOptionsFromJSON()e passa la configurazione risultante anavigator.credentials.create().- Chiama
credential.toJSON()per codificareAuthenticatorAttestationResponsein un oggetto valido serializzabile in JSON prima di recuperare l'endpoint di verifica.
- Chiama
- Gestisci le eccezioni WebAuthn:
InvalidStateError: esiste già una passkey corrispondente (corrispondenza perexcludeCredentials).NotAllowedError: l'utente ha annullato o è scaduto il tempo della finestra di dialogo della passkey di autenticazione.AbortError: l'operazione è stata interrotta.SecurityError: origini sicure (HTTPS) o errori di mancata corrispondenza dell'ID RP (problemi di configurazione).
- Separazione Try/Catch per l'API Signal:
- Inserisci la chiamata di verifica del server
fetch()in un blocco try/catch. ChiamasignalUnknownCredential()quando il recupero della verifica del server non va a buon fine (qualsiasi statoresponse.ok === falseo eccezione di rete).
- Inserisci la chiamata di verifica del server
// 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 di fallback
Sincronizzazione di riserva dell'API Signal
L'API WebAuthn Signal (webauthn-signals) è un'ottimizzazione progressiva utilizzata per mantenere i gestori delle password sincronizzati con lo stato delle credenziali del server.
- Esperienza di riserva: se
PublicKeyCredential.signalUnknownCredentialnon è supportato dal browser, la chiamata DEVE essere ignorata in modo sicuro tramite il controllo del rilevamento delle funzionalità (if (PublicKeyCredential.signalUnknownCredential)) e la verifica lato server registra semplicemente l'errore senza attivare gli aggiornamenti del gestore.
Easy JSON Serialization Fallback
Installa sempre "webauthn-polyfills" e importalo nel contesto.
Tieni presente che, se PublicKeyCredential è supportato, lo sono anche PublicKeyCredential.parseCreationOptionsFromJSON e PublicKeyCredential.prototype.toJSON.
import 'webauthn-polyfills';