Pourquoi les mots de passe posent problème
Le mot de passe est à la fois simple à comprendre et difficile à sécuriser. Les utilisateurs le réutilisent, le choisissent trop court, le stockent parfois dans des endroits peu fiables, puis doivent gérer des flux de réinitialisation qui deviennent eux-mêmes des surfaces d’attaque.
Pour un développeur web, cela signifie beaucoup de complexité : hachage robuste, politique de longueur, protection contre le credential stuffing, récupération de compte, double facteur, sessions, journalisation, détection d’anomalies. Même avec une bonne Content Security Policy, une interface bien conçue et un backend sérieux, le mot de passe reste une faiblesse structurelle.
Les passkeys proposent une autre approche : au lieu de demander à l’utilisateur de connaître un secret, on lui demande de prouver qu’il possède une clé privée stockée sur son appareil ou dans son gestionnaire de mots de passe. Le site ne reçoit jamais cette clé privée. Il conserve seulement une clé publique associée au compte.
WebAuthn, passkeys et FIDO2 : clarifier les termes
WebAuthn est une API web standard permettant à un navigateur de créer et d’utiliser des identifiants cryptographiques. FIDO2 désigne l’ensemble plus large qui inclut WebAuthn côté navigateur et CTAP côté authentificateur matériel ou système.
Une passkey est, en pratique, une expérience utilisateur moderne construite sur WebAuthn. Elle peut être synchronisée entre plusieurs appareils via un écosystème comme iCloud Keychain, Google Password Manager ou un gestionnaire compatible. L’utilisateur peut alors se connecter avec Face ID, Touch ID, Windows Hello, un code de déverrouillage ou une clé de sécurité.
L’intérêt principal est double :
- le serveur ne stocke pas de secret réutilisable ;
- l’identifiant est lié au domaine, ce qui réduit fortement le risque de phishing.
Cela ne veut pas dire que toute l’architecture de sécurité disparaît. Les passkeys remplacent l’étape d’authentification, mais il faut toujours gérer les sessions, les autorisations, les journaux, la révocation et les scénarios de récupération.
Le principe cryptographique en pratique
Lors de l’inscription, le serveur génère un challenge aléatoire. Le navigateur demande ensuite à l’authentificateur de créer une paire de clés. La clé privée reste protégée localement. La clé publique, l’identifiant de credential et quelques métadonnées sont renvoyés au serveur.
Lors de la connexion, le serveur génère un nouveau challenge. L’authentificateur signe ce challenge avec la clé privée. Le serveur vérifie la signature avec la clé publique enregistrée.
Le flux ressemble à ceci :
// Vue simplifiée d'un credential stocké côté serveur
type StoredCredential = {
userId: string;
credentialId: string;
publicKey: string;
counter: number;
transports?: AuthenticatorTransport[];
};
Le champ counter sert à détecter certains cas d’usage anormal, même si son comportement dépend des authentificateurs. Il ne faut donc pas construire toute la sécurité du système sur cette seule valeur.
Créer une passkey côté navigateur
L’API centrale est navigator.credentials.create(). Elle doit être appelée dans un contexte sécurisé, généralement en HTTPS, et à la suite d’une action utilisateur explicite.
async function registerPasskey() {
const optionsResponse = await fetch('/api/auth/webauthn/register/options', {
method: 'POST',
credentials: 'include'
});
const options = await optionsResponse.json();
const credential = await navigator.credentials.create({
publicKey: options
});
await fetch('/api/auth/webauthn/register/verify', {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(credential)
});
}
Dans un vrai projet, il faut convertir correctement les champs binaires entre base64url, ArrayBuffer et JSON. C’est l’un des points les plus fréquents d’erreur, car l’API WebAuthn manipule des données binaires tandis que les endpoints HTTP échangent souvent du JSON.
Pour éviter les conversions fragiles, il est recommandé d’utiliser une bibliothèque spécialisée comme @simplewebauthn/browser côté client et @simplewebauthn/server côté Node.js.
npm install @simplewebauthn/browser @simplewebauthn/server
Générer les options côté serveur
Côté serveur, le backend doit générer des options d’inscription contenant le challenge, l’identifiant applicatif et les informations utilisateur. L’exemple suivant illustre l’intention générale avec Node.js.
import { generateRegistrationOptions } from '@simplewebauthn/server';
export async function createRegistrationOptions(user: {
id: string;
email: string;
name: string;
}) {
return generateRegistrationOptions({
rpName: 'Formation développeur web',
rpID: 'formation-developpeur-web.fr',
userID: user.id,
userName: user.email,
userDisplayName: user.name,
attestationType: 'none',
authenticatorSelection: {
residentKey: 'preferred',
userVerification: 'preferred'
}
});
}
Le rpID est essentiel : il lie le credential à un domaine. En développement local, il faudra prévoir une configuration différente, par exemple localhost. En production, il faut être strict et éviter les variations incohérentes entre domaine racine, sous-domaines et environnements.
Vérifier l’inscription
Après navigator.credentials.create(), le serveur reçoit une réponse qu’il doit vérifier. Cette étape confirme que le challenge correspond, que l’origine est correcte et que le credential peut être associé à l’utilisateur.
import { verifyRegistrationResponse } from '@simplewebauthn/server';
export async function verifyPasskeyRegistration(params: {
response: unknown;
expectedChallenge: string;
expectedOrigin: string;
}) {
const verification = await verifyRegistrationResponse({
response: params.response,
expectedChallenge: params.expectedChallenge,
expectedOrigin: params.expectedOrigin,
expectedRPID: 'formation-developpeur-web.fr'
});
if (!verification.verified || !verification.registrationInfo) {
throw new Error('Passkey invalide');
}
return {
credentialId: verification.registrationInfo.credentialID,
publicKey: verification.registrationInfo.credentialPublicKey,
counter: verification.registrationInfo.counter
};
}
Comme pour tout endpoint recevant du JSON depuis le navigateur, ne supposez pas que la forme des données est fiable. Une approche comme celle décrite dans le typage des données API en TypeScript reste pertinente : unknown, validation explicite, erreurs contrôlées et normalisation des réponses.
Connecter l’utilisateur avec une passkey
Le flux de connexion repose sur navigator.credentials.get(). Le serveur génère d’abord des options d’authentification, puis le navigateur demande à l’authentificateur de signer le challenge.
import { startAuthentication } from '@simplewebauthn/browser';
async function loginWithPasskey() {
const optionsResponse = await fetch('/api/auth/webauthn/login/options', {
method: 'POST',
credentials: 'include'
});
const options = await optionsResponse.json();
const response = await startAuthentication(options);
const verificationResponse = await fetch('/api/auth/webauthn/login/verify', {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(response)
});
if (!verificationResponse.ok) {
throw new Error('Connexion impossible');
}
}
L’interface ne doit pas présenter les passkeys comme une fonctionnalité magique. Il faut expliquer clairement ce qui va se passer : l’utilisateur va confirmer son identité avec le mécanisme de déverrouillage de son appareil ou de son gestionnaire de mots de passe.
Concevoir une bonne expérience utilisateur
L’enjeu n’est pas seulement technique. Une authentification réussie est aussi un parcours compréhensible.
Prévoyez au minimum :
- un bouton explicite comme “Se connecter avec une passkey” ;
- un flux d’ajout de passkey depuis les paramètres du compte ;
- une liste des passkeys enregistrées ;
- la possibilité de supprimer une passkey ;
- un mécanisme de récupération de compte robuste.
Pour les formulaires d’inscription et de connexion, les principes décrits dans les formulaires accessibles restent indispensables : labels visibles, messages d’erreur compréhensibles, focus clavier, ordre logique et compatibilité avec les lecteurs d’écran.
Une erreur fréquente consiste à masquer entièrement le flux classique. En phase de transition, beaucoup de produits conservent un mode alternatif : lien magique, mot de passe existant, authentification sociale ou procédure de récupération. L’important est de ne pas recréer une porte dérobée moins sécurisée que le système principal.
Gérer les erreurs sans perdre l’utilisateur
WebAuthn peut échouer pour de nombreuses raisons : navigateur incompatible, action annulée, appareil non disponible, challenge expiré, domaine incorrect, passkey absente, authentificateur verrouillé.
Il faut distinguer les erreurs techniques des erreurs utilisateur. Une annulation volontaire ne doit pas être affichée comme une alerte critique. À l’inverse, une origine invalide ou un challenge incorrect doit être journalisé sérieusement côté serveur.
type PasskeyLoginResult =
| { status: 'success' }
| { status: 'cancelled' }
| { status: 'not-supported' }
| { status: 'failed'; message: string };
async function safeLoginWithPasskey(): Promise<PasskeyLoginResult> {
if (!window.PublicKeyCredential) {
return { status: 'not-supported' };
}
try {
await loginWithPasskey();
return { status: 'success' };
} catch (error) {
if (error instanceof DOMException && error.name === 'NotAllowedError') {
return { status: 'cancelled' };
}
return {
status: 'failed',
message: 'La connexion avec passkey a échoué.'
};
}
}
Cette modélisation explicite rejoint l’esprit du Result pattern en TypeScript : représenter les issues attendues plutôt que tout transformer en exception générique.
Sécurité : les points à ne pas négliger
Les passkeys réduisent fortement certains risques, mais elles ne remplacent pas toute la sécurité applicative.
Le serveur doit stocker les challenges temporairement, idéalement avec une durée de vie courte. Il doit vérifier l’origine, le rpID, le challenge et la signature. Il doit aussi protéger les endpoints contre les abus, par exemple avec du rate limiting.
Les sessions créées après authentification doivent être sécurisées avec des cookies HttpOnly, Secure et SameSite adaptés. Les actions sensibles peuvent exiger une réauthentification récente, même si l’utilisateur possède déjà une session active.
Il faut également penser à la récupération de compte. Si un utilisateur perd tous ses appareils, comment récupère-t-il son accès ? Une mauvaise réponse à cette question peut annuler les bénéfices de WebAuthn. Les codes de récupération, la validation manuelle ou les procédures administratives doivent être choisis selon le niveau de risque de l’application.
Progressive enhancement et compatibilité
Les passkeys sont de mieux en mieux supportées, mais il reste prudent de concevoir l’authentification comme une amélioration progressive. Testez la disponibilité de PublicKeyCredential, affichez des alternatives pertinentes et évitez de bloquer inutilement l’utilisateur.
export function supportsPasskeys() {
return typeof window !== 'undefined' && 'PublicKeyCredential' in window;
}
Cette démarche s’inscrit dans une logique de progressive enhancement : proposer une base fonctionnelle, puis enrichir l’expérience quand le navigateur, l’appareil et le contexte le permettent.
Conclusion
Les passkeys ne sont pas seulement une nouvelle option de connexion. Elles changent le modèle mental de l’authentification web : moins de secrets partagés, moins de phishing, moins de frictions pour l’utilisateur.
Pour les intégrer correctement, il faut toutefois traiter WebAuthn comme un vrai sujet d’architecture. Le frontend doit offrir une expérience claire et accessible. Le backend doit vérifier rigoureusement les challenges, signatures, origines et credentials. Le produit doit prévoir la gestion du cycle de vie : ajout, suppression, récupération, révocation et support.
Bien mises en place, les passkeys permettent de construire une authentification plus robuste que le traditionnel couple email/mot de passe, sans sacrifier l’ergonomie. C’est précisément le type d’évolution web qui mérite d’être compris en profondeur plutôt que simplement branché comme une dépendance de plus.