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

Tutoriel Passkeys : Comment implémenter les passkeys dans les applications web

Ce tutoriel explique comment implémenter les passkeys dans votre application web. Nous utilisons Node.js (TypeScript), SimpleWebAuthn, HTML / JavaScript Vanilla et MySQL.

Vincent Delitz

Vincent

Created: June 20, 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. Introduction : Comment implémenter les passkeys#

Dans ce tutoriel, nous vous aidons dans vos efforts d'implémentation des passkeys, en offrant un guide étape par étape sur la façon d'ajouter les passkeys à votre site web.

Demo Icon

Want to try passkeys yourself in a passkeys demo?

Try Passkeys

Disposer d'une authentification moderne, robuste et conviviale est essentiel lorsque vous souhaitez créer un excellent site web ou une excellente application. Les passkeys sont apparus comme la réponse à ce défi. En tant que nouvelle norme pour les connexions, ils promettent un avenir sans les inconvénients des mots de passe traditionnels, offrant une expérience de connexion véritablement sans mot de passe (qui est non seulement sécurisée mais aussi très pratique).

Ce qui exprime vraiment le potentiel des passkeys, c'est le soutien qu'ils ont recueilli. Tous les navigateurs importants, que ce soit Chrome, Firefox, Safari ou Edge, et tous les grands fabricants d'appareils (Apple, Microsoft, Google) ont intégré leur prise en charge. Cette adhésion unanime montre que les passkeys sont la nouvelle norme pour les connexions.

Oui, il existe déjà des tutoriels sur l'intégration des passkeys dans les applications web. Que ce soit pour des frameworks frontend comme React, Vue.js ou Next.js, il existe une pléthore de guides conçus pour atténuer les défis et accélérer vos implémentations de passkeys. Cependant, il manque un tutoriel de bout en bout qui reste minimaliste et proche du matériel. De nombreux développeurs nous ont contactés pour nous demander un tutoriel qui clarifie l'implémentation des passkeys pour les applications web.

C'est précisément pourquoi nous avons conçu ce guide. Notre objectif ? Créer une configuration minimale viable pour les passkeys, englobant les couches frontend, backend et base de données (cette dernière étant souvent négligée bien qu'elle puisse causer de sérieux maux de tête).

PasskeyAssessment Icon

Get a free passkey assessment in 15 minutes.

Book free consultation

À la fin de ce parcours, vous aurez construit une application web minimale viable, où vous pourrez :

  • Créer un passkey
  • Utiliser le passkey pour vous connecter

Pour ceux qui sont pressés ou qui souhaitent une référence, l'ensemble du code est disponible sur GitHub.

Curieux de voir à quoi ressemble le résultat final ? Voici un aperçu du projet final (nous admettons qu'il a l'air très basique, mais les choses intéressantes se trouvent sous la surface) :

Nous sommes pleinement conscients que certaines parties du code et du projet peuvent être réalisées différemment ou de manière plus sophistiquée, mais nous voulions nous concentrer sur l'essentiel. C'est pourquoi nous avons intentionnellement gardé les choses simples et centrées sur les passkeys.

StateOfPasskeys Icon

Want to find out how many people use passkeys?

View Adoption Data

Comment ajouter des passkeys à mon site web de production ?

Ceci est un exemple très minimal d'authentification par passkey. Les éléments suivants ne sont PAS pris en compte / implémentés dans ce tutoriel ou ne le sont que de manière très basique :

  • Interface utilisateur conditionnelle / Médiation conditionnelle / Remplissage automatique des passkeys
  • Gestion des appareils
  • Gestion des sessions
  • Ajout sécurisé de plusieurs appareils à un compte
  • Rétrocompatibilité
  • Prise en charge multiplateforme et multi-appareils appropriée
  • Authentification de secours
  • Gestion des erreurs appropriée
  • Page de gestion des passkeys

Obtenir une prise en charge complète de toutes ces fonctionnalités nécessite un effort de développement considérablement plus important. Pour ceux qui sont intéressés, nous recommandons de jeter un œil à cet article sur les idées fausses des développeurs de passkeys.

Slack Icon

Become part of our Passkeys Community for updates & support.

Join

2. Prérequis pour intégrer les passkeys#

Avant de plonger dans l'implémentation des passkeys, examinons les compétences et les outils nécessaires. Voici ce dont vous avez besoin pour commencer :

2.1 Frontend : HTML et JavaScript Vanilla#

Une solide maîtrise des éléments de base du web, à savoir HTML, CSS et JavaScript, est essentielle. Nous avons intentionnellement gardé les choses simples, en nous abstenant de tout framework JavaScript moderne et en nous appuyant sur du JavaScript / HTML Vanilla. La seule chose plus sophistiquée que nous utilisons est la bibliothèque wrapper WebAuthn @simplewebauthn/browser.

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

Pour notre backend, nous utilisons un serveur Node.js (Express) écrit en TypeScript. Nous avons également décidé de travailler avec l'implémentation serveur WebAuthn de SimpleWebAuthn (@simplewebauthn/server avec @simplewebauthn/typescript-types). Il existe de nombreuses implémentations de serveur WebAuthn disponibles, vous pouvez donc bien sûr utiliser n'importe laquelle d'entre elles. Comme nous avons opté pour le serveur WebAuthn en TypeScript, des connaissances de base en Node.js et npm sont requises.

2.3 Base de données : MySQL#

Toutes les données utilisateur et les clés publiques des passkeys sont stockées dans une base de données. Nous avons choisi MySQL comme technologie de base de données. Une compréhension fondamentale de MySQL et des bases de données relationnelles est utile, bien que nous vous guiderons à travers chaque étape.

Dans ce qui suit, nous utilisons souvent les termes WebAuthn et passkeys de manière interchangeable, même s'ils ne signifient pas officiellement la même chose. Pour une meilleure compréhension, en particulier dans la partie code, nous faisons cependant cette supposition.

Avec ces prérequis en place, vous êtes prêt à plonger dans le monde des passkeys.

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.

10,000+ devs trust Corbado & make the Internet safer with passkeys. Got questions? We’ve written 150+ blog posts on passkeys.

Join Passkeys Community

3. Vue d'ensemble de l'architecture : Exemple d'implémentation de Passkey#

Avant d'entrer dans le code et les configurations, jetons un œil à l'architecture du système que nous voulons construire. Voici une décomposition de l'architecture que nous allons mettre en place :

  • Frontend : Il se compose de deux boutons, l'un pour l'inscription de l'utilisateur (création d'un passkey) et l'autre pour l'authentification (connexion à l'aide du passkey).
  • Appareil et navigateur : Une fois qu'une action est déclenchée sur le frontend, l'appareil et le navigateur entrent en jeu. Ils facilitent la création et la vérification du passkey, agissant comme intermédiaires entre l'utilisateur et le backend.
  • Backend : Le backend est l'endroit où la vraie magie opère dans notre application. Il gère toutes les requêtes initiées par le frontend. Ce processus implique la création et la vérification des passkeys. Au cœur des opérations du backend se trouve le serveur WebAuthn. Contrairement à ce que le nom pourrait suggérer, ce n'est pas un serveur autonome. Il s'agit plutôt d'une bibliothèque ou d'un paquet qui implémente la norme WebAuthn. Les deux fonctions principales sont : Inscription (Sign-up) où les nouveaux utilisateurs créent leurs passkeys et Authentification (Login) : Où les utilisateurs existants se connectent en utilisant leurs passkeys. Dans sa forme la plus simple, le serveur WebAuthn fournit quatre points de terminaison d'API publics, divisés en deux catégories : deux pour l'inscription et deux pour l'authentification. Ils sont conçus pour recevoir des données dans un format spécifique, qui sont ensuite traitées par le serveur WebAuthn. Le serveur WebAuthn est responsable de toutes les opérations cryptographiques nécessaires. Un aspect essentiel à noter est que ces points de terminaison d'API doivent être servis via HTTPS.
  • Base de données MySQL : Agissant comme notre épine dorsale de stockage, la base de données MySQL est responsable de la conservation des données utilisateur et de leurs identifiants correspondants.
Analyzer Icon

Are your users passkey-ready?

Test Passkey-Readiness

Avec cette vue d'ensemble de l'architecture, vous devriez avoir une carte conceptuelle de la façon dont les composants de notre application fonctionnent. Au fur et à mesure, nous approfondirons chacun de ces composants, en détaillant leur configuration, leur paramétrage et leur interaction.

Le diagramme suivant décrit le flux du processus lors de l'inscription (sign-up) :

Le diagramme suivant décrit le flux du processus lors de l'authentification (connexion) :

De plus, vous trouverez ici la structure du projet (uniquement les fichiers les plus importants) :

passkeys-tutorial ├── src # Contient tout le code source TypeScript du backend │ ├── controllers # Logique métier pour gérer des types de requêtes spécifiques │ │ ├── authentication.ts # Logique d'authentification Passkey │ │ └── registration.ts # Logique d'inscription Passkey │ ├── middleware │ │ ├── customError.ts # Ajoute des messages d'erreur personnalisés de manière standardisée │ │ └── errorHandler.ts # Gestionnaire d'erreurs général │ ├── public │ │ ├── index.html # Fichier HTML principal du frontend │ │ ├── css │ │ │ └── style.css # Style de base │ │ └── js │ │ └── script.js # Logique JavaScript (incl. API WebAuthn) │ ├── routes # Définitions des routes API et de leurs gestionnaires │ │ └── routes.ts # Routes spécifiques aux passkeys │ ├── services │ │ ├── credentialService.ts# Interagit avec la table des identifiants │ │ └── userService.ts # Interagit avec la table des utilisateurs │ ├── utils # Fonctions d'aide et utilitaires │ | ├── constants.ts # Quelques constantes (ex. rpID) │ | └── utils.ts # Fonction d'aide │ ├── database.ts # Crée la connexion de Node.js à la base de données MySQL │ ├── index.ts # Point d'entrée du serveur Node.js │ └── server.ts # Gère tous les paramètres du serveur ├── config.json # Quelques configurations pour le projet Node.js ├── docker-compose.yml # Définit les services, réseaux et volumes pour les conteneurs Docker ├── Dockerfile # Crée une image Docker du projet ├── init-db.sql # Définit notre schéma de base de données MySQL ├── package.json # Gère les dépendances et les scripts du projet Node.js └── tsconfig.json # Configure la manière dont TypeScript compile votre code

4. Configuration de la base de données MySQL#

Lors de l'implémentation des passkeys, la configuration de la base de données est un composant clé. Notre approche utilise un conteneur Docker exécutant MySQL, offrant un environnement simple et isolé, essentiel pour des tests et un déploiement fiables.

Notre schéma de base de données est intentionnellement minimaliste, ne comportant que deux tables. Cette simplicité facilite une compréhension plus claire et une maintenance plus aisée.

Structure détaillée des tables

1. Table des identifiants (Credentials) : Centrale pour l'authentification par passkey, cette table stocke les identifiants passkey. Colonnes critiques :

  • credential_id : Un identifiant unique pour chaque identifiant. Le choix du type de données correct pour ce champ est vital pour éviter les erreurs de formatage.
  • public_key : Stocke la clé publique pour chaque identifiant. Comme pour credential_id, un type de données et un formatage appropriés sont cruciaux.

2. Table des utilisateurs (Users) : Lie les comptes utilisateurs à leurs identifiants correspondants.

Notez que nous avons nommé la première table credentials car cela correspond à notre expérience et à ce que d'autres bibliothèques recommandent comme étant plus approprié (contrairement à la suggestion de SimpleWebAuthn de la nommer authenticator ou authenticator_device).

Les types de données pour credential_id et public_key sont cruciaux. Des erreurs surviennent souvent à cause de types de données, d'encodage ou de formatage incorrects (en particulier, la différence entre Base64 et Base64URL est une cause fréquente d'erreurs), ce qui peut perturber tout le processus d'inscription ou de connexion.

Toutes les commandes SQL nécessaires à la configuration de ces tables sont contenues dans le fichier init-db.sql. Ce script garantit une initialisation rapide et sans erreur de la base de données.

Pour des cas plus sophistiqués, vous pouvez ajouter credential_device_type ou credential_backed_up pour stocker plus d'informations sur les identifiants et améliorer l'expérience utilisateur. Nous nous en abstenons cependant dans ce tutoriel.

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) );

Après avoir créé ce fichier, nous créons un nouveau fichier docker-compose.yml à la racine du projet :

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

Ce fichier démarre la base de données MySQL sur le port 3306 et crée la structure de base de données définie. Il est important de noter que le nom et le mot de passe de la base de données utilisés ici sont simples à des fins de démonstration. Dans un environnement de production, vous devriez utiliser des identifiants plus complexes pour une sécurité renforcée.

Ensuite, nous passons à l'exécution de notre conteneur Docker. À ce stade, notre fichier docker-compose.yml ne comprend que ce seul conteneur, mais nous ajouterons d'autres composants plus tard. Pour démarrer le conteneur, utilisez la commande suivante :

docker compose up -d

Une fois le conteneur en cours d'exécution, nous devons vérifier si la base de données fonctionne comme prévu. Ouvrez un terminal et exécutez la commande suivante pour interagir avec la base de données MySQL :

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

Il vous sera demandé d'entrer le mot de passe root, qui est my-secret-pw dans notre exemple. Après vous être connecté, sélectionnez la base de données webauthn_db et affichez les tables à l'aide de ces commandes :

use webauthn_db; show tables;

À ce stade, vous devriez voir les deux tables définies dans notre script. Initialement, ces tables seront vides, ce qui indique que notre configuration de base de données est terminée et prête pour les prochaines étapes de l'implémentation des passkeys.

5. Implémentation des passkeys : Étapes d'intégration du backend#

Le backend est le cœur de toute application passkey, agissant comme le hub central pour le traitement des requêtes d'authentification utilisateur provenant du frontend. Il communique avec la bibliothèque serveur WebAuthn pour gérer les requêtes d'inscription et de connexion, et il interagit avec votre base de données MySQL pour stocker et récupérer les identifiants utilisateur. Ci-dessous, nous vous guiderons dans la configuration de votre backend en utilisant Node.js (Express) avec TypeScript, qui exposera une API publique pour gérer toutes les requêtes.

5.1 Initialiser le serveur Node.js (Express)#

Tout d'abord, créez un nouveau répertoire pour votre projet et naviguez-y à l'aide de votre terminal ou de votre invite de commande.

Exécutez la commande

npx create-express-typescript-application passkeys-tutorial

Cela crée un squelette de code de base d'une application Node.js (Express) écrite en TypeScript que nous pouvons utiliser pour des adaptations ultérieures.

Votre projet nécessite plusieurs paquets clés que nous devons installer en plus :

  • @simplewebauthn/server : Une bibliothèque côté serveur pour faciliter les opérations WebAuthn, telles que l'inscription (sign-up) et l'authentification (login) des utilisateurs.
  • express-session : Middleware pour Express.js pour gérer les sessions, stocker les données de session côté serveur et gérer les cookies.
  • uuid : Un utilitaire pour générer des identifiants uniques universels (UUID), couramment utilisés pour créer des clés ou des identifiants uniques dans les applications.
  • mysql2 : Un client Node.js pour MySQL, fournissant des capacités pour se connecter et exécuter des requêtes sur des bases de données MySQL.

Passez dans le nouveau répertoire et installez-les avec les commandes suivantes (nous installons également les types TypeScript requis) :

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

Pour confirmer que tout est correctement installé, exécutez

npm run dev:nodemon

Cela devrait démarrer votre serveur Node.js en mode développement avec Nodemon, qui redémarre automatiquement le serveur à chaque modification de fichier.

Conseil de dépannage : Si vous rencontrez des erreurs, essayez de mettre à jour ts-node vers la version 10.8.1 dans le fichier package.json, puis exécutez npm i pour installer les mises à jour.

Votre fichier server.ts a la configuration de base et le middleware pour une application Express. Pour intégrer la fonctionnalité passkey, vous devrez ajouter :

  • Routes : Définir de nouvelles routes pour l'inscription (sign-up) et l'authentification (login) par passkey.
  • Contrôleurs : Créer des contrôleurs pour gérer la logique de ces routes.
  • Middleware : Intégrer un middleware pour la gestion des requêtes et des erreurs.
  • Services : Construire des services pour récupérer et stocker des données dans la base de données.
  • Fonctions utilitaires : Inclure des fonctions utilitaires pour des opérations de code efficaces.

Ces améliorations sont essentielles pour permettre l'authentification par passkey dans le backend de votre application. Nous les configurerons plus tard.

Debugger Icon

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

Try for Free

5.2 Connexion à la base de données MySQL#

Après avoir créé et démarré la base de données dans la section 4, nous devons maintenant nous assurer que notre backend peut se connecter à la base de données MySQL. Pour ce faire, nous créons un nouveau fichier database.ts dans le dossier /src et ajoutons le contenu suivant :

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();

Ce fichier sera utilisé plus tard par notre serveur pour accéder à la base de données.

5.3 Configuration du serveur d'application#

Jetons un bref coup d'œil à notre config.json, où deux variables sont déjà définies : le port sur lequel nous exécutons l'application et l'environnement :

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

package.json peut rester tel quel et devrait ressembler à :

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 ressemble à :

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); });

Dans server.ts, nous devons adapter quelques autres choses. De plus, un cache temporaire d'une sorte (par exemple, redis, memcache ou express-session) est nécessaire pour stocker les challenges temporaires contre lesquels les utilisateurs peuvent s'authentifier. Nous avons décidé d'utiliser express-session et de déclarer le module express-session en haut pour que les choses fonctionnent avec express-session. De plus, nous rationalisons le routage et supprimons la gestion des erreurs pour l'instant (cela sera ajouté au middleware plus tard) :

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 Service des identifiants et service des utilisateurs#

Pour gérer efficacement les données dans nos deux tables créées, nous allons développer deux services distincts dans un nouveau répertoire src/services : authenticatorService.ts et userService.ts.

Chaque service encapsulera les méthodes CRUD (Create, Read, Update, Delete), nous permettant d'interagir avec la base de données de manière modulaire et organisée. Ces services faciliteront le stockage, la récupération et la mise à jour des données dans les tables authenticator et user. Voici comment la structure de ces fichiers requis devrait être organisée :

userService.ts ressemble à ceci :

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 ressemble à ce qui suit :

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#

Pour gérer les erreurs de manière centralisée et faciliter le débogage, nous ajoutons un fichier 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 }); };

De plus, nous ajoutons un nouveau fichier customError.ts car nous voudrons plus tard pouvoir créer des erreurs personnalisées pour nous aider à trouver les bogues plus rapidement :

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 Utilitaires#

Dans le dossier utils, nous créons deux fichiers constants.ts et utils.ts.

constant.ts contient quelques informations de base sur le serveur WebAuthn, comme le nom de la relying party, l'ID de la relying party et l'origine :

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

utils.ts contient deux fonctions dont nous aurons besoin plus tard pour l'encodage et le décodage des données :

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 Contrôleurs Passkey avec SimpleWebAuthn#

Nous arrivons maintenant au cœur de notre backend : les contrôleurs. Nous créons deux contrôleurs, un pour la création d'un nouveau passkey (registration.ts) et un pour la connexion avec un passkey (authentication.ts).

registration.ts ressemble à ceci :

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; } };

Passons en revue les fonctionnalités de nos contrôleurs, qui gèrent les deux points de terminaison clés du processus d'inscription WebAuthn. C'est aussi là que réside l'une des plus grandes différences avec l'authentification par mot de passe : pour chaque tentative d'inscription ou de connexion, deux appels API backend sont nécessaires, ce qui requiert un contenu frontend spécifique entre les deux. Les mots de passe n'ont généralement besoin que d'un seul point de terminaison.

1. Point de terminaison handleRegisterStart :

Ce point de terminaison est déclenché par le frontend, recevant un nom d'utilisateur pour créer un nouveau passkey et un nouveau compte. Dans cet exemple, nous n'autorisons la création d'un nouveau compte / passkey que s'il n'existe pas encore de compte. Dans les applications réelles, vous devriez gérer cela de manière à ce que les utilisateurs soient informés qu'un passkey existe déjà et que l'ajout depuis le même appareil n'est pas possible (mais l'utilisateur pourrait ajouter des passkeys depuis un autre appareil après une forme de confirmation). Pour des raisons de simplicité, nous ignorons cela dans ce tutoriel.

Les PublicKeyCredentialCreationOptions sont préparées. residentKey est défini sur preferred, et attestationType sur direct, recueillant plus de données de l'authentificateur pour un stockage potentiel en base de données.

En général, les PublicKeyCredentialCreationOptions se composent des données suivantes :

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 : Représente les informations de la relying party (site web ou service), incluant généralement son nom (rp.name) et le domaine (rp.id).
  • user : Contient les détails du compte utilisateur comme user.name, user.id et user.displayName.
  • challenge : Une valeur aléatoire et sécurisée créée par le serveur WebAuthn pour empêcher les attaques par rejeu lors du processus d'inscription.
  • pubKeyCredParams : Spécifie le type d'identifiant à clé publique à créer, y compris l'algorithme cryptographique utilisé.
  • timeout : Optionnel, définit le temps en millisecondes dont dispose l'utilisateur pour terminer l'interaction.
  • excludeCredentials : Une liste d'identifiants à exclure ; utilisée pour empêcher l'enregistrement d'un passkey pour le même appareil / authentificateur plusieurs fois.
  • authenticatorSelection : Critères pour sélectionner l'authentificateur, par exemple s'il doit prendre en charge la vérification de l'utilisateur ou comment les clés résidentes (resident keys) doivent être encouragées.
  • attestation : Spécifie la préférence de transmission de l'attestation, comme « none », « indirect » ou « direct ».
  • extensions : Optionnel, permet des extensions client supplémentaires.

L'ID utilisateur et le challenge sont stockés dans un objet de session, simplifiant le processus à des fins de tutoriel. De plus, la session est effacée après chaque tentative d'inscription ou de connexion.

2. Point de terminaison handleRegisterFinish :

Ce point de terminaison récupère l'ID utilisateur et le challenge définis précédemment. Il vérifie la RegistrationResponse avec le challenge. Si elle est valide, il stocke un nouvel identifiant pour l'utilisateur. Une fois stocké dans la base de données, l'ID utilisateur et le challenge sont supprimés de la session.

Conseil : Lors du débogage de votre application, nous vous recommandons vivement d'utiliser Chrome comme navigateur et ses fonctionnalités intégrées pour améliorer l'expérience de développement des applications basées sur les passkeys, par exemple l'authentificateur WebAuthn virtuel et le journal de l'appareil (voir nos conseils supplémentaires pour les développeurs de passkeys ci-dessous pour plus d'informations).

Ensuite, nous passons à authentication.ts, qui a une structure et une fonctionnalité similaires.

authentication.ts ressemble à ceci :

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; } };

Notre processus d'authentification (connexion) implique deux points de terminaison :

1. Point de terminaison handleLoginStart :

Ce point de terminaison est activé lorsqu'un utilisateur tente de se connecter. Il vérifie d'abord si le nom d'utilisateur existe dans la base de données, renvoyant une erreur s'il n'est pas trouvé. Dans un scénario réel, vous pourriez proposer de créer un nouveau compte à la place.

Pour les utilisateurs existants, il récupère l'ID utilisateur de la base de données, le stocke dans la session et génère des options PublicKeyCredentialRequestOptions. allowCredentials est laissé vide pour ne pas restreindre l'utilisation des identifiants. C'est pourquoi tous les passkeys disponibles pour cette relying party peuvent être sélectionnés dans la modale des passkeys.

Le challenge généré est également stocké dans la session et les PublicKeyCredentialRequestOptions sont renvoyées au frontend.

Les PublicKeyCredentialRequestOptions se composent des données suivantes :

dictionary PublicKeyCredentialRequestOptions { required BufferSource challenge; unsigned long timeout; USVString rpId; sequence<PublicKeyCredentialDescriptor> allowCredentials = []; DOMString userVerification = "preferred"; AuthenticationExtensionsClientInputs extensions; };
  • challenge : Une valeur aléatoire et sécurisée du serveur WebAuthn utilisée pour empêcher les attaques par rejeu lors du processus d'authentification.
  • timeout : Optionnel, définit le temps en millisecondes dont dispose l'utilisateur pour répondre à la demande d'authentification.
  • rpId : L'ID de la relying party, généralement le domaine du service.
  • allowCredentials : Une liste optionnelle de descripteurs d'identifiants, spécifiant quels identifiants peuvent être utilisés pour cette authentification (connexion).
  • userVerification : Spécifie l'exigence de vérification de l'utilisateur, comme « required », « preferred » ou « discouraged ».
  • extensions : Optionnel, permet des extensions client supplémentaires.

2. Point de terminaison handleLoginFinish :

Ce point de terminaison récupère le currentChallenge et le loggedInUserId de la session.

Il interroge la base de données pour le bon identifiant en utilisant l'ID de l'identifiant du corps de la requête. Si l'identifiant est trouvé, cela signifie que l'utilisateur associé à cet ID d'identifiant peut maintenant être authentifié (connecté). Ensuite, nous pouvons interroger l'utilisateur dans la table des utilisateurs via l'ID utilisateur que nous obtenons de l'identifiant et vérifier l'authenticationResponse en utilisant le challenge et le corps de la requête. Si tout est réussi, nous affichons le message de succès de la connexion. Si aucun identifiant correspondant n'est trouvé, une erreur est envoyée.

De plus, si la vérification réussit, le compteur de l'identifiant est mis à jour, le challenge utilisé et le loggedInUserId sont supprimés de la session.

En plus de cela, nous pouvons supprimer le dossier src/app et src/constant ainsi que tous les fichiers qu'ils contiennent.

Note : La gestion de session appropriée et la protection des routes, cruciales dans les applications réelles, sont omises ici pour des raisons de simplicité dans ce tutoriel.

5.8 Routes Passkey#

Enfin, nous devons nous assurer que nos contrôleurs sont accessibles en ajoutant les routes appropriées à routes.ts qui se trouve dans un nouveau répertoire 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. Intégrer les passkeys dans le frontend#

Cette partie du tutoriel sur les passkeys se concentre sur la manière de prendre en charge les passkeys dans le frontend de votre application. Nous avons un frontend très basique composé de trois fichiers : index.html, styles.css et script.js. Ces trois fichiers se trouvent dans un nouveau dossier src/public.

Le fichier index.html contient un champ de saisie pour le nom d'utilisateur et deux boutons pour s'inscrire et se connecter. De plus, nous importons le script @simplewebauthn/browser qui simplifie l'interaction avec l'API Web Authentication du navigateur dans le fichier js/script.js.

index.html ressemble à ceci :

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 ressemble à ce qui suit :

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); } }

Dans script.js, il y a trois fonctions principales :

1. Fonction showMessage :

C'est une fonction utilitaire utilisée principalement pour afficher des messages d'erreur, aidant au débogage.

2. Fonction Register :

Déclenchée lorsque l'utilisateur clique sur « Register ». Elle extrait le nom d'utilisateur du champ de saisie et l'envoie au point de terminaison passkeyRegisterStart. La réponse inclut les PublicKeyCredentialCreationOptions, qui sont converties en JSON et passées à SimpleWebAuthnBrowser.startRegistration. Cet appel active l'authentificateur de l'appareil (comme Face ID ou Touch ID). Après une authentification locale réussie, le challenge signé est renvoyé au point de terminaison passkeyRegisterFinish, complétant le processus de création de passkey.

Pendant le processus d'inscription, l'objet d'attestation joue un rôle crucial, alors examinons-le de plus près.

L'objet d'attestation se compose principalement de trois éléments : fmt, attStmt et authData. L'élément fmt signifie le format de la déclaration d'attestation, tandis que attStmt représente la déclaration d'attestation elle-même. Dans les scénarios où l'attestation est jugée inutile, le fmt sera désigné comme « none », ce qui conduit à un attStmt vide.

L'accent est mis sur le segment authData au sein de cette structure. Ce segment est essentiel pour récupérer des éléments clés tels que l'ID de la relying party, les flags, le compteur et les données d'identifiant attestées sur notre serveur. Concernant les flags, BS (Backup State) et BE (Backup Eligibility) sont particulièrement intéressants car ils fournissent plus d'informations si un passkey est synchronisé (par exemple via le Trousseau iCloud ou 1Password). De plus, UV (User Verification) et UP (User Presence) fournissent d'autres informations utiles.

Il est important de noter que diverses parties de l'objet d'attestation, y compris les données de l'authentificateur, l'ID de la relying party et la déclaration d'attestation, sont soit hachées, soit signées numériquement par l'authentificateur à l'aide de sa clé privée. Ce processus est essentiel pour maintenir l'intégrité globale de l'objet d'attestation.

3. Fonction Login :

Activée lorsque l'utilisateur clique sur « Login ». Similaire à la fonction d'inscription, elle extrait le nom d'utilisateur et l'envoie au point de terminaison passkeyLoginStart. La réponse, contenant les PublicKeyCredentialRequestOptions, est convertie en JSON et utilisée avec SimpleWebAuthnBrowser.startAuthentication. Cela déclenche l'authentification locale sur l'appareil. Le challenge signé est ensuite renvoyé au point de terminaison passkeyLoginFinish. Une réponse réussie de ce point de terminaison indique que l'utilisateur s'est connecté à l'application avec succès.

De plus, le fichier CSS d'accompagnement fournit un style simple pour l'application :

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. Exécuter l'application d'exemple Passkey#

Pour voir votre application en action, compilez et exécutez votre code TypeScript avec :

npm run dev

Votre serveur devrait maintenant être opérationnel à l'adresse http://localhost:8080.

Considérations pour la production :

N'oubliez pas que ce que nous avons couvert n'est qu'un aperçu de base. Lors du déploiement d'une application passkey dans un environnement de production, vous devez approfondir les points suivants :

  • Mesures de sécurité : Mettre en œuvre des pratiques de sécurité robustes pour protéger les données des utilisateurs.
  • Gestion des erreurs : S'assurer que votre application gère et enregistre les erreurs avec élégance.
  • Gestion de la base de données : Optimiser les opérations de la base de données pour la scalabilité et la fiabilité.

8. Intégration DevOps des Passkeys#

Nous avons déjà configuré un conteneur Docker pour notre base de données. Ensuite, nous allons étendre notre configuration Docker Compose pour inclure le serveur avec le backend et le frontend. Votre fichier docker-compose.yml doit être mis à jour en conséquence.

Pour conteneuriser notre application, nous créons un nouveau Dockerfile qui installe les paquets requis et démarre le serveur de développement :

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"]

Ensuite, nous étendons également le fichier docker-compose.yml pour démarrer ce conteneur :

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

Si vous exécutez maintenant docker compose up dans votre terminal et accédez à http://localhost:8080, vous devriez voir la version fonctionnelle de votre application web passkey (ici, fonctionnant sur Windows 11 23H2 + Chrome 119) :

9. Conseils supplémentaires sur les Passkeys pour les développeurs#

Comme nous travaillons depuis un certain temps sur des implémentations de passkeys, nous avons rencontré quelques défis si vous travaillez sur des applications passkey réelles :

  • Compatibilité et prise en charge des appareils / plateformes
  • Intégration et éducation des utilisateurs
  • Gestion des appareils perdus ou changés
  • Authentification multiplateforme
  • Mécanismes de secours
  • Complexité de l'encodage : L'encodage est souvent la partie la plus difficile car vous devez gérer du JSON, du CBOR, des uint8arrays, des buffers, des blobs, différentes bases de données, base64 et base64url où de nombreuses erreurs peuvent survenir
  • Gestion des passkeys (par exemple, pour ajouter, supprimer ou renommer des passkeys)

De plus, nous avons les conseils suivants pour les développeurs en ce qui concerne la partie implémentation :

Utiliser le débogueur de Passkeys

Le débogueur de Passkeys aide à tester différentes configurations de serveur WebAuthn et réponses client. De plus, il fournit un excellent analyseur pour les réponses de l'authentificateur.

Déboguer avec la fonctionnalité de journal de l'appareil de Chrome

Utilisez le journal de l'appareil de Chrome (accessible via chrome://device-log/) pour surveiller les appels FIDO/WebAuthn. Cette fonctionnalité fournit des journaux en temps réel du processus d'authentification (connexion), vous permettant de voir les données échangées et de résoudre les problèmes qui surviennent.

Un autre raccourci très utile pour obtenir tous vos passkeys dans Chrome est d'utiliser chrome://settings/passkeys.

Utiliser l'authentificateur WebAuthn virtuel de Chrome

Pour éviter d'utiliser l'invite Touch ID, Face ID ou Windows Hello pendant le développement, Chrome est livré avec un authentificateur WebAuthn virtuel très pratique qui émule un véritable authentificateur. Nous vous recommandons vivement de l'utiliser pour accélérer les choses. Trouvez plus de détails ici.

Tester sur différentes plateformes et navigateurs

Assurez la compatibilité et la fonctionnalité sur divers navigateurs et plateformes. WebAuthn se comporte différemment selon les navigateurs, un test approfondi est donc essentiel.

Tester sur différents appareils

Ici, il est particulièrement utile de travailler avec des outils comme ngrok, où vous pouvez rendre votre application locale accessible sur d'autres appareils (mobiles).

Définir la vérification de l'utilisateur sur « Preferred »

Lors de la définition des propriétés pour userVerification dans les PublicKeyCredentialRequestOptions, choisissez de les définir sur preferred car c'est un bon compromis entre convivialité et sécurité. Cela signifie que des contrôles de sécurité sont en place sur les appareils appropriés tout en maintenant la convivialité sur les appareils sans capacités biométriques.

10. Conclusion : Tutoriel Passkey#

Nous espérons que ce tutoriel sur les passkeys vous a permis de bien comprendre comment implémenter efficacement les passkeys. Tout au long du tutoriel, nous avons parcouru les étapes essentielles pour créer une application passkey, en nous concentrant sur les concepts fondamentaux et l'implémentation pratique. Bien que ce guide serve de point de départ, il y a beaucoup plus à explorer et à affiner dans le monde de WebAuthn.

Nous encourageons les développeurs à approfondir les nuances des passkeys (par exemple, l'ajout de plusieurs passkeys, la vérification de la compatibilité des appareils avec les passkeys ou l'offre de solutions de récupération). C'est un voyage qui vaut la peine d'être entrepris, offrant à la fois des défis et d'immenses récompenses dans l'amélioration de l'authentification des utilisateurs. Avec les passkeys, vous ne construisez pas seulement une fonctionnalité ; vous contribuez à un monde numérique plus sûr et plus convivial.

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