
WebAuthn e Passkey: come funziona davvero l'autenticazione senza password
Come funziona il protocollo WebAuthn dietro le passkey — chiavi pubbliche, attestation, un esempio pratico — e perché non sono solo un'alternativa più comoda alla password.
“Niente più password” è il modo in cui le passkey vengono vendute al grande pubblico, e non è sbagliato, ma è la parte meno interessante della storia. La parte interessante è perché eliminare la password elimina anche intere categorie di attacco che nessuna policy su lunghezza minima o scadenza forzata ha mai davvero risolto: phishing, credential stuffing, database di password rubati che si rivelano utili altrove. Capire il protocollo sotto una passkey aiuta a capire perché.
Il problema che la password porta sempre con sé
Una password è un segreto condiviso: sia tu che il server la conoscete (o dovreste — nella pratica il server ne conosce solo l’hash, ma il principio resta). Ogni segreto condiviso è qualcosa che può essere copiato, intercettato, riutilizzato altrove se l’utente lo riusa su più siti, o semplicemente digitato dentro un sito di phishing che sembra identico a quello vero.
WebAuthn (lo standard W3C dietro le passkey) elimina il segreto condiviso: usa crittografia a chiave pubblica. Il dispositivo dell’utente genera una coppia di chiavi — pubblica e privata — specifica per ogni sito. La chiave privata non lascia mai il dispositivo (spesso vive in un secure enclave hardware). Il server conserva solo la chiave pubblica, che è per definizione inutile a un attaccante: sapere la chiave pubblica di qualcuno non permette di autenticarsi al suo posto.
Registrazione: creare una passkey
const options = await fetch('/webauthn/register-options').then((r) => r.json());
const credential = await navigator.credentials.create({
publicKey: {
challenge: base64ToBuffer(options.challenge),
rp: { name: 'La Mia App', id: 'esempio.com' },
user: {
id: base64ToBuffer(options.userId),
name: '[email protected]',
displayName: 'Nome Utente',
},
pubKeyCredParams: [{ alg: -7, type: 'public-key' }], // ES256
authenticatorSelection: { residentKey: 'required', userVerification: 'required' },
},
});
await fetch('/webauthn/register', {
method: 'POST',
body: JSON.stringify({ credential: credentialToJSON(credential) }),
});
Il challenge è un valore casuale generato dal server, univoco per quella richiesta — impedisce a un attaccante di riusare una risposta catturata in precedenza (lo stesso principio del nonce in una Content Security Policy: un valore che non si può predire e non si può riutilizzare). L’utente conferma con l’autenticatore del dispositivo — Face ID, un’impronta, una chiave di sicurezza fisica — e solo a quel punto il browser genera la coppia di chiavi e restituisce la chiave pubblica da salvare lato server.
🔎 Nel dettaglio
userVerification: 'required' non è opzionale nella pratica. Questo parametro controlla se l’autenticatore deve verificare che sia davvero l’utente proprietario (biometria, PIN) o solo che il dispositivo sia presente fisicamente. Per l’autenticazione principale di un account, 'required' è quello che vuoi: senza, un dispositivo sbloccato rubato basterebbe.
Login: dimostrare il possesso della chiave privata, senza mai rivelarla
const options = await fetch('/webauthn/login-options').then((r) => r.json());
const assertion = await navigator.credentials.get({
publicKey: {
challenge: base64ToBuffer(options.challenge),
rpId: 'esempio.com',
userVerification: 'required',
},
});
await fetch('/webauthn/login', {
method: 'POST',
body: JSON.stringify({ assertion: assertionToJSON(assertion) }),
});
Il dispositivo firma il challenge con la chiave privata. Il server verifica la firma usando la chiave pubblica salvata in fase di registrazione. Se la firma è valida, sa con certezza crittografica che la richiesta viene da chi possiede quella chiave privata — senza che nessun segreto abbia mai viaggiato sulla rete, e quindi senza nulla da rubare via phishing: anche se un utente venisse convinto a visitare un sito falso identico all’originale, il browser genererebbe una firma legata al dominio reale usato in fase di registrazione (rpId), inutilizzabile sul sito falso.
🔎 Nel dettaglio
Il binding al dominio è la vera protezione anti-phishing, non un dettaglio implementativo: rpId lega la credenziale a uno specifico dominio in modo verificato dal browser stesso, non dal tuo codice applicativo. Un sito di phishing su un dominio diverso, per quanto simile visivamente, semplicemente non può richiedere una firma valida per il dominio originale. È la differenza sostanziale rispetto a una password, che l’utente può digitare ovunque gli venga chiesta, incluso un sito falso.
Attestation: quanto ti fidi del dispositivo che genera la chiave
Un dettaglio che le guide introduttive spesso saltano: in fase di registrazione, l’autenticatore può fornire una attestation — una prova crittografica firmata dal produttore del dispositivo che attesta che tipo di autenticatore ha generato quella chiave (una YubiKey fisica, il secure enclave di un telefono, un password manager software). Per un’app consumer normale, l’attestation quasi sempre non serve: fidarsi del fatto che qualche autenticatore abbia generato la chiave è sufficiente. Per contesti con requisiti di compliance specifici (accesso a sistemi bancari, governativi, dove serve garantire hardware certificato) l’attestation diventa il meccanismo per verificarlo — ma richiede di mantenere un elenco aggiornato di produttori/modelli fidati, un costo operativo non banale.
Il caso reale che le guide veloci non coprono: cosa succede quando l’utente perde il dispositivo
Qui sta il problema pratico più sottovalutato. Una password puoi recuperarla via email. Una chiave privata legata all’hardware, per definizione, non è recuperabile — è il punto di forza (non copiabile) che diventa anche il punto debole del recovery.
Le soluzioni in uso oggi sono due, spesso combinate:
- Sincronizzazione cloud della passkey (iCloud Keychain, Google Password Manager): la chiave privata viene sincronizzata, cifrata, tra i dispositivi dello stesso ecosistema dell’utente. Comodo, ma sposta la superficie di attacco sulla sicurezza dell’account cloud che fa da sincronizzazione.
- Registrare più passkey per lo stesso account fin dall’inizio (telefono + chiave fisica di backup), esattamente come si consiglierebbe di avere più di un metodo di recovery per qualunque sistema critico.
💡 Consiglio
Non eliminare il fallback troppo in fretta. Nella fase di transizione — probabilmente ancora per diversi anni — ha senso offrire le passkey come opzione principale ma non come unico metodo, con un fallback verificato (email + secondo fattore, non solo email da sola) per chi perde l’accesso a tutti i propri dispositivi registrati. Un sistema di autenticazione perfetto sulla carta ma che blocca fuori un utente legittimo senza via di recupero è, in pratica, peggio di uno più semplice ma recuperabile.
Da dove iniziare
Implementare WebAuthn a mano, gestendo challenge, encoding e verifica delle firme direttamente, è possibile ma sconsigliato per un progetto reale: librerie mature come @simplewebauthn/server (Node.js) gestiscono correttamente i dettagli critici — validazione del challenge, verifica della firma, gestione dei formati di attestation — che sbagliati a mano diventano vulnerabilità silenziose, non errori evidenti in fase di test.