Registrazione tramite passkey lato server

Panoramica

Ecco una panoramica generale dei passaggi principali della registrazione delle passkey:

Flusso di registrazione della passkey

  • Definisci le opzioni per creare una passkey. Inviale al client in modo da passarle alla chiamata per la creazione della passkey: la chiamata API WebAuthn navigator.credentials.create sul web e credentialManager.createCredential su Android. Dopo che l'utente conferma la creazione della passkey, la chiamata per la creazione della passkey viene risolta e restituisce una credenziale PublicKeyCredential.
  • Verifica la credenziale e memorizzala sul server.

Le sezioni seguenti illustrano le specifiche di ciascun passaggio.

Opzioni di creazione delle credenziali

Il primo passaggio da eseguire sul server è creare un oggetto PublicKeyCredentialCreationOptions.

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, generateRegistrationOptions.

PublicKeyCredentialCreationOptions deve includere tutto ciò che serve per la creazione della passkey: informazioni sull'utente, sulla parte soggetta a limitazioni e una configurazione delle proprietà della credenziale che stai creando. Dopo aver definito tutti questi elementi, passali, se necessario, alla funzione nella libreria lato server FIDO responsabile della creazione dell'oggetto PublicKeyCredentialCreationOptions.

Alcuni dei campi di PublicKeyCredentialCreationOptions possono essere costanti. Altre devono essere definite in modo dinamico sul server:

  • rpId: per compilare l'ID RP sul server, utilizza le funzioni o le variabili lato server che indicano il nome host della tua applicazione web, ad esempio example.com.
  • user.name e user.displayName:per compilare questi campi, utilizza le informazioni sulla sessione dell'utente che ha eseguito l'accesso (o i dati del nuovo account utente, se l'utente sta creando una passkey al momento della registrazione). user.name è in genere un indirizzo email ed è univoco per la parte soggetta a limitazioni. user.displayName è un nome facile da usare. Tieni presente che non tutte le piattaforme utilizzeranno displayName.
  • user.id: una stringa univoca e casuale generata al momento della creazione dell'account. Questo nome deve essere permanente, a differenza di un nome utente modificabile. Lo User-ID identifica un account, ma non deve contenere informazioni che consentono l'identificazione personale (PII). Probabilmente hai già un ID utente nel tuo sistema, ma se necessario, creane uno specifico per le passkey per evitare PII.
  • excludeCredentials: un elenco di ID delle credenziali esistenti per impedire la duplicazione di una passkey del provider. Per compilare questo campo, cerca le credenziali esistenti di questo utente nel database. Rivedi i dettagli nella pagina Impedire la creazione di una nuova passkey se ne esiste già una.
  • challenge: per la registrazione delle credenziali, la verifica non è pertinente, a meno che non utilizzi l'attestazione, una tecnica più avanzata per verificare l'identità di un provider di passkey e i dati che emette. Tuttavia, anche se non utilizzi l'attestazione, la verifica è comunque un campo obbligatorio. In questo caso, puoi impostare questa sfida su un singolo 0 per semplicità. Le istruzioni per creare una verifica di sicurezza per l'autenticazione sono disponibili in Autenticazione tramite passkey lato server.

Codifica e decodifica

PublicKeyCredentialCreationOptions inviate dal server
PublicKeyCredentialCreationOptions inviata dal server. challenge, user.id e excludeCredentials.credentials devono essere codificati sul lato server in base64URL, in modo che PublicKeyCredentialCreationOptions possano essere pubblicati tramite HTTPS.

PublicKeyCredentialCreationOptions include campi che sono ArrayBuffer, pertanto non sono supportati da JSON.stringify(). Ciò significa che, al momento, per consegnare PublicKeyCredentialCreationOptions tramite HTTPS, alcuni campi devono essere codificati manualmente sul server utilizzando base64URL e poi decodificati sul client.

  • Sul server, la codifica e la decodifica vengono generalmente gestite dalla libreria lato server FIDO.
  • Sul client, la codifica e la decodifica devono essere eseguite manualmente al momento. In futuro diventerà più semplice: sarà disponibile un metodo per convertire opzioni come JSON in PublicKeyCredentialCreationOptions. Controlla lo stato dell'implementazione in Chrome.

Codice di esempio: crea opzioni di creazione delle credenziali

Utilizziamo la libreria SimpleWebAuthn nei nostri esempi. Qui passiamo la creazione di opzioni di credenziali di chiave pubblica alla relativa funzione generateRegistrationOptions.

import {
  generateRegistrationOptions,
  verifyRegistrationResponse,
  generateAuthenticationOptions,
  verifyAuthenticationResponse
} from '@simplewebauthn/server';
import { isoBase64URL } from '@simplewebauthn/server/helpers';

router.post('/registerRequest', csrfCheck, sessionCheck, async (req, res) => {
  const { user } = res.locals;
  // 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 {
    // `excludeCredentials` prevents users from re-registering existing
    // credentials for a given passkey provider
    const excludeCredentials = [];
    const credentials = Credentials.findByUserId(user.id);
    if (credentials.length > 0) {
      for (const cred of credentials) {
        excludeCredentials.push({
          id: isoBase64URL.toBuffer(cred.id),
          type: 'public-key',
          transports: cred.transports,
        });
      }
    }

    // Generate registration options for WebAuthn create
    const options = generateRegistrationOptions({
      rpName: process.env.RP_NAME,
      rpID: process.env.HOSTNAME,
      userID: user.id,
      userName: user.username,
      userDisplayName: user.displayName || '',
      attestationType: 'none',
      excludeCredentials,
      authenticatorSelection: {
        authenticatorAttachment: 'platform',
        requireResidentKey: true
      },
    });

    // Keep the challenge in the session
    req.session.challenge = options.challenge;

    return res.json(options);
  } catch (e) {
    console.error(e);
    return res.status(400).send({ error: e.message });
  }
});

Archivia la chiave pubblica

PublicKeyCredentialCreationOptions inviate dal server
navigator.credentials.create restituisce un oggetto PublicKeyCredential.

Se navigator.credentials.create viene risolto correttamente sul client, significa che è stata creata una passkey. Viene restituito un oggetto PublicKeyCredential.

L'oggetto PublicKeyCredential contiene un oggetto AuthenticatorAttestationResponse, che rappresenta la risposta del provider di passkey all'istruzione del client di creare una passkey. Contiene informazioni sulla nuova credenziale necessaria come RP per autenticare l'utente in un secondo momento. Scopri di più su AuthenticatorAttestationResponse nell'Appendice: AuthenticatorAttestationResponse.

Invia l'oggetto PublicKeyCredential al server. Dopo averlo ricevuto, verificalo.

Passa questo passaggio di verifica alla tua libreria lato server FIDO. In genere offre una funzione di utilità a questo scopo. SimpleWebAuthn offre, ad esempio, verifyRegistrationResponse. Scopri cosa succede in dettaglio nell'Appendice: verifica della risposta alla registrazione.

Una volta che la verifica ha esito positivo, archivia le informazioni sulle credenziali nel tuo database in modo che l'utente possa eseguire l'autenticazione in seguito con la passkey associata alla credenziale in questione.

Usa una tabella dedicata per le credenziali di chiave pubblica associate alle passkey. Un utente può avere una sola password, ma può avere più passkey, ad esempio una passkey sincronizzata tramite il portachiavi iCloud di Apple e una tramite Gestore delle password di Google.

Ecco uno schema di esempio che puoi utilizzare per archiviare le informazioni sulle credenziali:

Schema del database per le passkey

  • Tabella Utenti:
    • user_id: l'ID utente principale. Un ID casuale, univoco e permanente dell'utente. Utilizzala come chiave primaria per la tabella Utenti.
    • username. Un nome utente definito dall'utente, potenzialmente modificabile.
    • passkey_user_id: l'ID utente senza PII specifico per passkey, rappresentato da user.id nelle opzioni di registrazione. Quando l'utente tenterà in un secondo momento di autenticarsi, l'autenticatore renderà disponibile questo passkey_user_id nella sua risposta di autenticazione in userHandle. Ti consigliamo di non impostare passkey_user_id come chiave primaria. Le chiavi primarie tendono a diventare di fatto PII nei sistemi, perché sono ampiamente utilizzate.
  • Tabella Credenziali chiave pubblica:
    • id: ID credenziali. Utilizzala come chiave primaria per la tabella Credenziali chiave pubblica.
    • public_key: chiave pubblica della credenziale.
    • passkey_user_id: utilizzala come chiave esterna per stabilire un collegamento con la tabella Utenti.
    • backed_up: se la passkey è sincronizzata dal provider della passkey, viene eseguito il backup. La memorizzazione dello stato del backup è utile se vuoi prendere in considerazione l'eliminazione delle password in futuro per gli utenti che dispongono di passkey backed_up. Puoi verificare se è stato eseguito il backup della passkey esaminando i flag in authenticatorData oppure utilizzando una funzionalità di libreria lato server FIDO, in genere disponibile, per accedere facilmente a queste informazioni. Memorizzare l'idoneità al backup può essere utile per rispondere a potenziali richieste degli utenti.
    • name: facoltativamente, un nome visualizzato per la credenziale per consentire agli utenti di assegnare nomi personalizzati alle credenziali.
    • transports: un array di trasporti. L'archiviazione dei trasporti è utile per l'esperienza utente di autenticazione. Quando sono disponibili i trasporti, il browser può comportarsi di conseguenza e visualizzare un'interfaccia utente che corrisponde al trasporto che il provider di passkey utilizza per comunicare con i client, in particolare per i casi d'uso di riautenticazione in cui il campo allowCredentials non è vuoto.

Può essere utile memorizzare altre informazioni ai fini dell'esperienza utente, ad esempio elementi come il fornitore della passkey, l'ora di creazione delle credenziali e l'ora dell'ultimo utilizzo. Scopri di più nel design dell'interfaccia utente delle passkey.

Codice di esempio: memorizza la credenziale

Utilizziamo la libreria SimpleWebAuthn nei nostri esempi. Qui passiamo la verifica della risposta di registrazione alla relativa funzione verifyRegistrationResponse.

import { isoBase64URL } from '@simplewebauthn/server/helpers';


router.post('/registerResponse', csrfCheck, sessionCheck, async (req, res) => {
  const expectedChallenge = req.session.challenge;
  const expectedOrigin = getOrigin(req.get('User-Agent'));
  const expectedRPID = process.env.HOSTNAME;
  const response = req.body;
  // This sample code is for registering a passkey for an existing,
  // signed-in user

  // 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 {
    // Verify the credential
    const { verified, registrationInfo } = await verifyRegistrationResponse({
      response,
      expectedChallenge,
      expectedOrigin,
      expectedRPID,
      requireUserVerification: false,
    });

    if (!verified) {
      throw new Error('Verification failed.');
    }

    const { credentialPublicKey, credentialID } = registrationInfo;

    // Existing, signed-in user
    const { user } = res.locals;
    
    // Save the credential
    await Credentials.update({
      id: base64CredentialID,
      publicKey: base64PublicKey,
      // Optional: set the platform as a default name for the credential
      // (example: "Pixel 7")
      name: req.useragent.platform, 
      transports: response.response.transports,
      passkey_user_id: user.passkey_user_id,
      backed_up: registrationInfo.credentialBackedUp
    });

    // Kill the challenge for this session
    delete req.session.challenge;

    return res.json(user);
  } catch (e) {
    delete req.session.challenge;

    console.error(e);
    return res.status(400).send({ error: e.message });
  }
});

Appendice: AuthenticatorAttestationResponse

AuthenticatorAttestationResponse contiene due oggetti importanti:

  • response.clientDataJSON è una versione JSON dei dati client, che sul web sono dati come vengono visti dal browser. Contiene l'origine della parte soggetta a limitazioni, il challenge e androidPackageName se il client è un'app per Android. In qualità di parte soggetta a limitazioni, la lettura clientDataJSONti consente di accedere alle informazioni visualizzate dal browser al momento della richiesta di create.
  • response.attestationObjectcontiene due informazioni:
    • attestationStatement, che non è pertinente, a meno che non utilizzi l'attestazione.
    • authenticatorData include i dati rilevati dal fornitore della passkey. In qualità di parte soggetta a limitazioni, la lettura di authenticatorDatati consente di accedere ai dati visualizzati dal fornitore di passkey e restituiti al momento della richiesta di create.

authenticatorDatacontiene informazioni essenziali sulla credenziale della chiave pubblica associata alla passkey appena creata:

  • La credenziale della chiave pubblica stessa e un rispettivo ID univoco.
  • L'ID parte soggetta a limitazioni associato alla credenziale.
  • Flag che descrivono lo stato dell'utente al momento della creazione della passkey, ovvero se un utente era effettivamente presente e se è stato verificato (vedi userVerification).
  • AAGUID, che identifica il provider di passkey. Visualizzare il fornitore di passkey può essere utile per i tuoi utenti, soprattutto se hanno una passkey registrata per il tuo servizio su più provider di passkey.

Anche se authenticatorData è nidificato all'interno di attestationObject, le informazioni che contiene sono necessarie per l'implementazione della passkey indipendentemente dal fatto che utilizzi l'attestazione. authenticatorData è codificato e contiene campi codificati in formato binario. In genere la libreria lato server gestisce l'analisi e la decodifica. Se non utilizzi una libreria lato server, valuta la possibilità di sfruttare il lato client di getAuthenticatorData() per evitare alcune attività di analisi e decodifica sul lato server.

Appendice: verifica della risposta alla registrazione

In fondo, la verifica della risposta alla registrazione consiste nei seguenti controlli:

  • Assicurati che l'ID della parte soggetta a limitazioni corrisponda al tuo sito.
  • Assicurati che l'origine della richiesta sia un'origine prevista per il tuo sito (URL del sito principale, app per Android).
  • Se richiedi la verifica dell'utente, assicurati che il flag di verifica dell'utente authenticatorData.uv sia true. Verifica che il flag della presenza dell'utente authenticatorData.up sia true, poiché la presenza dell'utente è sempre obbligatoria per le passkey.
  • Verifica che il cliente sia stato in grado di suggerire la sfida che hai proposto. Se non utilizzi l'attestazione, questo controllo non è importante. Tuttavia, l'implementazione di questo controllo è una best practice: garantisce che il codice sia pronto se decidi di utilizzare l'attestazione in futuro.
  • Assicurati che l'ID credenziale non sia ancora registrato per nessun utente.
  • Verifica che l'algoritmo utilizzato dal provider di passkey per creare la credenziale sia un algoritmo che hai elencato (in ogni campo alg di publicKeyCredentialCreationOptions.pubKeyCredParams, in genere definito all'interno della tua libreria lato server e non visibile da te). In questo modo ti assicuri che gli utenti possano registrarsi soltanto con gli algoritmi che hai scelto di consentire.

Per saperne di più, consulta il codice sorgente di verifyRegistrationResponse di SimpleWebAuthn o consulta l'elenco completo delle verifiche nella specifica.

Successivo

Autenticazione tramite passkey lato server