Panoramica
Di seguito è riportata una panoramica generale dei passaggi chiave dell'autenticazione tramite passkey:
- Definisci la verifica e altre opzioni necessarie per l'autenticazione con una passkey. Inviali al client in modo da poterli trasmettere alla chiamata di autenticazione tramite passkey (
navigator.credentials.get
sul web). Dopo che l'utente conferma l'autenticazione tramite passkey, la chiamata di autenticazione della 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, autentica l'utente.
Le sezioni seguenti approfondiscono le specifiche di ogni passaggio.
Crea la sfida
In pratica, una sfida è 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 verifica non venga mai utilizzata più di una volta. Genera una nuova verifica a ogni tentativo di accesso. Ignora la verifica dopo ogni tentativo di accesso, riuscito o non riuscito. Ignora la sfida anche dopo un determinato 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 con crittografia sicura, ti consigliamo di utilizzare una libreria FIDO lato server attendibile. Se invece crei personalmente le verifiche, utilizza la funzionalità crittografica integrata disponibile nel tuo stack tecnico o cerca librerie progettate per casi d'uso crittografici. Alcuni esempi sono iso-crypto in Node.js o secret in Python. In base alla specifica, la verifica deve essere lunga almeno 16 byte per essere considerata sicura.
Una volta creata una verifica, salvala nella sessione dell'utente per verificarla in un secondo momento.
Crea opzioni per la richiesta di credenziali
Crea opzioni per la richiesta di credenziali come oggetto publicKeyCredentialRequestOptions
.
A questo scopo, affidati alla libreria FIDO lato server. In genere offre una funzione di utilità in grado di 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 tua libreria lato server FIDO responsabile della creazione dell'oggetto publicKeyCredentialRequestOptions
.
Parte di publicKeyCredentialRequestOptions
campi possono essere costanti. Altre devono essere definite dinamicamente sul server:
rpId
: l'ID parte soggetta a limitazioni a cui prevedi che venga associata la credenziale, ad esempioexample.com
. L'autenticazione avrà esito positivo solo se l'ID parte soggetta a limitazioni qui fornito corrisponde all'ID parte soggetta a limitazioni associato alla credenziale. Per compilare l'ID parte soggetta a limitazioni, usa lo stesso valore dell'ID parte soggetta a limitazioni impostato inpublicKeyCredentialCreationOptions
durante la registrazione delle credenziali.challenge
: un dato che il fornitore di passkey firmerà per dimostrare che l'utente è in possesso della passkey al momento della richiesta di autenticazione. Esamina i dettagli in Creare la sfida.allowCredentials
: un array di credenziali accettabili per questa autenticazione. Trasmetti un array vuoto per consentire all'utente di selezionare una passkey disponibile da un elenco mostrato dal browser. Per maggiori dettagli, consulta Recupera una sfida dal server RP e Approfondimento sulle credenziali rilevabili.userVerification
: indica se la verifica dell'utente tramite il blocco schermo del dispositivo è "obbligatoria" o "preferita" o "sconsigliato". Consulta Recuperare 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 dell'intera durata dell'challenge
. Il valore predefinito consigliato è 5 minuti, ma puoi aumentarlo fino a 10 minuti, entro i limiti dell'intervallo consigliato. I timeout lunghi hanno senso se prevedi che gli utenti utilizzino il flusso di lavoro ibrido, che in genere richiede più tempo. Se l'operazione scade, viene restituito unNotAllowedError
.
Dopo aver creato publicKeyCredentialRequestOptions
, invialo al client.
Codice di esempio: opzioni per la creazione di richieste di credenziali
Nei nostri esempi utilizziamo la libreria SimpleWebAuthn. In questo caso, la creazione delle opzioni per le richieste di credenziali viene trasferita 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
.
response
è una AuthenticatorAssertionResponse
. Rappresenta la risposta del fornitore di passkey all'istruzione del client per creare gli elementi necessari per provare ad eseguire l'autenticazione con una passkey nella parte soggetta a limitazioni. Contiene:
response.authenticatorData
eresponse.clientDataJSON
, ad esempio al passaggio di registrazione della passkey.response.signature
che contiene una firma su 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:
- .
- Ottieni la verifica prevista che hai memorizzato nella sessione quando hai generato le opzioni di autenticazione.
- Ottieni i valori origin e ID RP previsti.
- Individua nel database chi è l'utente. Nel caso di credenziali rilevabili, non conosci l'utente che effettua una richiesta di autenticazione. Per scoprirlo, hai due opzioni:
- Opzione 1: utilizza
response.userHandle
nell'oggettoPublicKeyCredential
. Nella tabella Utenti, cerca il valorepasskey_user_id
che corrisponde auserHandle
. - Opzione 2: utilizza la credenziale
id
presente nell'oggettoPublicKeyCredential
. Nella tabella Credenziali chiave pubblica, cerca la credenzialeid
corrispondente alla credenzialeid
presente nell'oggettoPublicKeyCredential
. Quindi cerca l'utente corrispondente utilizzando la chiave esternapasskey_user_id
nella tabella Utenti.
- Opzione 1: utilizza
- Trova nel tuo 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
corrispondente a quellaid
presente nell'oggettoPublicKeyCredential
.
Verifica l'asserzione di autenticazione. Trasferisci questo passaggio di verifica alla tua libreria lato server FIDO, che in genere offre una funzione di utilità a questo scopo. SimpleWebAuthn offre, ad esempio,
verifyAuthenticationResponse
. Scopri che cosa accade dietro le quinte nell'Appendice: verifica della risposta di autenticazione.Elimina la verifica indipendentemente dal fatto che la verifica abbia esito positivo o meno, per evitare attacchi di ripetizione.
Esegui l'accesso dell'utente. Se la verifica ha avuto esito positivo, aggiorna le informazioni della sessione per contrassegnare l'utente come connesso. 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 e accedi all'utente
Nei nostri esempi utilizziamo la libreria SimpleWebAuthn. 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 parte soggetta a limitazioni corrisponda al tuo sito.
- Assicurati che l'origine della richiesta corrisponda all'origine di accesso del sito. Per le app per Android, consulta l'articolo Verificare l'origine.
- Verifica che il dispositivo sia stato in grado di rispondere alla sfida che hai dato.
- Verifica che durante l'autenticazione l'utente abbia rispettato i requisiti che richiedi come parte soggetta a limitazioni. Se richiedi la verifica utente, assicurati che il flag
uv
(utente verificato) inauthenticatorData
siatrue
. Controlla che il flagup
(utente presente) inauthenticatorData
siatrue
, poiché la presenza dell'utente è sempre obbligatoria per le passkey. - Verifica la firma. Per verificare la firma, devi avere:
- La firma, ovvero la richiesta di verifica firmata:
response.signature
- La chiave pubblica con cui verificare la firma.
- I dati originali firmati. Si tratta dei dati di cui verificare la firma.
- L'algoritmo crittografico utilizzato per creare la firma.
- La firma, ovvero la richiesta di verifica firmata:
Per scoprire di più su questi passaggi, consulta il codice sorgente per verifyAuthenticationResponse
di SimpleWebAuthn o consulta l'elenco completo delle verifiche nella specifica.