Get your free and exclusive 80-page Banking Passkey Report
passkey tutorial how to implement passkeys

Tutorial sulle passkey: come implementare le passkey nelle app web

Questo tutorial spiega come implementare le passkey nella tua app web. Utilizziamo Node.js (TypeScript), SimpleWebAuthn, HTML/JavaScript Vanilla e MySQL.

Vincent Delitz

Vincent

Created: June 17, 2025

Updated: June 24, 2025


We aim to make the Internet a safer place using passkeys. That's why we want to support developers with tutorials on how to implement passkeys.

1. Introduzione: come implementare le passkey#

In questo tutorial, ti aiuteremo nei tuoi sforzi di implementazione delle passkey, offrendo una guida passo-passo su come aggiungere le passkey al tuo sito web.

Demo Icon

Want to try passkeys yourself in a passkeys demo?

Try Passkeys

Avere un'autenticazione moderna, robusta e di facile utilizzo è fondamentale quando si vuole creare un ottimo sito web o un'app. Le passkey sono emerse come la risposta a questa sfida. Fungendo da nuovo standard per gli accessi, promettono un futuro senza gli svantaggi delle password tradizionali, fornendo un'esperienza di accesso veramente senza password (che non è solo sicura ma anche estremamente comoda).

Ciò che esprime veramente il potenziale delle passkey è il sostegno che hanno raccolto. Tutti i browser più importanti, che si tratti di Chrome, Firefox, Safari o Edge, e tutti i principali produttori di dispositivi (Apple, Microsoft, Google) hanno incorporato il supporto. Questa adozione unanime dimostra che le passkey sono il nuovo standard per gli accessi.

Sì, esistono già tutorial sull'integrazione delle passkey nelle applicazioni web. Che si tratti di framework frontend come React, Vue.js o Next.js, c'è una pletora di guide progettate per mitigare le sfide e accelerare le implementazioni delle passkey. Tuttavia, manca un tutorial end-to-end che rimanga minimalista e di base. Molti sviluppatori ci hanno contattato e chiesto un tutorial che facesse chiarezza sull'implementazione delle passkey per le app web.

Questo è esattamente il motivo per cui abbiamo creato questa guida. Il nostro obiettivo? Creare una configurazione minima funzionante per le passkey, che comprenda il livello frontend, backend e database (quest'ultimo spesso trascurato anche se può causare seri grattacapi).

PasskeyAssessment Icon

Get a free passkey assessment in 15 minutes.

Book free consultation

Alla fine di questo percorso, avrai costruito un'applicazione web minima funzionante, in cui potrai:

  • Creare una passkey
  • Usare la passkey per accedere

Per chi ha fretta o desidera un riferimento, l'intero codice sorgente è disponibile su GitHub.

Curioso di vedere come sarà il risultato finale? Ecco un'anteprima del progetto finale (ammettiamo che sembra molto basilare, ma le cose interessanti sono sotto la superficie):

Siamo pienamente consapevoli che parti del codice e del progetto possono essere realizzate in modo diverso o più sofisticato, ma volevamo concentrarci sull'essenziale. Ecco perché abbiamo intenzionalmente mantenuto le cose semplici e incentrate sulle passkey.

StateOfPasskeys Icon

Want to find out how many people use passkeys?

View Adoption Data

Come aggiungere le passkey al mio sito web di produzione?

Questo è un esempio molto minimale di autenticazione con passkey. I seguenti aspetti NON sono considerati/implementati in questo tutorial o lo sono solo in modo molto basilare:

  • UI condizionale / Mediazione condizionale / compilazione automatica delle passkey
  • Gestione dei dispositivi
  • Gestione delle sessioni
  • Aggiunta sicura di più dispositivi a un account
  • Compatibilità con le versioni precedenti
  • Supporto adeguato multipiattaforma e multi-dispositivo
  • Autenticazione di fallback
  • Gestione corretta degli errori
  • Pagina di gestione delle passkey

Ottenere un supporto completo per tutte queste funzionalità richiede uno sforzo di sviluppo enormemente maggiore. Per chi fosse interessato, consigliamo di dare un'occhiata a questo articolo sui malintesi degli sviluppatori riguardo le passkey.

Slack Icon

Become part of our Passkeys Community for updates & support.

Join

2. Prerequisiti per integrare le passkey#

Prima di immergerci nell'implementazione delle passkey, diamo un'occhiata alle competenze e agli strumenti necessari. Ecco cosa ti serve per iniziare:

2.1 Frontend: HTML e JavaScript Vanilla#

Una solida conoscenza degli elementi costitutivi del web — HTML, CSS e JavaScript — è essenziale. Abbiamo intenzionalmente mantenuto le cose semplici, astenendoci da qualsiasi framework JavaScript moderno e affidandoci a JavaScript/HTML Vanilla. L'unica cosa più sofisticata che usiamo è la libreria wrapper di WebAuthn @simplewebauthn/browser.

2.2 Backend: Node.js (Express) in TypeScript + SimpleWebAuthn#

Per il nostro backend, usiamo un server Node.js (Express) scritto in TypeScript. Abbiamo anche deciso di lavorare con l'implementazione del server WebAuthn di SimpleWebAuthn (@simplewebauthn/server insieme a @simplewebauthn/typescript-types). Esistono numerose implementazioni di server WebAuthn, quindi puoi ovviamente usare anche una di queste. Poiché abbiamo optato per il server WebAuthn in TypeScript, sono richieste conoscenze di base di Node.js e npm.

2.3 Database: MySQL#

Tutti i dati utente e le chiavi pubbliche delle passkey sono memorizzati in un database. Abbiamo scelto MySQL come tecnologia di database. Una comprensione fondamentale di MySQL e dei database relazionali è vantaggiosa, anche se ti guideremo attraverso i singoli passaggi.

Di seguito, useremo spesso i termini WebAuthn e passkey in modo intercambiabile, anche se ufficialmente potrebbero non significare la stessa cosa. Per una migliore comprensione, specialmente nella parte del codice, facciamo comunque questa supposizione.

Con questi prerequisiti, sei pronto per immergerti nel mondo delle passkey.

Ben Gould Testimonial

Ben Gould

Head of Engineering

I’ve built hundreds of integrations in my time, including quite a few with identity providers and I’ve never been so impressed with a developer experience as I have been with Corbado.

Oltre 10.000 sviluppatori si fidano di Corbado e rendono Internet più sicuro con le passkey. Hai domande? Abbiamo scritto più di 150 articoli del blog sulle passkey.

Unisciti alla community delle passkey

3. Panoramica dell'architettura: esempio di implementazione delle passkey#

Prima di entrare nel codice e nelle configurazioni, diamo un'occhiata all'architettura del sistema che vogliamo costruire. Ecco una scomposizione dell'architettura che andremo a configurare:

  • Frontend: Consiste in due pulsanti, uno per la registrazione dell'utente (creazione di una passkey) e l'altro per l'autenticazione (accesso tramite la passkey).
  • Dispositivo e Browser: Una volta che un'azione viene attivata sul frontend, entrano in gioco il dispositivo e il browser. Facilitano la creazione e la verifica della passkey, agendo da intermediari tra l'utente e il backend.
  • Backend: Il backend è dove si svolge la vera magia nella nostra applicazione. Gestisce tutte le richieste avviate dal frontend. Questo processo include la creazione e la verifica delle passkey. Al centro delle operazioni del backend c'è il server WebAuthn. Contrariamente a quanto il nome potrebbe suggerire, non è un server autonomo. Invece, è una libreria o un pacchetto che implementa lo standard WebAuthn. Le due funzioni principali sono: Registrazione (Sign-up) dove i nuovi utenti creano le loro passkey e Autenticazione (Login): dove gli utenti esistenti accedono usando le loro passkey. Nella sua forma più semplice, il server WebAuthn fornisce quattro endpoint API pubblici, divisi in due categorie: due per la registrazione e due per l'autenticazione. Sono progettati per ricevere dati in un formato specifico, che viene poi elaborato dal server WebAuthn. Il server WebAuthn è responsabile di tutte le operazioni crittografiche necessarie. Un aspetto essenziale da notare è che questi endpoint API devono essere serviti tramite HTTPS.
  • Database MySQL: Agendo come nostra spina dorsale di archiviazione, il database MySQL è responsabile della conservazione dei dati utente e delle loro credenziali corrispondenti.
Analyzer Icon

Are your users passkey-ready?

Test Passkey-Readiness

Con questa panoramica dell'architettura, dovresti avere una mappa concettuale di come funzionano i componenti della nostra applicazione. Man mano che procediamo, approfondiremo ciascuno di questi componenti, dettagliandone la configurazione, l'impostazione e l'interazione.

Il seguente diagramma descrive il flusso del processo durante la registrazione (sign-up):

Il seguente diagramma descrive il flusso del processo durante l'autenticazione (login):

Inoltre, qui trovi la struttura del progetto (solo i file più importanti):

passkeys-tutorial ├── src # Contiene tutto il codice sorgente TypeScript del backend │ ├── controllers # Logica di business per la gestione di tipi specifici di richieste │ │ ├── authentication.ts # Logica di autenticazione con passkey │ │ └── registration.ts # Logica di registrazione con passkey │ ├── middleware │ │ ├── customError.ts # Aggiunge messaggi di errore personalizzati in modo standardizzato │ │ └── errorHandler.ts # Gestore di errori generale │ ├── public │ │ ├── index.html # File HTML principale del frontend │ │ ├── css │ │ │ └── style.css # Stile di base │ │ └── js │ │ └── script.js # Logica JavaScript (incl. API WebAuthn) │ ├── routes # Definizioni delle route API e dei loro gestori │ │ └── routes.ts # Route specifiche per le passkey │ ├── services │ │ ├── credentialService.ts# Interagisce con la tabella delle credenziali │ │ └── userService.ts # Interagisce con la tabella degli utenti │ ├── utils # Funzioni di supporto e utilità │ | ├── constants.ts # Alcune costanti (es. rpID) │ | └── utils.ts # Funzione di supporto │ ├── database.ts # Crea la connessione da Node.js al database MySQL │ ├── index.ts # Punto di ingresso del server Node.js │ └── server.ts # Gestisce tutte le impostazioni del server ├── config.json # Alcune configurazioni per il progetto Node.js ├── docker-compose.yml # Definisce servizi, reti e volumi per i container Docker ├── Dockerfile # Crea un'immagine Docker del progetto ├── init-db.sql # Definisce il nostro schema del database MySQL ├── package.json # Gestisce le dipendenze e gli script del progetto Node.js └── tsconfig.json # Configura come TypeScript compila il tuo codice

4. Configurazione del database MySQL#

Quando si implementano le passkey, la configurazione del database è un componente chiave. Il nostro approccio utilizza un container Docker che esegue MySQL, offrendo un ambiente semplice e isolato, essenziale per test e deployment affidabili.

Il nostro schema del database è intenzionalmente minimalista, con solo due tabelle. Questa semplicità aiuta a una comprensione più chiara e a una manutenzione più facile.

Struttura dettagliata delle tabelle

1. Tabella Credentials: Centrale per l'autenticazione con passkey, questa tabella memorizza le credenziali delle passkey. Colonne critiche:

  • credential_id: Un identificatore univoco per ogni credenziale. La scelta del tipo di dati corretto per questo campo è vitale per evitare errori di formattazione.
  • public_key: Memorizza la chiave pubblica per ogni credenziale. Come per credential_id, il tipo di dati e la formattazione appropriati sono cruciali.

2. Tabella Users: Collega gli account utente alle loro credenziali corrispondenti.

Nota che abbiamo chiamato la prima tabella credentials poiché, secondo la nostra esperienza e ciò che altre librerie raccomandano, è più adatto (contrariamente al suggerimento di SimpleWebAuthn di chiamarla authenticator o authenticator_device).

I tipi di dati per credential_id e public_key sono cruciali. Gli errori spesso derivano da tipi di dati, codifica o formattazione errati (specialmente la differenza tra Base64 e Base64URL è una causa comune di errori), che possono interrompere l'intero processo di registrazione (sign-up) o autenticazione (login).

Tutti i comandi SQL necessari per la configurazione di queste tabelle sono contenuti nel file init-db.sql. Questo script garantisce un'inizializzazione del database rapida e senza errori.

Per casi più sofisticati, è possibile aggiungere credential_device_type o credential_backed_up per memorizzare maggiori informazioni sulle credenziali e migliorare l'esperienza utente. In questo tutorial, tuttavia, ci asteniamo dal farlo.

init-db.sql
CREATE TABLE users ( id VARCHAR(255) PRIMARY KEY, username VARCHAR(255) NOT NULL UNIQUE ); CREATE TABLE credentials ( id INT AUTO_INCREMENT PRIMARY KEY, user_id VARCHAR(255) NOT NULL, credential_id VARCHAR(255) NOT NULL, public_key TEXT NOT NULL, counter INT NOT NULL, transports VARCHAR(255), FOREIGN KEY (user_id) REFERENCES users (id) );

Dopo aver creato questo file, creiamo un nuovo file docker-compose.yml a livello radice del progetto:

docker-compose.yml
version: "3.1" services: db: image: mysql command: --default-authentication-plugin=mysql_native_password restart: always environment: MYSQL_ROOT_PASSWORD: my-secret-pw MYSQL_DATABASE: webauthn_db ports: - "3306:3306" volumes: - ./init-db.sql:/docker-entrypoint-initdb.d/init-db.sql

Questo file avvia il database MySQL sulla porta 3306 e crea la struttura del database definita. È importante notare che il nome e la password del database utilizzati qui sono mantenuti semplici a scopo dimostrativo. In un ambiente di produzione, dovresti usare credenziali più complesse per una maggiore sicurezza.

Successivamente, passiamo all'esecuzione del nostro container Docker. A questo punto, il nostro file docker-compose.yml include solo questo singolo container, ma aggiungeremo altri componenti in seguito. Per avviare il container, usa il seguente comando:

docker compose up -d

Una volta che il container è attivo e funzionante, dobbiamo verificare se il database funziona come previsto. Apri un terminale ed esegui il seguente comando per interagire con il database MySQL:

docker exec -it <container ID> mysql -uroot -p

Ti verrà richiesto di inserire la password di root, che nel nostro esempio è my-secret-pw. Dopo aver effettuato l'accesso, seleziona il database webauthn_db e visualizza le tabelle usando questi comandi:

use webauthn_db; show tables;

A questo punto, dovresti vedere le due tabelle definite nel nostro script. Inizialmente, queste tabelle saranno vuote, indicando che la nostra configurazione del database è completa e pronta per i passaggi successivi nell'implementazione delle passkey.

5. Implementazione delle passkey: passaggi di integrazione del backend#

Il backend è il cuore di qualsiasi applicazione con passkey, fungendo da hub centrale per l'elaborazione delle richieste di autenticazione utente dal frontend. Comunica con la libreria del server WebAuthn per gestire le richieste di registrazione (sign-up) e autenticazione (login), e interagisce con il tuo database MySQL per memorizzare e recuperare le credenziali utente. Di seguito, ti guideremo nella configurazione del tuo backend usando Node.js (Express) con TypeScript, che esporrà un'API pubblica per gestire tutte le richieste.

5.1 Inizializzazione del server Node.js (Express)#

Innanzitutto, crea una nuova directory per il tuo progetto e naviga al suo interno usando il tuo terminale o prompt dei comandi.

Esegui il comando

npx create-express-typescript-application passkeys-tutorial

Questo crea uno scheletro di codice di base di un'app Node.js (Express) scritta in TypeScript che possiamo usare per ulteriori adattamenti.

Il tuo progetto richiede diversi pacchetti chiave che dobbiamo installare in aggiunta:

  • @simplewebauthn/server: Una libreria lato server per facilitare le operazioni WebAuthn, come la registrazione utente (sign-up) e l'autenticazione (login).
  • express-session: Middleware per Express.js per gestire le sessioni, memorizzando i dati di sessione lato server e gestendo i cookie.
  • uuid: Un'utilità per generare identificatori univoci universali (UUID), comunemente usati per creare chiavi o identificatori univoci nelle applicazioni.
  • mysql2: Un client Node.js per MySQL, che fornisce funzionalità per connettersi ed eseguire query su database MySQL.

Spostati nella nuova directory e installali con i seguenti comandi (installiamo anche i tipi TypeScript richiesti):

cd passkeys-tutorial npm install @simplewebauthn/server mysql2 uuid express-session @types/express-session @types/uuid

Per confermare che tutto sia installato correttamente, esegui

npm run dev:nodemon

Questo dovrebbe avviare il tuo server Node.js in modalità di sviluppo con Nodemon, che riavvia automaticamente il server a ogni modifica dei file.

Suggerimento per la risoluzione dei problemi: Se incontri errori, prova ad aggiornare ts-node alla versione 10.8.1 nel file package.json e poi esegui npm i per installare gli aggiornamenti.

Il tuo file server.ts ha la configurazione di base e il middleware per un'applicazione Express. Per integrare la funzionalità delle passkey, dovrai aggiungere:

  • Route: Definisci nuove route per la registrazione (sign-up) e l'autenticazione (login) con passkey.
  • Controller: Crea controller per gestire la logica di queste route.
  • Middleware: Integra middleware per la gestione delle richieste e degli errori.
  • Servizi: Costruisci servizi per recuperare e memorizzare dati nel database.
  • Funzioni di utilità: Includi funzioni di utilità per operazioni di codice efficienti.

Questi miglioramenti sono fondamentali per abilitare l'autenticazione con passkey nel backend della tua applicazione. Li configureremo più avanti.

Debugger Icon

Want to experiment with passkey flows? Try our Passkeys Debugger.

Try for Free

5.2 Connessione al database MySQL#

Dopo aver creato e avviato il database nella sezione 4, ora dobbiamo assicurarci che il nostro backend possa connettersi al database MySQL. Pertanto, creiamo un nuovo file database.ts nella cartella /src e aggiungiamo il seguente contenuto:

database.ts
import mysql from "mysql2"; // Create a MySQL pool const pool = mysql.createPool({ host: process.env.DB_HOST, user: process.env.DB_USER, password: process.env.DB_PASSWORD, database: process.env.DB_NAME, waitForConnections: true, connectionLimit: 10, queueLimit: 0, }); // Promisify for Node.js async/await. export const promisePool = pool.promise();

Questo file sarà successivamente utilizzato dal nostro server per accedere al database.

5.3 Configurazione del server dell'app#

Diamo un'occhiata breve al nostro config.json, dove sono già definite due variabili: la porta su cui eseguiamo l'applicazione e l'ambiente:

config.json
{ "PORT": 8080, "NODE_ENV": "development" }

package.json può rimanere così com'è e dovrebbe apparire così:

package.json
{ "name": "passkeys-tutorial", "version": "0.0.1", "description": "passkeys-tutorial initialised with create-express-typescript-application.", "main": "src/index.ts", "scripts": { "build": "tsc", "start": "node ./build/src/index.js", "dev": "ts-node ./src/index.ts", "dev:nodemon": "nodemon -w src -e ts,json -x ts-node ./src/index.ts", "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": ["express", "typescript"], "devDependencies": { "@types/cors": "^2.8.17", "@types/express": "^4.17.21", "@types/morgan": "^1.9.9", "@types/node": "^14.18.63", "@typescript-eslint/eslint-plugin": "^4.33.0", "@typescript-eslint/parser": "^4.33.0", "eslint": "^7.32.0", "nodemon": "^2.0.22", "ts-node": "^10.8.1", "typescript": "^4.9.5" }, "dependencies": { "@simplewebauthn/server": "^8.3.5", "@types/express-session": "^1.17.10", "@types/uuid": "^9.0.7", "cors": "^2.8.5", "env-cmd": "^10.1.0", "express": "^4.18.2", "express-session": "^1.17.3", "fs": "^0.0.1-security", "helmet": "^4.6.0", "morgan": "^1.10.0", "mysql2": "^3.6.5", "uuid": "^9.0.1" } }

index.ts appare così:

index.ts
import app from "./server"; import config from "../config.json"; // Start the application by listening to specific port const port = Number(process.env.PORT || config.PORT || 8080); app.listen(port, () => { console.info("Express application started on port: " + port); });

In server.ts, dobbiamo adattare alcune altre cose. Inoltre, è necessaria una cache temporanea di qualche tipo (ad es. redis, memcache o express-session) per memorizzare le challenge temporanee contro cui gli utenti possono autenticarsi. Abbiamo deciso di usare express-session e dichiariamo il modulo express-session in cima per far funzionare le cose con express-session. Inoltre, semplifichiamo il routing e rimuoviamo per ora la gestione degli errori (questa sarà aggiunta al middleware in seguito):

server.ts
import express, { Express } from "express"; import morgan from "morgan"; import helmet from "helmet"; import cors from "cors"; import config from "../config.json"; import { router as passkeyRoutes } from "./routes/routes"; import session from "express-session"; const app: Express = express(); declare module "express-session" { interface SessionData { currentChallenge?: string; loggedInUserId?: string; } } /************************************************************************************ * Basic Express Middlewares ***********************************************************************************/ app.set("json spaces", 4); app.use(express.json()); app.use(express.urlencoded({ extended: true })); app.use( session({ // @ts-ignore secret: process.env.SESSION_SECRET, saveUninitialized: true, resave: false, cookie: { maxAge: 86400000, httpOnly: true, // Ensure to not expose session cookies to clientside scripts }, }), ); // Handle logs in console during development if (process.env.NODE_ENV === "development" || config.NODE_ENV === "development") { app.use(morgan("dev")); app.use(cors()); } // Handle security and origin in production if (process.env.NODE_ENV === "production" || config.NODE_ENV === "production") { app.use(helmet()); } /************************************************************************************ * Register all routes ***********************************************************************************/ app.use("/api/passkey", passkeyRoutes); app.use(express.static("src/public")); export default app;

5.4 Servizio credenziali e servizio utenti#

Per gestire efficacemente i dati nelle nostre due tabelle create, svilupperemo due servizi distinti in una nuova directory src/services: authenticatorService.ts e userService.ts.

Ogni servizio incapsulerà i metodi CRUD (Create, Read, Update, Delete), consentendoci di interagire con il database in modo modulare e organizzato. Questi servizi faciliteranno la memorizzazione, il recupero e l'aggiornamento dei dati nelle tabelle authenticator e user. Ecco come dovrebbe essere la struttura di questi file richiesti:

userService.ts appare così:

userService.ts
import { promisePool } from "../database"; // Adjust the import path as necessary import { v4 as uuidv4 } from "uuid"; export const userService = { async getUserById(userId: string) { const [rows] = await promisePool.query("SELECT * FROM users WHERE id = ?", [ userId, ]); // @ts-ignore return rows[0]; }, async getUserByUsername(username: string) { try { const [rows] = await promisePool.query( "SELECT * FROM users WHERE username = ?", [username], ); // @ts-ignore return rows[0]; } catch (error) { return null; } }, async createUser(username: string) { const id = uuidv4(); await promisePool.query("INSERT INTO users (id, username) VALUES (?, ?)", [ id, username, ]); return { id, username }; }, };

credentialService.ts appare come segue:

credentialService.ts
import { promisePool } from "../database"; import type { AuthenticatorDevice } from "@simplewebauthn/typescript-types"; export const credentialService = { async saveNewCredential( userId: string, credentialId: string, publicKey: string, counter: number, transports: string, ) { try { await promisePool.query( "INSERT INTO credentials (user_id, credential_id, public_key, counter, transports) VALUES (?, ?, ?, ?, ?)", [userId, credentialId, publicKey, counter, transports], ); } catch (error) { console.error("Error saving new credential:", error); throw error; } }, async getCredentialByCredentialId( credentialId: string, ): Promise<AuthenticatorDevice | null> { try { const [rows] = await promisePool.query( "SELECT * FROM credentials WHERE credential_id = ? LIMIT 1", [credentialId], ); // @ts-ignore if (rows.length === 0) return null; // @ts-ignore const row = rows[0]; return { userID: row.user_id, credentialID: row.credential_id, credentialPublicKey: row.public_key, counter: row.counter, transports: row.transports ? row.transports.split(",") : [], } as AuthenticatorDevice; } catch (error) { console.error("Error retrieving credential:", error); throw error; } }, async updateCredentialCounter(credentialId: string, newCounter: number) { try { await promisePool.query( "UPDATE credentials SET counter = ? WHERE credential_id = ?", [newCounter, credentialId], ); } catch (error) { console.error("Error updating credential counter:", error); throw error; } }, };

5.5 Middleware#

Per gestire gli errori centralmente e anche per facilitare il debug, aggiungiamo un file errorHandler.ts:

errorHandler.ts
import { Request, Response, NextFunction } from "express"; import { CustomError } from "./customError"; interface ErrorWithStatus extends Error { statusCode?: number; } export const handleError = ( err: CustomError, req: Request, res: Response, next: NextFunction, ) => { const statusCode = err.statusCode || 500; const message = err.message || "Internal Server Error"; console.log(message); res.status(statusCode).send({ error: message }); };

Inoltre, aggiungiamo un nuovo file customError.ts poiché in seguito vorremo essere in grado di creare errori personalizzati per aiutarci a trovare i bug più rapidamente:

customError.ts
export class CustomError extends Error { statusCode: number; constructor(message: string, statusCode: number = 500) { super(message); this.statusCode = statusCode; Object.setPrototypeOf(this, CustomError.prototype); } }

5.6 Utilità#

Nella cartella utils, creiamo due file constants.ts e utils.ts.

constant.ts contiene alcune informazioni di base del server WebAuthn, come il nome della relying party, l'ID della relying party e l'origine:

constant.ts
export const rpName: string = "Passkeys Tutorial"; export const rpID: string = "localhost"; export const origin: string = `http://${rpID}:8080`;

utils.ts contiene due funzioni che ci serviranno in seguito per la codifica e la decodifica dei dati:

utils.ts
export const uint8ArrayToBase64 = (uint8Array: Uint8Array): string => Buffer.from(uint8Array).toString("base64"); export const base64ToUint8Array = (base64: string): Uint8Array => new Uint8Array(Buffer.from(base64, "base64"));

5.7 Controller delle passkey con SimpleWebAuthn#

Ora, arriviamo al cuore del nostro backend: i controller. Creiamo due controller, uno per la creazione di una nuova passkey (registration.ts) e uno per l'accesso con una passkey (authentication.ts).

registration.ts appare così:

registration.ts
import { generateRegistrationOptions, verifyRegistrationResponse, } from "@simplewebauthn/server"; import { uint8ArrayToBase64 } from "../utils/utils"; import { rpName, rpID, origin } from "../utils/constants"; import { credentialService } from "../services/credentialService"; import { userService } from "../services/userService"; import { RegistrationResponseJSON } from "@simplewebauthn/typescript-types"; import { Request, Response, NextFunction } from "express"; import { CustomError } from "../middleware/customError"; export const handleRegisterStart = async ( req: Request, res: Response, next: NextFunction, ) => { const { username } = req.body; if (!username) { return next(new CustomError("Username empty", 400)); } try { let user = await userService.getUserByUsername(username); if (user) { return next(new CustomError("User already exists", 400)); } else { user = await userService.createUser(username); } const options = await generateRegistrationOptions({ rpName, rpID, userID: user.id, userName: user.username, timeout: 60000, attestationType: "direct", excludeCredentials: [], authenticatorSelection: { residentKey: "preferred", }, // Support for the two most common algorithms: ES256, and RS256 supportedAlgorithmIDs: [-7, -257], }); req.session.loggedInUserId = user.id; req.session.currentChallenge = options.challenge; res.send(options); } catch (error) { next( error instanceof CustomError ? error : new CustomError("Internal Server Error", 500), ); } }; export const handleRegisterFinish = async ( req: Request, res: Response, next: NextFunction, ) => { const { body } = req; const { currentChallenge, loggedInUserId } = req.session; if (!loggedInUserId) { return next(new CustomError("User ID is missing", 400)); } if (!currentChallenge) { return next(new CustomError("Current challenge is missing", 400)); } try { const verification = await verifyRegistrationResponse({ response: body as RegistrationResponseJSON, expectedChallenge: currentChallenge, expectedOrigin: origin, expectedRPID: rpID, requireUserVerification: true, }); if (verification.verified && verification.registrationInfo) { const { credentialPublicKey, credentialID, counter } = verification.registrationInfo; await credentialService.saveNewCredential( loggedInUserId, uint8ArrayToBase64(credentialID), uint8ArrayToBase64(credentialPublicKey), counter, body.response.transports, ); res.send({ verified: true }); } else { next(new CustomError("Verification failed", 400)); } } catch (error) { next( error instanceof CustomError ? error : new CustomError("Internal Server Error", 500), ); } finally { req.session.loggedInUserId = undefined; req.session.currentChallenge = undefined; } };

Esaminiamo le funzionalità dei nostri controller, che gestiscono i due endpoint chiave nel processo di registrazione (sign-up) di WebAuthn. È qui che si trova una delle maggiori differenze rispetto all'autenticazione basata su password: per ogni tentativo di registrazione (sign-up) o autenticazione (login), sono necessarie due chiamate API al backend, che richiedono un contenuto specifico del frontend nel mezzo. Le password di solito necessitano di un solo endpoint.

1. Endpoint handleRegisterStart:

Questo endpoint viene attivato dal frontend, ricevendo un nome utente per creare una nuova passkey e un nuovo account. In questo esempio, consentiamo la creazione di un nuovo account/passkey solo se non esiste ancora un account. Nelle applicazioni reali, dovresti gestire questo in modo che agli utenti venga detto che una passkey esiste già e che l'aggiunta dallo stesso dispositivo non è possibile (ma l'utente potrebbe aggiungere passkey da un dispositivo diverso dopo una qualche forma di conferma). Per semplicità, in questo tutorial trascuriamo questo aspetto.

Le PublicKeyCredentialCreationOptions vengono preparate. residentKey è impostato su preferred e attestationType su direct, raccogliendo più dati dall'authenticator per una potenziale memorizzazione nel database.

In generale, le PublicKeyCredentialCreationOptions sono composte dai seguenti dati:

dictionary PublicKeyCredentialCreationOptions { required PublicKeyCredentialRpEntity rp; required PublicKeyCredentialUserEntity user; required BufferSource challenge; required sequence<PublicKeyCredentialParameters> pubKeyCredParams; unsigned long timeout; sequence<PublicKeyCredentialDescriptor> excludeCredentials = []; AuthenticatorSelectionCriteria authenticatorSelection; DOMString attestation = "none"; AuthenticationExtensionsClientInputs extensions; };
  • rp: Rappresenta le informazioni della relying party (sito web o servizio), includendo tipicamente il suo nome (rp.name) e il dominio (rp.id).
  • user: Contiene i dettagli dell'account utente come user.name, user.id e user.displayName.
  • challenge: Un valore casuale e sicuro creato dal server WebAuthn per prevenire attacchi di replay durante il processo di registrazione.
  • pubKeyCredParams: Specifica il tipo di credenziale a chiave pubblica da creare, incluso l'algoritmo crittografico utilizzato.
  • timeout: Opzionale, imposta il tempo in millisecondi che l'utente ha per completare l'interazione.
  • excludeCredentials: Un elenco di credenziali da escludere; utilizzato per impedire la registrazione di una passkey per lo stesso dispositivo/authenticator più volte.
  • authenticatorSelection: Criteri per selezionare l'authenticator, ad esempio se deve supportare la verifica dell'utente o come incoraggiare le chiavi residenti.
  • attestation: Specifica la preferenza di trasmissione dell'attestazione desiderata, come "none", "indirect" o "direct".
  • extensions: Opzionale, consente estensioni client aggiuntive.

L'ID utente e la challenge vengono memorizzati in un oggetto di sessione, semplificando il processo a scopo di tutorial. Inoltre, la sessione viene cancellata dopo ogni tentativo di registrazione (sign-up) o autenticazione (login).

2. Endpoint handleRegisterFinish:

Questo endpoint recupera l'ID utente e la challenge impostati in precedenza. Verifica la RegistrationResponse con la challenge. Se valida, memorizza una nuova credenziale per l'utente. Una volta memorizzati nel database, l'ID utente e la challenge vengono rimossi dalla sessione.

Suggerimento: durante il debug della tua applicazione, consigliamo vivamente di utilizzare Chrome come browser e le sue funzionalità integrate per migliorare l'esperienza di sviluppo di applicazioni basate su passkey, ad esempio l'authenticator WebAuthn virtuale e il log del dispositivo (vedi i nostri consigli per gli sviluppatori di seguito per maggiori informazioni).

Successivamente, passiamo a authentication.ts, che ha una struttura e una funzionalità simili.

authentication.ts appare così:

authentication.ts
import { Request, Response, NextFunction } from "express"; import { generateAuthenticationOptions, verifyAuthenticationResponse, } from "@simplewebauthn/server"; import { uint8ArrayToBase64, base64ToUint8Array } from "../utils/utils"; import { rpID, origin } from "../utils/constants"; import { credentialService } from "../services/credentialService"; import { userService } from "../services/userService"; import { AuthenticatorDevice } from "@simplewebauthn/typescript-types"; import { isoBase64URL } from "@simplewebauthn/server/helpers"; import { VerifiedAuthenticationResponse, VerifyAuthenticationResponseOpts, } from "@simplewebauthn/server/esm"; import { CustomError } from "../middleware/customError"; export const handleLoginStart = async ( req: Request, res: Response, next: NextFunction, ) => { const { username } = req.body; try { const user = await userService.getUserByUsername(username); if (!user) { return next(new CustomError("User not found", 404)); } req.session.loggedInUserId = user.id; // allowCredentials is purposely for this demo left empty. This causes all existing local credentials // to be displayed for the service instead only the ones the username has registered. const options = await generateAuthenticationOptions({ timeout: 60000, allowCredentials: [], userVerification: "required", rpID, }); req.session.currentChallenge = options.challenge; res.send(options); } catch (error) { next( error instanceof CustomError ? error : new CustomError("Internal Server Error", 500), ); } }; export const handleLoginFinish = async ( req: Request, res: Response, next: NextFunction, ) => { const { body } = req; const { currentChallenge, loggedInUserId } = req.session; if (!loggedInUserId) { return next(new CustomError("User ID is missing", 400)); } if (!currentChallenge) { return next(new CustomError("Current challenge is missing", 400)); } try { const credentialID = isoBase64URL.toBase64(body.rawId); const bodyCredIDBuffer = isoBase64URL.toBuffer(body.rawId); const dbCredential: AuthenticatorDevice | null = await credentialService.getCredentialByCredentialId(credentialID); if (!dbCredential) { return next(new CustomError("Credential not registered with this site", 404)); } // @ts-ignore const user = await userService.getUserById(dbCredential.userID); if (!user) { return next(new CustomError("User not found", 404)); } // @ts-ignore dbCredential.credentialID = base64ToUint8Array(dbCredential.credentialID); // @ts-ignore dbCredential.credentialPublicKey = base64ToUint8Array( dbCredential.credentialPublicKey, ); let verification: VerifiedAuthenticationResponse; const opts: VerifyAuthenticationResponseOpts = { response: body, expectedChallenge: currentChallenge, expectedOrigin: origin, expectedRPID: rpID, authenticator: dbCredential, }; verification = await verifyAuthenticationResponse(opts); const { verified, authenticationInfo } = verification; if (verified) { await credentialService.updateCredentialCounter( uint8ArrayToBase64(bodyCredIDBuffer), authenticationInfo.newCounter, ); res.send({ verified: true }); } else { next(new CustomError("Verification failed", 400)); } } catch (error) { next( error instanceof CustomError ? error : new CustomError("Internal Server Error", 500), ); } finally { req.session.currentChallenge = undefined; req.session.loggedInUserId = undefined; } };

Il nostro processo di autenticazione (login) coinvolge due endpoint:

1. Endpoint handleLoginStart:

Questo endpoint viene attivato quando un utente tenta di accedere. Prima controlla se il nome utente esiste nel database, restituendo un errore se non viene trovato. In uno scenario reale, potresti offrire di creare un nuovo account.

Per gli utenti esistenti, recupera l'ID utente dal database, lo memorizza nella sessione e genera le opzioni PublicKeyCredentialRequestOptions. allowCredentials viene lasciato vuoto per non limitare l'uso delle credenziali. Ecco perché tutte le passkey disponibili per questa relying party possono essere selezionate nella finestra modale delle passkey.

La challenge generata viene anche memorizzata nella sessione e le PublicKeyCredentialRequestOptions vengono inviate al frontend.

Le PublicKeyCredentialRequestOptions sono composte dai seguenti dati:

dictionary PublicKeyCredentialRequestOptions { required BufferSource challenge; unsigned long timeout; USVString rpId; sequence<PublicKeyCredentialDescriptor> allowCredentials = []; DOMString userVerification = "preferred"; AuthenticationExtensionsClientInputs extensions; };
  • challenge: Un valore casuale e sicuro dal server WebAuthn utilizzato per prevenire attacchi di replay durante il processo di autenticazione.
  • timeout: Opzionale, imposta il tempo in millisecondi che l'utente ha per rispondere alla richiesta di autenticazione.
  • rpId: L'ID della relying party, tipicamente il dominio del servizio.
  • allowCredentials: Un elenco opzionale di descrittori di credenziali, che specifica quali credenziali possono essere utilizzate per questa autenticazione (login).
  • userVerification: Specifica il requisito per la verifica dell'utente, come "required", "preferred" o "discouraged".
  • extensions: Opzionale, consente estensioni client aggiuntive.

2. Endpoint handleLoginFinish:

Questo endpoint recupera currentChallenge e loggedInUserId dalla sessione.

Interroga il database per la credenziale corretta utilizzando l'ID della credenziale dal corpo della richiesta. Se la credenziale viene trovata, significa che l'utente associato a questo ID di credenziale può ora essere autenticato (loggato). Quindi, possiamo interrogare l'utente dalla tabella degli utenti tramite l'ID utente che otteniamo dalla credenziale e verificare la authenticationResponse utilizzando la challenge e il corpo della richiesta. Se tutto ha successo, mostriamo il messaggio di successo del login. Se non viene trovata alcuna credenziale corrispondente, viene inviato un errore.

Inoltre, se la verifica ha successo, il contatore della credenziale viene aggiornato, la challenge utilizzata e il loggedInUserId vengono rimossi dalla sessione.

Inoltre, possiamo eliminare le cartelle src/app e src/constant insieme a tutti i file al loro interno.

Nota: la gestione corretta delle sessioni e la protezione delle route, cruciali nelle applicazioni reali, sono omesse qui per semplicità in questo tutorial.

5.8 Route delle passkey#

Infine, ma non meno importante, dobbiamo assicurarci che i nostri controller siano raggiungibili aggiungendo le route appropriate a routes.ts che si trova in una nuova directory src/routes:

routes.ts
import express from "express"; import { handleError } from "../middleware/errorHandler"; import { handleRegisterStart, handleRegisterFinish } from "../controllers/registration"; import { handleLoginStart, handleLoginFinish } from "../controllers/authentication"; const router = express.Router(); router.post("/registerStart", handleRegisterStart); router.post("/registerFinish", handleRegisterFinish); router.post("/loginStart", handleLoginStart); router.post("/loginFinish", handleLoginFinish); router.use(handleError); export { router };
Substack Icon

Subscribe to our Passkeys Substack for the latest news.

Subscribe

6. Integrare le passkey nel frontend#

Questa parte del tutorial sulle passkey si concentra su come supportare le passkey nel frontend della tua applicazione. Abbiamo un frontend molto basilare composto da tre file: index.html, styles.css e script.js. Tutti e tre i file si trovano in una nuova cartella src/public.

Il file index.html contiene un campo di input per il nome utente e due pulsanti per registrarsi e accedere. Inoltre, importiamo lo script @simplewebauthn/browser che semplifica l'interazione con l'API di autenticazione web del browser nel file js/script.js.

index.html appare così:

index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Passkey Tutorial</title> <link rel="stylesheet" href="css/style.css" /> </head> <body> <div class="container"> <h1>Passkey Tutorial</h1> <div id="message"></div> <div class="input-group"> <input type="text" id="username" placeholder="Enter username" /> <button id="registerButton">Register</button> <button id="loginButton">Login</button> </div> </div> <script src="https://unpkg.com/@simplewebauthn/browser/dist/bundle/index.es5.umd.min.js"></script> <script src="js/script.js"></script> </body> </html>

script.js appare come segue:

script.js
document.getElementById("registerButton").addEventListener("click", register); document.getElementById("loginButton").addEventListener("click", login); function showMessage(message, isError = false) { const messageElement = document.getElementById("message"); messageElement.textContent = message; messageElement.style.color = isError ? "red" : "green"; } async function register() { // Retrieve the username from the input field const username = document.getElementById("username").value; try { // Get registration options from your server. Here, we also receive the challenge. const response = await fetch("/api/passkey/registerStart", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username: username }), }); console.log(response); // Check if the registration options are ok. if (!response.ok) { throw new Error( "User already exists or failed to get registration options from server", ); } // Convert the registration options to JSON. const options = await response.json(); console.log(options); // This triggers the browser to display the passkey / WebAuthn modal (e.g. Face ID, Touch ID, Windows Hello). // A new attestation is created. This also means a new public-private-key pair is created. const attestationResponse = await SimpleWebAuthnBrowser.startRegistration(options); // Send attestationResponse back to server for verification and storage. const verificationResponse = await fetch("/api/passkey/registerFinish", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(attestationResponse), }); if (verificationResponse.ok) { showMessage("Registration successful"); } else { showMessage("Registration failed", true); } } catch (error) { showMessage("Error: " + error.message, true); } } async function login() { // Retrieve the username from the input field const username = document.getElementById("username").value; try { // Get login options from your server. Here, we also receive the challenge. const response = await fetch("/api/passkey/loginStart", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username: username }), }); // Check if the login options are ok. if (!response.ok) { throw new Error("Failed to get login options from server"); } // Convert the login options to JSON. const options = await response.json(); console.log(options); // This triggers the browser to display the passkey / WebAuthn modal (e.g. Face ID, Touch ID, Windows Hello). // A new assertionResponse is created. This also means that the challenge has been signed. const assertionResponse = await SimpleWebAuthnBrowser.startAuthentication(options); // Send assertionResponse back to server for verification. const verificationResponse = await fetch("/api/passkey/loginFinish", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(assertionResponse), }); if (verificationResponse.ok) { showMessage("Login successful"); } else { showMessage("Login failed", true); } } catch (error) { showMessage("Error: " + error.message, true); } }

In script.js, ci sono tre funzioni principali:

1. Funzione showMessage:

Questa è una funzione di utilità utilizzata principalmente per visualizzare messaggi di errore, aiutando nel debug.

2. Funzione Register:

Attivata quando l'utente fa clic su "Register". Estrae il nome utente dal campo di input e lo invia all'endpoint passkeyRegisterStart. La risposta include PublicKeyCredentialCreationOptions, che vengono convertite in JSON e passate a SimpleWebAuthnBrowser.startRegistration. Questa chiamata attiva l'authenticator del dispositivo (come Face ID o Touch ID). Dopo un'autenticazione locale riuscita, la challenge firmata viene inviata all'endpoint passkeyRegisterFinish, completando il processo di creazione della passkey.

Durante il processo di registrazione (sign-up), l'oggetto di attestazione gioca un ruolo cruciale, quindi diamogli un'occhiata più da vicino.

L'oggetto di attestazione è composto principalmente da tre componenti: fmt, attStmt e authData. L'elemento fmt indica il formato della dichiarazione di attestazione, mentre attStmt rappresenta la dichiarazione di attestazione stessa. In scenari in cui l'attestazione non è ritenuta necessaria, fmt sarà designato come "none", portando a un attStmt vuoto.

L'attenzione si concentra sul segmento authData all'interno di questa struttura. Questo segmento è fondamentale per recuperare elementi essenziali come l'ID della relying party, i flag, il contatore e i dati della credenziale attestata sul nostro server. Per quanto riguarda i flag, di particolare interesse sono BS (Backup State) e BE (Backup Eligibility) che forniscono maggiori informazioni se una passkey è sincronizzata (ad esempio tramite iCloud Keychain o 1Password). Inoltre, UV (User Verification) e UP (User Presence) forniscono informazioni più utili.

È importante notare che varie parti dell'oggetto di attestazione, inclusi i dati dell'authenticator, l'ID della relying party e la dichiarazione di attestazione, sono o sottoposte a hash o firmate digitalmente dall'authenticator utilizzando la sua chiave privata. Questo processo è fondamentale per mantenere l'integrità complessiva dell'oggetto di attestazione.

3. Funzione Login:

Attivata quando l'utente fa clic su "Login". Similmente alla funzione di registrazione, estrae il nome utente e lo invia all'endpoint passkeyLoginStart. La risposta, contenente PublicKeyCredentialRequestOptions, viene convertita in JSON e utilizzata con SimpleWebAuthnBrowser.startAuthentication. Questo attiva l'autenticazione locale sul dispositivo. La challenge firmata viene quindi inviata all'endpoint passkeyLoginFinish. Una risposta positiva da questo endpoint indica che l'utente ha effettuato l'accesso all'app con successo.

Inoltre, il file CSS di accompagnamento fornisce uno stile semplice per l'applicazione:

body { font-family: "Helvetica Neue", Arial, sans-serif; text-align: center; padding: 40px; background-color: #f3f4f6; color: #333; } .container { max-width: 400px; margin: auto; background: white; padding: 20px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); border-radius: 8px; } h1 { color: #007bff; font-size: 24px; margin-bottom: 20px; } .input-group { margin-bottom: 20px; } input[type="text"] { padding: 10px; margin-bottom: 10px; border: 1px solid #ced4da; border-radius: 4px; width: calc(100% - 22px); } button { width: calc(50% - 20px); padding: 10px 0; margin: 5px; font-size: 16px; cursor: pointer; border: none; border-radius: 4px; background-color: #007bff; color: white; } button:hover { background-color: #0056b3; } #message { color: #dc3545; margin: 20px; }

7. Eseguire l'app di esempio con le passkey#

Per vedere la tua applicazione in azione, compila ed esegui il tuo codice TypeScript con:

npm run dev

Il tuo server dovrebbe ora essere attivo e funzionante su http://localhost:8080.

Considerazioni per la produzione:

Ricorda, ciò che abbiamo trattato è uno schema di base. Quando si distribuisce un'applicazione con passkey in un ambiente di produzione, è necessario approfondire:

  • Misure di sicurezza: Implementa pratiche di sicurezza robuste per proteggere i dati degli utenti.
  • Gestione degli errori: Assicurati che la tua applicazione gestisca e registri gli errori in modo appropriato.
  • Gestione del database: Ottimizza le operazioni del database per la scalabilità e l'affidabilità.

8. Integrazione DevOps per le passkey#

Abbiamo già configurato un container Docker per il nostro database. Successivamente, espanderemo la nostra configurazione di Docker Compose per includere il server con sia il backend che il frontend. Il tuo file docker-compose.yml dovrebbe essere aggiornato di conseguenza.

Per containerizzare la nostra applicazione, creiamo un nuovo Dockerfile che installa i pacchetti richiesti e avvia il server di sviluppo:

Docker
# Use an official Node runtime as a parent image FROM node:20-alpine # Set the working directory in the container WORKDIR /usr/src/app # Copy package.json and package-lock.json COPY package*.json ./ # Install any needed packages RUN npm install # Bundle your app's source code inside the Docker image COPY . . # Make port 8080 available to the world outside this container EXPOSE 8080 # Define the command to run your app CMD ["npm", "run", "dev"]

Quindi, estendiamo anche il file docker-compose.yml per avviare questo container:

docker-compose.yml
version: "3.1" services: db: image: mysql command: --default-authentication-plugin=mysql_native_password restart: always environment: MYSQL_ROOT_PASSWORD: my-secret-pw MYSQL_DATABASE: webauthn_db ports: - "3306:3306" volumes: - ./init-db.sql:/docker-entrypoint-initdb.d/init-db.sql app: build: . ports: - "8080:8080" environment: - DB_HOST=db - DB_USER=root - DB_PASSWORD=my-secret-pw - DB_NAME=webauthn_db - SESSION_SECRET=secret123 depends_on: - db

Se ora esegui docker compose up nel tuo terminale e accedi a http://localhost:8080, dovresti vedere la versione funzionante della tua app web con passkey (qui in esecuzione su Windows 11 23H2 + Chrome 119):

9. Consigli aggiuntivi sulle passkey per gli sviluppatori#

Poiché lavoriamo da un po' di tempo con le implementazioni di passkey, abbiamo incontrato un paio di sfide quando si lavora su app con passkey reali:

  • Compatibilità e supporto di dispositivi/piattaforme
  • Onboarding ed educazione degli utenti
  • Gestione di dispositivi persi o cambiati
  • Autenticazione multipiattaforma
  • Meccanismi di fallback
  • Complessità della codifica: la codifica è spesso la parte più difficile poiché si ha a che fare con JSON, CBOR, uint8array, buffer, blob, diversi database, base64 e base64url, dove possono verificarsi molti errori
  • Gestione delle passkey (ad es. per aggiungere, eliminare o rinominare le passkey)

Inoltre, abbiamo i seguenti suggerimenti per gli sviluppatori per quanto riguarda la parte di implementazione:

Utilizza il Passkeys Debugger

Il Passkeys debugger aiuta a testare diverse impostazioni del server WebAuthn e le risposte del client. Inoltre, fornisce un ottimo parser per le risposte dell'authenticator.

Esegui il debug con la funzione Device Log di Chrome

Usa il log del dispositivo di Chrome (accessibile tramite chrome://device-log/) per monitorare le chiamate FIDO/WebAuthn. Questa funzione fornisce log in tempo reale del processo di autenticazione (login), consentendoti di vedere i dati scambiati e risolvere eventuali problemi che si presentano.

Un'altra scorciatoia molto utile per ottenere tutte le tue passkey in Chrome è usare chrome://settings/passkeys.

Usa l'Authenticator WebAuthn virtuale di Chrome

Per evitare di utilizzare il prompt di Touch ID, Face ID o Windows Hello durante lo sviluppo, Chrome è dotato di un pratico authenticator WebAuthn virtuale che emula un vero authenticator. Consigliamo vivamente di usarlo per accelerare le cose. Trova maggiori dettagli qui.

Testa su diverse piattaforme e browser

Assicurati la compatibilità e la funzionalità su vari browser e piattaforme. WebAuthn si comporta in modo diverso su browser diversi, quindi un test approfondito è fondamentale.

Testa su dispositivi diversi

Qui è particolarmente utile lavorare con strumenti come ngrok, dove puoi rendere la tua applicazione locale raggiungibile su altri dispositivi (mobili).

Imposta la verifica dell'utente su preferred

Quando si definiscono le proprietà per userVerification nelle PublicKeyCredentialRequestOptions, scegli di impostarle su preferred poiché questo è un buon compromesso tra usabilità e sicurezza. Ciò significa che i controlli di sicurezza sono attivi sui dispositivi idonei, mentre la facilità d'uso viene mantenuta sui dispositivi senza capacità biometriche.

10. Conclusione: tutorial sulle passkey#

Speriamo che questo tutorial sulle passkey fornisca una chiara comprensione di come implementare le passkey in modo efficace. Durante il tutorial, abbiamo esaminato i passaggi essenziali per creare un'applicazione con passkey, concentrandoci sui concetti fondamentali e sull'implementazione pratica. Sebbene questa guida serva come punto di partenza, c'è molto altro da esplorare e perfezionare nel mondo di WebAuthn.

Incoraggiamo gli sviluppatori ad approfondire le sfumature delle passkey (ad esempio, aggiungendo più passkey, verificando la prontezza dei dispositivi per le passkey o offrendo soluzioni di recupero). È un viaggio che vale la pena intraprendere, che offre sia sfide che immense ricompense nel migliorare l'autenticazione degli utenti. Con le passkey, non stai solo costruendo una funzionalità; stai contribuendo a un mondo digitale più sicuro e facile da usare.

Add passkeys to your app in <1 hour with our UI components, SDKs & guides.

Start for free

Share this article


LinkedInTwitterFacebook

Enjoyed this read?

🤝 Join our Passkeys Community

Share passkeys implementation tips and get support to free the world from passwords.

🚀 Subscribe to Substack

Get the latest news, strategies, and insights about passkeys sent straight to your inbox.

Related Articles