1. Comprendre le principe avant de coder

Une passkey remplace le mot de passe par une paire de clés cryptographiques : une clé publique stockée côté serveur, une clé privée qui ne quitte jamais l'appareil de l'utilisateur (Secure Enclave sur iPhone, TPM sur Windows, puce sécurisée sur Android).

Le protocole repose sur deux standards ouverts :
- WebAuthn : l'API du navigateur, validée par le W3C
- FIDO2 : l'ensemble plus large porté par la FIDO Alliance, qui inclut WebAuthn côté navigateur et CTAP2 pour dialoguer avec des authentificateurs externes (clé USB YubiKey, par exemple)

Deux flux à retenir, appelés « cérémonies » dans le vocabulaire WebAuthn :

-Inscription :Le serveur génère un challenge → l'appareil crée une paire de clés → il signe le challenge avec la clé privée → le serveur stocke la clé publique
-Authentification : Le serveur génère un nouveau challenge → l'appareil le signe avec la clé privée déjà enregistrée → le serveur vérifie avec la clé publique stockée

Comme le secret ne transite jamais et ne réside jamais côté serveur, il n'y a rien à voler en cas de phishing ou de fuite de base de données.

 2. Prérequis

- Node.js
- Une base de données (les exemples ci-dessous utilisent un schéma relationnel générique, adaptable à PostgreSQL, MySQL, SQLite…)
- HTTPS obligatoire — WebAuthn ne fonctionne qu'en contexte sécurisé. Seul localhost est exempté pour le développement local.

Installation de la librairie de référence :
bash
npm install @simplewebauthn/server @simplewebauthn/browser

@simplewebauthn/server gère la partie backend (génération des challenges, vérification des signatures). @simplewebauthn/browser s'occupe côté client de l'appel aux API natives du navigateur navigator.credentials.create() / navigator.credentials.get()).

 3. Modéliser les données

Chaque utilisateur peut avoir plusieurs passkeys (un par appareil). Un schéma minimal :

```sql
CREATE TABLE users (
  id SERIAL PRIMARY KEY,
  username TEXT UNIQUE NOT NULL
);

CREATE TABLE credentials (
  id TEXT PRIMARY KEY,              -- credential ID (base64url)
  user_id INTEGER REFERENCES users(id),
  public_key BYTEA NOT NULL,        -- clé publique COSE
  counter BIGINT NOT NULL DEFAULT 0,-- compteur anti-clonage
  device_type TEXT,                 -- 'singleDevice' ou 'multiDevice'
  backed_up BOOLEAN DEFAULT FALSE,
  transports TEXT[]                 -- ['internal', 'hybrid', ...]
);
```

Le counter sert à détecter un clonage de clé physique : s'il ne progresse pas entre deux authentifications alors qu'il le devrait, c'est un signal d'alerte.

 4. Cérémonie d'inscription

Étape 1 — générer les options d'inscription

```javascript
import { generateRegistrationOptions } from '@simplewebauthn/server';

app.post('/register/start', async (req, res) => {
  const user = await getUserByUsername(req.body.username);

  const options = await generateRegistrationOptions({
    rpName: 'Mon Application',
    rpID: 'monapp.com',           // domaine exact, sans protocole ni port
    userID: Buffer.from(String(user.id)),
    userName: user.username,
    attestationType: 'none',      // suffisant pour la plupart des cas d'usage
    excludeCredentials: (await getCredentialsByUserId(user.id)).map(c => ({
      id: c.id,
      transports: c.transports,
    })),
    authenticatorSelection: {
      residentKey: 'required',     // credential "discoverable"
      userVerification: 'preferred',
    },
  });

  // Le challenge doit être accessible à l'étape de vérification
  await saveChallenge(user.id, options.challenge);

  res.json(options);
});
```
 Étape 2 — vérifier la réponse de l'appareil

```javascript
import { verifyRegistrationResponse } from '@simplewebauthn/server';

app.post('/register/verify', async (req, res) => {
  const user = await getUserByUsername(req.body.username);
  const expectedChallenge = await getChallenge(user.id);

  const verification = await verifyRegistrationResponse({
    response: req.body.credential,
    expectedChallenge,
    expectedOrigin: 'https://monapp.com',
    expectedRPID: 'monapp.com',
  });

  if (verification.verified) {
    const { credentialID, credentialPublicKey, counter } = verification.registrationInfo;
    await saveCredential({
      id: credentialID,
      userId: user.id,
      publicKey: credentialPublicKey,
      counter,
    });
  }

  res.json({ verified: verification.verified });
});
```

 5. Cérémonie d'authentification

 Étape 1 — générer les options de connexion

```javascript
import { generateAuthenticationOptions } from '@simplewebauthn/server';

app.post('/login/start', async (req, res) => {
  const options = await generateAuthenticationOptions({
    rpID: 'monapp.com',
    userVerification: 'preferred',
    // pas besoin de préciser allowCredentials avec des credentials discoverable :
    // l'appareil sait déjà quel utilisateur se connecte
  });

  await saveChallenge(null, options.challenge); // ou en session, selon l'architecture

  res.json(options);
});
```

Étape 2 — vérifier la signature

```javascript
import { verifyAuthenticationResponse } from '@simplewebauthn/server';

app.post('/login/verify', async (req, res) => {
  const credential = await getCredentialById(req.body.credential.id);
  const expectedChallenge = await getChallenge();

  const verification = await verifyAuthenticationResponse({
    response: req.body.credential,
    expectedChallenge,
    expectedOrigin: 'https://monapp.com',
    expectedRPID: 'monapp.com',
    authenticator: {
      credentialID: credential.id,
      credentialPublicKey: credential.publicKey,
      counter: credential.counter,
    },
  });

  if (verification.verified) {
    await updateCredentialCounter(credential.id, verification.authenticationInfo.newCounter);
    // créer la session utilisateur ici
  }

  res.json({ verified: verification.verified });
});
```

 6. Côté client

```javascript
import { startRegistration, startAuthentication } from '@simplewebauthn/browser';

// Inscription
async function registerPasskey(username) {
  const optionsResponse = await fetch('/register/start', {
    method: 'POST',
    body: JSON.stringify({ username }),
  });
  const options = await optionsResponse.json();

  const credential = await startRegistration(options); // déclenche Face ID / Touch ID / Windows Hello

  await fetch('/register/verify', {
    method: 'POST',
    body: JSON.stringify({ username, credential }),
  });
}

// Connexion
async function loginWithPasskey() {
  const optionsResponse = await fetch('/login/start', { method: 'POST' });
  const options = await optionsResponse.json();

  const credential = await startAuthentication(options);

  await fetch('/login/verify', {
    method: 'POST',
    body: JSON.stringify({ credential }),
  });
}
```

 7. Points d'attention pour une mise en production

- Stockage du challenge : il doit rester accessible pendant toute la durée de la cérémonie (quelques minutes). Une session ou un cache à courte durée de vie (Redis, TTL ~5 min) convient, surtout si l'infrastructure est distribuée sur plusieurs instances.
- Discoverable vs non-discoverable : les passkeys modernes stockent l'identité de l'utilisateur dans l'authentificateur lui-même residentKey: 'required'), ce qui permet une connexion sans rien taper. C'est la bonne pratique par défaut en 2026.
- Synchronisation entre appareils : un passkey iCloud Keychain ou Google Password Manager se retrouve automatiquement sur tous les appareils liés au même compte. Un passkey Windows Hello, lui, reste attaché à la puce TPM de la machine — pas de synchronisation cloud.
- Récupération de compte : c'est le point le plus souvent sous-estimé. Prévoir un code de récupération donné à l'inscription et/ou un lien magique par email vérifié comme filet de sécurité.
- Migration progressive : ne pas remplacer le mot de passe du jour au lendemain. Ajouter la passkey en option, mesurer l'adoption, garder un fallback (mot de passe, OTP par SMS) tant que l'accès aux appareils biométriques n'est pas universel dans votre base d'utilisateurs.

 8. Pour aller plus loin

- Documentation officielle SimpleWebAuthn : couvre les cas avancés (attestation, credentials cross-platform, gestion multi-appareils)
- Passkeys.dev (guide de la FIDO Alliance et de grands acteurs comme Google et Apple) : bonnes pratiques UX et design de flux
- Interface de gestion des credentials : prévoir un écran où l'utilisateur peut voir et supprimer ses passkeys enregistrées — indispensable dès qu'il y a plusieurs appareils par compte



Ce tutoriel couvre l'essentiel pour un premier flux fonctionnel. La suite naturelle est de gérer les cas particuliers : perte d'appareil, changement de navigateur, comptes multi-appareils, et télémétrie d'adoption.