Panoramica
Ecco una panoramica generale dei passaggi principali dell'autenticazione tramite passkey:
- Definisci la verifica e altre opzioni necessarie per l'autenticazione con una passkey. Inviale al client in modo da poterle passare alla chiamata di autenticazione tramite passkey (
navigator.credentials.get
sul web). Dopo che l'utente conferma l'autenticazione tramite passkey, la chiamata di autenticazione tramite passkey viene risolta e restituisce una credenziale (PublicKeyCredential
). La credenziale contiene un'asserzione di autenticazione.
- Verifica l'asserzione di autenticazione.
- Se l'asserzione di autenticazione è valida, esegui l'autenticazione dell'utente.
Le sezioni seguenti illustrano le specifiche di ciascun passaggio.
Crea la sfida
In pratica, una challenge è un array di byte casuali, rappresentati come un oggetto ArrayBuffer
.
// Example challenge, base64URL-encoded
weMLPOSx1VfSnMV6uPwDKbjGdKRMaUDGxeDEUTT5VN8
Per assicurarti che la sfida soddisfi il suo scopo, devi:
- Assicurati che la stessa sfida non venga mai utilizzata più di una volta. Genera una nuova verifica a ogni tentativo di accesso. Ignorare la verifica dopo ogni tentativo di accesso, che sia andato a buon fine o meno. Ignora la sfida anche dopo un certo periodo di tempo. Non accettare mai la stessa sfida in una risposta più di una volta.
- Assicurati che la verifica sia crittograficamente sicura. Una sfida dovrebbe essere praticamente impossibile da indovinare. Per creare una verifica lato server di verifica con sicurezza crittografica, è preferibile utilizzare una libreria lato server FIDO che ritieni attendibile. Se invece crei sfide personalizzate, usa la funzionalità crittografica integrata disponibile nel tuo stack tecnico oppure cerca librerie progettate per casi d'uso crittografici. Gli esempi includono iso-crypto in Node.js o secrets in Python. In base alla specifica, la verifica deve avere una lunghezza di almeno 16 byte per essere considerata sicura.
Una volta creata una sfida, salvala nella sessione dell'utente per verificarla in seguito.
Opzioni per la richiesta di creazione di credenziali
Crea opzioni di richiesta delle credenziali come oggetto publicKeyCredentialRequestOptions
.
Per farlo, utilizza la tua libreria lato server FIDO. In genere offre una funzione di utilità che può creare queste opzioni per te. SimpleWebAuthn offre, ad esempio, generateAuthenticationOptions
.
publicKeyCredentialRequestOptions
deve contenere tutte le informazioni necessarie per l'autenticazione tramite passkey. Passa queste informazioni alla funzione nella libreria lato server FIDO responsabile della creazione dell'oggetto publicKeyCredentialRequestOptions
.
Alcuni dei campi di publicKeyCredentialRequestOptions
possono essere costanti. Altre devono essere definite in modo dinamico sul server:
rpId
: a quale ID parte soggetta a limitazioni ti aspetti che vengano associate la credenziale, ad esempioexample.com
. L'autenticazione avrà esito positivo solo se l'ID della parte soggetta a limitazioni fornito qui corrisponde all'ID della parte soggetta a limitazioni associato alla credenziale. Per compilare l'ID parte soggetta a limitazioni, utilizza lo stesso valore dell'ID parte soggetta a limitazioni impostato inpublicKeyCredentialCreationOptions
durante la registrazione delle credenziali.challenge
: un dato che il fornitore della passkey firmerà per dimostrare che l'utente è in possesso della passkey al momento della richiesta di autenticazione. Rivedi i dettagli in Creare la sfida.allowCredentials
: un array di credenziali accettabili per questa autenticazione. Passa un array vuoto per consentire all'utente di selezionare una passkey disponibile da un elenco mostrato dal browser. Per informazioni dettagliate, consulta Recupero di una verifica dal server RP e Approfondimento sulle credenziali rilevabili.userVerification
: indica se la verifica dell'utente tramite il blocco schermo del dispositivo è "obbligatoria", "preferita" o "scoraggiata". Consulta la sezione Recupero di una verifica dal server RP.timeout
: il tempo (in millisecondi) che l'utente può impiegare per completare l'autenticazione. Deve essere ragionevolmente generoso e più breve rispetto al periodo di validità dichallenge
. Il valore predefinito consigliato è 5 minuti, ma puoi aumentarlo fino a un massimo di 10 minuti, che rientra nell'intervallo consigliato. I timeout lunghi sono utili se prevedi che gli utenti utilizzino il flusso di lavoro ibrido, che in genere richiede un po' più di tempo. In caso di timeout dell'operazione, verrà generato un valoreNotAllowedError
.
Dopo aver creato publicKeyCredentialRequestOptions
, invialo al client.
Codice di esempio: crea opzioni per la richiesta di credenziali
Utilizziamo la libreria SimpleWebAuthn nei nostri esempi. Qui passiamo la creazione delle opzioni per le richieste di credenziali alla relativa funzione generateAuthenticationOptions
.
import {
generateRegistrationOptions,
verifyRegistrationResponse,
generateAuthenticationOptions,
verifyAuthenticationResponse
} from '@simplewebauthn/server';
router.post('/signinRequest', csrfCheck, async (req, res) => {
// Ensure you nest calls in try/catch blocks.
// If something fails, throw an error with a descriptive error message.
// Return that message with an appropriate error code to the client.
try {
// Use the generateAuthenticationOptions function from SimpleWebAuthn
const options = await generateAuthenticationOptions({
rpID: process.env.HOSTNAME,
allowCredentials: [],
});
// Save the challenge in the user session
req.session.challenge = options.challenge;
return res.json(options);
} catch (e) {
console.error(e);
return res.status(400).json({ error: e.message });
}
});
Verifica e accedi all'utente
Quando navigator.credentials.get
viene risolto correttamente sul client, restituisce un oggetto PublicKeyCredential
.
Il response
è un AuthenticatorAssertionResponse
. Rappresenta la risposta del provider di passkey alle istruzioni del client per creare ciò che è necessario per provare ad autenticarsi con una passkey nella parte soggetta a limitazioni. Contiene:
response.authenticatorData
eresponse.clientDataJSON
, ad esempio nel passaggio per la registrazione della passkey.response.signature
che contiene una firma sopra questi valori.
Invia l'oggetto PublicKeyCredential
al server.
Sul server, procedi nel seguente modo:
- Raccogli le informazioni necessarie per verificare l'asserzione e autenticare l'utente:
- Ricevi la richiesta di verifica prevista che hai archiviato nella sessione quando hai generato le opzioni di autenticazione.
- Recupera l'origine e l'ID RP previsti.
- Individua nel database l'identità dell'utente. Nel caso di credenziali rilevabili, non sai chi sia l'utente che effettua una richiesta di autenticazione. Per scoprirlo, hai due opzioni:
- Opzione 1: utilizza
response.userHandle
nell'oggettoPublicKeyCredential
. Nella tabella Utenti, cerca ilpasskey_user_id
che corrisponde auserHandle
. - Opzione 2: utilizza la credenziale
id
presente nell'oggettoPublicKeyCredential
. Nella tabella Credenziali chiave pubblica, cerca la credenzialeid
che corrisponde alla credenzialeid
presente nell'oggettoPublicKeyCredential
. Cerca quindi l'utente corrispondente utilizzando la chiave esternapasskey_user_id
nella tabella Utenti.
- Opzione 1: utilizza
- Trova nel database le informazioni sulle credenziali della chiave pubblica che corrispondono all'asserzione di autenticazione che hai ricevuto. Per farlo, nella tabella Credenziali chiave pubblica, cerca la credenziale
id
che corrisponde alla credenzialeid
presente nell'oggettoPublicKeyCredential
.
Verifica l'asserzione di autenticazione. Passa questo passaggio di verifica alla tua libreria lato server FIDO, che in genere ti offrirà una funzione di utilità per questo scopo. SimpleWebAuthn offre, ad esempio,
verifyAuthenticationResponse
. Scopri cosa sta succedendo in dettaglio nell'Appendice: verifica della risposta di autenticazione.Elimina la verifica indipendentemente dal fatto che la verifica sia andata a buon fine o meno, per evitare attacchi che si ripetono.
Eseguire l'accesso all'utente. Se la verifica è andata a buon fine, aggiorna le informazioni della sessione per contrassegnare l'utente come che ha eseguito l'accesso. Potresti anche voler restituire un oggetto
user
al client, in modo che il frontend possa utilizzare le informazioni associate all'utente che ha appena eseguito l'accesso.
Codice di esempio: verifica l'utente e consenti l'accesso
Utilizziamo la libreria SimpleWebAuthn nei nostri esempi. Qui passiamo la verifica della risposta di autenticazione alla sua funzione verifyAuthenticationResponse
.
import {
generateRegistrationOptions,
verifyRegistrationResponse,
generateAuthenticationOptions,
verifyAuthenticationResponse
} from '@simplewebauthn/server';
import { isoBase64URL } from '@simplewebauthn/server/helpers';
router.post('/signinResponse', csrfCheck, async (req, res) => {
const response = req.body;
const expectedChallenge = req.session.challenge;
const expectedOrigin = getOrigin(req.get('User-Agent'));
const expectedRPID = process.env.HOSTNAME;
// Ensure you nest verification function calls in try/catch blocks.
// If something fails, throw an error with a descriptive error message.
// Return that message with an appropriate error code to the client.
try {
// Find the credential stored to the database by the credential ID
const cred = Credentials.findById(response.id);
if (!cred) {
throw new Error('Credential not found.');
}
// Find the user - Here alternatively we could look up the user directly
// in the Users table via userHandle
const user = Users.findByPasskeyUserId(cred.passkey_user_id);
if (!user) {
throw new Error('User not found.');
}
// Base64URL decode some values
const authenticator = {
credentialPublicKey: isoBase64URL.toBuffer(cred.publicKey),
credentialID: isoBase64URL.toBuffer(cred.id),
transports: cred.transports,
};
// Verify the credential
const { verified, authenticationInfo } = await verifyAuthenticationResponse({
response,
expectedChallenge,
expectedOrigin,
expectedRPID,
authenticator,
requireUserVerification: false,
});
if (!verified) {
throw new Error('User verification failed.');
}
// Kill the challenge for this session.
delete req.session.challenge;
req.session.username = user.username;
req.session['signed-in'] = 'yes';
return res.json(user);
} catch (e) {
delete req.session.challenge;
console.error(e);
return res.status(400).json({ error: e.message });
}
});
Appendice: verifica della risposta di autenticazione
La verifica della risposta di autenticazione prevede i seguenti controlli:
- Assicurati che l'ID della parte soggetta a limitazioni corrisponda al tuo sito.
- Assicurati che l'origine della richiesta corrisponda a quella di accesso del tuo sito. Per le app per Android, consulta l'articolo Verificare l'origine.
- Verifica che il dispositivo sia stato in grado di suggerire la sfida che hai proposto.
- Verifica che durante l'autenticazione l'utente abbia seguito i requisiti da te stabiliti in qualità di parte soggetta a limitazioni. Se richiedi la verifica dell'utente, assicurati che il flag
uv
(utente verificato) inauthenticatorData
siatrue
. Verifica che il flagup
(utente presente) inauthenticatorData
siatrue
, poiché la presenza dell'utente è sempre obbligatoria per le passkey. - Verifica la firma. Per verificare la firma, sono necessari:
- La firma, che rappresenta la sfida firmata:
response.signature
- La chiave pubblica con cui verificare la firma.
- I dati originali firmati. Si tratta dei dati di cui deve essere verificata la firma.
- L'algoritmo crittografico utilizzato per creare la firma.
- La firma, che rappresenta la sfida firmata:
Per saperne di più su questi passaggi, consulta il codice sorgente di SimpleWebAuthn per verifyAuthenticationResponse
o consulta l'elenco completo delle verifiche nella specifica.