---
url: 'https://www.corbado.com/fr/blog/tutoriel-passkeys-comment-implementer-passkeys'
title: 'Tutoriel Passkeys : Comment implémenter les passkeys dans les applications web'
description: 'Ce tutoriel explique comment implémenter les passkeys dans votre application web. Nous utilisons Node.js (TypeScript), SimpleWebAuthn, HTML / JavaScript Vanilla et MySQL.'
lang: 'fr'
author: 'Vincent Delitz'
date: '2025-06-20T11:06:12.676Z'
lastModified: '2026-08-22T06:03:24.419Z'
keywords: 'tutoriel passkeys'
category: 'Passkeys Implementation'
---

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

## 1. Introduction : Comment implémenter les passkeys

Dans ce tutoriel, nous vous aidons dans vos efforts
d'[implémentation](https://www.corbado.com/fr/blog/aide-memoire-cles-dacces) des passkeys, en offrant un guide
étape par étape sur la façon d'ajouter les passkeys à votre site web.

Disposer d'une [authentification](https://www.corbado.com/fr/blog/webauthn-relying-party-id-rpid-passkeys)
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](https://www.corbado.com/fr/faq/connexion-second-appareil-cle-acces-liee) véritablement
[sans mot de passe](https://www.corbado.com/fr/blog/comment-passer-totalement-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](https://www.corbado.com/blog/react-passkeys),
[Vue.js](https://www.corbado.com/blog/vuejs-passkeys) ou [Next.js](https://www.corbado.com/blog/nextjs-passkeys), 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](https://www.corbado.com/fr/blog/aide-memoire-cles-dacces) 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).

À 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](https://github.com/corbado/passkey-tutorial).

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

![Écran de connexion du tutoriel Passkey](https://www.corbado.com/website-assets/6572cd2ed5d547903c8ad74d_passkey_tutorial_register_login_screen_f55a0f5ae9.png)

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.

**Comment ajouter des passkeys à mon site web de production ?**

Ceci est un exemple très minimal
d'[authentification](https://www.corbado.com/fr/blog/webauthn-relying-party-id-rpid-passkeys) 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](https://www.corbado.com/fr/blog/webauthn-relying-party-id-rpid-passkeys) 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](https://www.corbado.com/fr/blog/aide-memoire-cles-dacces) de passkeys.

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

Avant de plonger dans l'[implémentation](https://www.corbado.com/fr/blog/aide-memoire-cles-dacces) 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](https://simplewebauthn.dev/docs/packages/browser/).

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

Pour notre backend, nous utilisons un serveur [Node.js](https://www.corbado.com/blog/nodejs-passkeys) (Express)
écrit en TypeScript. Nous avons également décidé de travailler avec
l'[implémentation](https://www.corbado.com/fr/blog/aide-memoire-cles-dacces) serveur WebAuthn de
[SimpleWebAuthn](https://simplewebauthn.dev/docs/packages/server)
(`@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](https://www.corbado.com/blog/nodejs-passkeys) 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](https://www.corbado.com/blog/passkey-webauthn-database-guide) comme
technologie de base de données. Une compréhension fondamentale de
[MySQL](https://www.corbado.com/blog/passkey-webauthn-database-guide) 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.

## 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](https://www.corbado.com/blog/passkey-webauthn-database-guide) est responsable de la
  conservation des données utilisateur et de leurs identifiants correspondants.

Avec cette [vue](https://www.corbado.com/blog/vuejs-passkeys) 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) :

![Diagramme du processus d'inscription avec Passkey](https://www.corbado.com/website-assets/6572cd4d3243003bb3589e88_passkey_sign_up_process_chart_b73d643b4c.png)

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

![Diagramme du processus de connexion avec Passkey](https://www.corbado.com/website-assets/6572cd5f04fd73a7ff5d501b_passkey_login_process_chart_a44262b767.png)

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](https://www.corbado.com/fr/faq/connexion-second-appareil-cle-acces-liee).

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.

```sql filename="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 :

```yaml filename="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](https://www.corbado.com/fr/blog/comment-activer-passkeys-android) 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 :

```bash
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 :

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

Il vous sera demandé d'entrer le [mot de passe](https://www.corbado.com/fr/blog/comment-activer-passkeys-android)
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 :

```sql
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](https://www.corbado.com/fr/blog/implementation-serveur-webauthn) pour gérer les
requêtes d'inscription et de
[connexion](https://www.corbado.com/fr/faq/connexion-second-appareil-cle-acces-liee), 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](https://www.corbado.com/blog/nodejs-passkeys) (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

```bash
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](https://www.corbado.com/blog/nodejs-passkeys) 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) :

```bash
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

```bash
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.

![Démarrage de l'application du tutoriel Passkey](https://www.corbado.com/website-assets/6572cd86e1fa2982352c7ca1_passkey_tutorial_start_app_b442392b4d.png)

**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.

### 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](#4-setup-of-the-mysql-database), 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 :

```ts filename="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 :

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

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

```json filename="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 à :

```ts filename="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) :

```ts filename="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 :

```ts filename="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 :

```ts filename="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` :

```ts filename="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 :

```ts filename="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](https://www.corbado.com/glossary/relying-party), l'ID de la
[relying party](https://www.corbado.com/glossary/relying-party) et l'origine :

```ts filename="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 :

```ts filename="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 :

```ts filename="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](https://www.corbado.com/fr/blog/comment-activer-passkeys-android) : 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](https://www.corbado.com/glossary/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](https://www.corbado.com/glossary/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](#9-additional-passkey-tips-for-developers)
ci-dessous pour plus d'informations).

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

`authentication.ts` ressemble à ceci :

```ts filename="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` :

```ts filename="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 };
```

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

```html filename="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 :

```javascript filename="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](https://www.corbado.com/faq/is-face-id-passkey) 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](https://www.corbado.com/glossary/attestation) joue un
rôle crucial, alors examinons-le de plus près.

![Objet d'attestation Passkey](https://www.corbado.com/website-assets/6572cda3f3869adee2c38e05_passkey_attestation_object_5bfcce1977.png)

L'objet d'[attestation](https://www.corbado.com/glossary/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](https://www.corbado.com/blog/1password-passkeys-best-practices-analysis)). De
plus, UV (User Verification) et UP (User Presence) fournissent d'autres informations
utiles.

![Données de l'authentificateur Passkey](https://www.corbado.com/website-assets/6572cf3ce1fa2982352d6c88_passkey_authenticator_data_7f61aceaea.png)

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 :

```css
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 :

```bash
npm run dev
```

Votre serveur devrait maintenant être opérationnel à l'adresse
[http://localhost:8080](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 filename="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 :

```yaml filename="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](http://localhost:8080), vous devriez voir la version fonctionnelle
de votre application web passkey (ici, fonctionnant sur
[Windows 11](https://www.corbado.com/blog/passkeys-windows-11) 23H2 + Chrome 119) :

![Tutoriel Passkey Windows Hello](https://www.corbado.com/website-assets/6572cf57f3869adee2c49ff5_passkey_tutorial_windows_hello_5ac0585957.png)

## 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](https://www.corbado.com/glossary/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](https://www.corbado.com/fr/blog/aide-memoire-cles-dacces) en ce qui concerne la partie
implémentation :

**Utiliser le débogueur de Passkeys**

Le [**débogueur de Passkeys**](https://www.passkeys-debugger.io/) 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/](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](chrome://settings/passkeys).

**Utiliser l'authentificateur WebAuthn virtuel de Chrome**

Pour éviter d'utiliser l'invite Touch ID, [Face ID](https://www.corbado.com/faq/is-face-id-passkey) ou
[Windows Hello](https://www.corbado.com/glossary/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](https://developer.chrome.com/docs/devtools/webauthn/).

**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.
