Authentification sécurisée et gestion des utilisateur·ices
Hash de mots de passe, sessions vs JWT, OAuth, magic links, passkeys, MFA, reset password. Pour implémenter une auth qui ne fuite pas.
À la fin du cours, tu sais
- Savoir quand utiliser une lib d'auth plutôt que coder soi-même
- Hasher correctement un mot de passe (argon2id / bcrypt)
- Choisir entre sessions serveur et JWT, et le bon cookie
- Mettre en place OAuth2 + PKCE pour Sign in with Google/GitHub
- Comprendre les passkeys et la sortie du mot de passe
- Implémenter MFA, password reset et rate limiting sans faille
Prérequis
- Connaître Node.js / Express ou équivalent
- Avoir suivi le cours OWASP Top 10 (ou notions équivalentes)
- Comprendre cookies, HTTPS, headers HTTP
Étape 1 sur 11 : Pourquoi tu ne devrais (presque) jamais coder l'auth de zéro
Chapitre 1
Pourquoi tu ne devrais (presque) jamais coder l'auth de zéro
L'authentification est le composant le plus attaqué de ton app. Une faille = fuite, RGPD, réputation. Le réflexe par défaut : ne pas coder soi-même.
Commençons ce cours par un conseil qui peut sembler paradoxal de la part d'un cours technique : dans la plupart des cas, tu ne devrais pas coder ton authentification de zéro. L'auth est le composant le plus attaqué de toute application, parce que c'est la porte d'entrée : la forcer donne accès à tout le reste. Une seule faille, et c'est une fuite de données, une notification à la CNIL au titre du RGPD, et une réputation entamée. Face à un tel enjeu, la sagesse consiste à s'appuyer sur du code éprouvé par des milliers d'yeux plutôt que sur le sien.
La raison est que la surface d'attaque est immense, bien plus large qu'on ne l'imagine. Il ne s'agit pas seulement de « vérifier un mot de passe » : il faut se prémunir contre le credential stuffing (réutilisation de mots de passe fuités ailleurs), le brute force, le vol de session par XSS ou CSRF, l'énumération de comptes, les attaques sur la réinitialisation de mot de passe, les race conditions sur le second facteur… Chacun de ces vecteurs a ses subtilités, et en oublier un seul suffit à tout compromettre. C'est précisément le genre de problème que des bibliothèques spécialisées ont déjà résolu, testé et maintenu.
Selon ton besoin, plusieurs options existent : des bibliothèques open source qui gardent le code dans ton dépôt (Lucia, Auth.js), ou des services managés qui externalisent tout (Clerk, WorkOS, Supabase Auth). Ce cours ne te pousse pas à réinventer la roue, mais à comprendre ce qui se passe sous le capot de ces outils — car même en les utilisant, tu dois savoir configurer un cookie correctement, choisir un flux OAuth, ou juger si une implémentation est saine. Et si tu dois vraiment coder l'auth toi-même, suis a minima le standard OWASP ASVS niveau 2, la checklist de référence. Les chapitres suivants te donnent ces fondations.
La surface d'attaque est énorme
- Credential stuffing (réutilisation de mots de passe leakés)
- Brute force sur le login
- Vol de session (XSS, CSRF, session fixation)
- Énumération de comptes (différentes réponses selon que l'email existe)
- Attaques sur le password reset (token réutilisable, devinable)
- Race conditions sur les MFA
Libs et services à considérer
- Lucia : lib auth open source, contrôle total, code dans ton repo
- Auth.js (ex NextAuth) : standard de l'écosystème Next.js, multi-providers
- Clerk / WorkOS / Supabase Auth : services managés, prêts à l'emploi
- Keycloak / ZITADEL : auth self-hosted entreprise
Si tu codes l'auth toi-même
Pour aller plus loin
- OWASP Authentication Cheat Sheet · OWASP (en anglais) (nouvel onglet)
- OWASP Password Storage Cheat Sheet · OWASP (en anglais) (nouvel onglet)
- Authentification web : les bases · MDN Web Docs (nouvel onglet)
- Authentication and Authorization · Auth0 Docs (en anglais) (nouvel onglet)
Chapitre 2
Hash des mots de passe : bcrypt vs argon2
Un mot de passe ne se stocke jamais en clair. Et pas avec une simple fonction de hash : il faut une fonction lente, salée, conçue pour résister au brute force.
Première règle absolue de l'authentification : un mot de passe ne se stocke jamais en clair. Cela paraît évident, et pourtant les fuites de bases contenant des mots de passe lisibles font encore la une régulièrement. Mais la nuance essentielle va plus loin : il ne suffit pas de « hasher » avec n'importe quelle fonction. Un hash classique comme SHA-256 est conçu pour être rapide — parfait pour vérifier l'intégrité d'un fichier, catastrophique pour un mot de passe, car un attaquant peut en tester des milliards par seconde sur une carte graphique.
Ce qu'il faut, c'est une fonction de hachage lente et salée, conçue spécifiquement pour résister au brute force. Trois algorithmes font référence : argon2id (le choix recommandé par l'OWASP, résistant aux attaques par carte graphique et matériel dédié), bcrypt (éprouvé depuis des décennies, avec un facteur de coût d'au moins 12), et scrypt. À l'inverse, on n'utilise jamais MD5, SHA-1, un SHA-256 nu, ni un chiffrement réversible comme AES — car le chiffrement, contrairement au hachage, peut être annulé par quiconque possède la clé, et la clé finit toujours par fuiter un jour.
Deux points rassurants pour finir. D'abord, le sel : ces bibliothèques génèrent automatiquement un sel aléatoire unique par mot de passe et l'intègrent dans la valeur stockée — tu n'as rien à gérer à la main, contrairement à une croyance répandue. Le sel garantit que deux personnes ayant le même mot de passe obtiennent des hash différents, ce qui neutralise les tables précalculées. Ensuite, la migration : si tu hérites d'une vieille base avec des hash faibles, tu n'as pas besoin de forcer tout le monde à changer de mot de passe. Au prochain login réussi, tu re-hashes avec argon2id de façon transparente. La sécurité s'améliore alors progressivement, sans friction pour les utilisateur·ices.
Les algos recommandés en 2025
- argon2id : recommandé OWASP, résistant aux attaques GPU/ASIC, paramètres modernes
- bcrypt : éprouvé, présent partout, cost factor 12+ en 2025
- scrypt : aussi bon, moins répandu côté JS
Ce qu'on ne fait JAMAIS
argon2id en Node.js
import argon2 from 'argon2';
// Hash à l'inscription
const hash = await argon2.hash(password, {
type: argon2.argon2id,
memoryCost: 19456, // 19 MiB (recommandation OWASP)
timeCost: 2,
parallelism: 1,
});
// Vérification au login
const ok = await argon2.verify(hash, passwordSaisi);bcrypt en Node.js
import bcrypt from 'bcrypt';
// Cost factor 12 = ~250ms par hash (acceptable)
const hash = await bcrypt.hash(password, 12);
const ok = await bcrypt.compare(passwordSaisi, hash);Migration en douceur
Vrai ou faux ?
Chiffrer un mot de passe avec AES, c'est suffisant pour le stocker en sécurité.
Vrai ou faux ?
Avec argon2id ou bcrypt, je n'ai pas besoin d'ajouter un sel moi-même.
Chapitre 3
Sessions vs JWT et le bon cookie
Deux modèles, deux philosophies. Le choix par défaut en 2025 reste les sessions serveur, pas les JWT.
Une fois l'utilisateur·ice authentifié·e, il faut le/la garder connecté·e d'une requête à l'autre. Deux modèles s'affrontent, et ils reflètent deux philosophies opposées. Les sessions serveur stockent l'état côté serveur : le navigateur ne détient qu'un identifiant de session aléatoire, et c'est le serveur qui sait à qui il correspond. Les JWT (JSON Web Tokens) sont à l'inverse stateless : toute l'information est contenue dans un jeton signé que le client présente à chaque requête, et le serveur n'a rien à mémoriser.
Chaque modèle a ses forces. La session serveur offre une révocation instantanée — tu supprimes la session et la personne est déconnectée immédiatement — et reste plus simple et plus sûre par défaut. Le JWT brille dans les architectures distribuées (microservices) où éviter un accès centralisé à chaque requête est un vrai gain, mais il est difficile à révoquer : un jeton reste valide jusqu'à son expiration, sauf à maintenir une liste noire qui ruine son avantage stateless. Pour l'immense majorité des applications web, le choix par défaut recommandé reste donc les sessions serveur ; le JWT ne se justifie que si tu sais précisément pourquoi tu en as besoin.
Quel que soit le modèle, tout repose sur un cookie bien configuré, car c'est lui qui transporte l'identifiant. Trois attributs sont non négociables, illustrés dans le code ci-dessous. HttpOnly rend le cookie inaccessible au JavaScript, ce qui protège contre le vol par XSS. Secure interdit son envoi en clair sur HTTP. SameSite limite les attaques CSRF. Et un piège mortel à éviter absolument : ne stocke jamais un jeton d'authentification dans le localStorage. Une seule faille XSS suffit alors à le voler, et tu ne peux pas le révoquer. Un jeton d'auth vit dans un cookie sécurisé, point.
Sessions serveur
- ID de session aléatoire (32 bytes) en cookie
- État (userId, expiration, données) stocké côté serveur (DB ou Redis)
- Révocation instantanée : tu supprimes la session, l'utilisateur·ice est déconnecté·e
- Plus simple à mettre en place, plus sûr par défaut
JWT (JSON Web Tokens)
- Stateless : tout est dans le token signé
- Pratique en microservices ou architectures distribuées
- Plus dur à révoquer : le token est valide jusqu'à expiration, sauf à maintenir une blacklist côté serveur
- Risque de vol si mal stocké
Le piège mortel : JWT en localStorage
localStorage. Une XSS = vol immédiat du token, et tu ne peux pas le révoquer. JWT (si tu l'utilises) doit être dans un cookie HttpOnly, Secure, SameSite.Le cookie de session sécurisé
import crypto from 'crypto';
// 1. Générer un ID de session aléatoire et imprévisible
const sessionId = crypto.randomBytes(32).toString('base64url');
// 2. Stocker la session côté serveur
await db.session.create({
data: {
id: sessionId,
userId: user.id,
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
},
});
// 3. Poser le cookie avec TOUS les flags
res.cookie('sid', sessionId, {
httpOnly: true, // inaccessible en JS (mitige XSS)
secure: true, // HTTPS uniquement
sameSite: 'lax', // mitige CSRF (lax suffit pour la plupart)
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 jours
});Recommandation par défaut : sessions serveur. JWT uniquement si tu sais pourquoi tu en as besoin (multi-services, mobile sans backend session).
Chapitre 4
OAuth2 et OIDC : Sign in with Google/GitHub
Déléguer l'auth à un provider de confiance, c'est souvent plus sûr que de gérer des mots de passe. Et beaucoup plus simple pour l'utilisateur·ice.
Le bouton « Se connecter avec Google » que tu vois partout repose sur un mécanisme précis, et l'implémenter correctement est souvent plus sûr que de gérer soi-même des mots de passe : tu délègues la partie la plus sensible à un fournisseur qui investit des millions dans sa sécurité, et l'utilisateur·ice n'a pas un énième mot de passe à créer. Encore faut-il comprendre le protocole pour ne pas introduire de faille dans la délégation.
Première clarification, car les deux termes se confondent : OAuth2 gère l'autorisation — permettre à ton application d'agir au nom de l'utilisateur·ice sur un autre service. OIDC (OpenID Connect) est une couche d'identité ajoutée par-dessus, qui te dit qui est la personne (identifiant, email, profil). Pour un simple « Se connecter avec… », c'est donc toujours OIDC que l'on utilise. Le flux recommandé s'appelle Authorization Code + PKCE : c'est le standard actuel, aussi bien pour une application web classique que pour une application monopage ou mobile.
Le principe, détaillé étape par étape ci-dessous : ton application redirige l'utilisateur·ice vers le fournisseur, celui-ci l'authentifie et renvoie un code temporaire, que ton serveur échange contre les jetons d'identité. Deux vérifications sont obligatoires et souvent oubliées par les débutant·es : valider le paramètre state (protection anti-CSRF, il garantit que la réponse correspond bien à la demande que tu as initiée) et le nonce (protection anti-rejeu). Les négliger ouvre la porte à des attaques où un adversaire force ton application à connecter sa propre identité à la place de la victime. En pratique, on ne code jamais ce flux à la main : des bibliothèques certifiées comme openid-client ou arctic l'implémentent correctement.
OAuth2 vs OIDC
- OAuth2 : autorisation. Permet à une app A d'agir au nom de l'utilisateur·ice sur une app B
- OIDC (OpenID Connect) : couche identité par-dessus OAuth2. Te donne qui est l'utilisateur·ice (id, email, profil)
- Pour Sign in with..., on utilise toujours OIDC
Le flow recommandé : Authorization Code + PKCE
- Ton app génère un
code_verifier(aléatoire) et soncode_challenge(hash). Stockestateetnonceen session. - Redirige l'utilisateur·ice vers le provider avec
code_challenge,state,nonce. - L'utilisateur·ice s'authentifie chez le provider et donne son consentement.
- Le provider redirige vers ton callback avec un
code. - Tu vérifies que le
statereçu correspond à celui en session. - Tu échanges le
codecontre unid_tokenetaccess_token, en envoyant lecode_verifier. - Tu valides le
id_token(signature,nonce,iss,aud,exp). - Tu crées la session locale, tu poses ton cookie.
Vérifications obligatoires
state (anti-CSRF) et nonce (anti-replay) au retour du provider. Sans ces vérifications, un attaquant peut forcer ton app à logger sa propre identité chez l'utilisateur·ice cible.Libs recommandées
- openid-client : référence Node.js, certifié OpenID
- arctic : minimaliste, multi-providers (Google, GitHub, Discord, etc.)
- Auth.js : full-featured, intégration Next.js native
Chapitre 5
Magic links et passwordless
Pas de mot de passe : on envoie un lien à usage unique par email. Pratique pour l'utilisateur·ice, à sécuriser correctement côté serveur.
Et si on supprimait purement et simplement le mot de passe ? C'est l'idée du magic link : plutôt que de demander un mot de passe, on envoie par email un lien à usage unique qui connecte directement l'utilisateur·ice. L'avantage pour la personne est évident — rien à mémoriser, rien à saisir — et cela élimine d'un coup toute une classe de risques liés aux mots de passe (réutilisation, mots de passe faibles, fuite). Mais ce confort déplace la responsabilité sur ton serveur, qui doit générer et vérifier ces liens avec le plus grand soin.
Le mécanisme, détaillé ci-dessous, tient en quatre temps : l'utilisateur·ice saisit son email, tu génères un jeton aléatoire dont tu stockes le hash en base (jamais le jeton en clair), tu envoies le jeton dans un lien par email, et au clic tu vérifies, invalides et connectes. La sécurité de tout le système repose sur la qualité de ce jeton, et les exigences sont strictes : il doit être cryptographiquement aléatoire (crypto.randomBytes, jamais Math.random qui est prévisible), faire au moins 32 octets, être stocké haché, être à usage unique, et avoir une durée de vie très courte — dix à quinze minutes maximum.
Deux raisons expliquent ces contraintes. Le stockage haché protège tes utilisateur·ices même si ta base fuite : un attaquant qui la dérobe ne récupère que des empreintes inutilisables, pas des liens de connexion actifs. La durée de vie courte et l'usage unique réduisent la fenêtre pendant laquelle un lien intercepté (dans des logs, un email transféré) reste exploitable. Ajoute impérativement une limitation de débit : sans elle, un attaquant peut générer des liens à l'infini pour inonder une boîte mail ou tenter une fraude. Une limite de trois envois par quart d'heure, par email et par adresse IP, referme cette porte.
Le principe
- L'utilisateur·ice saisit son email
- Tu génères un token aléatoire (32 bytes minimum), tu en stockes le hash en base
- Tu envoies le token (en clair) dans un lien par email
- Au clic, tu vérifies le hash, tu invalides le token, tu connectes
Sécurité du token
- Aléatoire cryptographique :
crypto.randomBytes, pasMath.random - 32 bytes minimum (256 bits d'entropie)
- Stocké hashé en base (un attaquant qui dump la DB ne peut pas se logger)
- Single-use : invalidé dès la première utilisation
- TTL court : 10 à 15 minutes maximum
- Nouveau lien généré : invalide l'ancien
import crypto from 'crypto';
import argon2 from 'argon2';
// Génération
const token = crypto.randomBytes(32).toString('base64url');
const tokenHash = await argon2.hash(token);
await db.magicLink.create({
data: {
tokenHash,
userId,
expiresAt: new Date(Date.now() + 15 * 60 * 1000), // 15 min
},
});
// Envoyer l'email avec le token (en clair) dans l'URL
const url = `https://app.fr/magic?token=${token}`;
await sendEmail({ to: user.email, subject: 'Ton lien de connexion', html: `<a href="${url}">Se connecter</a>` });Rate limit obligatoire
Chapitre 6
WebAuthn et passkeys : la nouvelle référence
Les passkeys remplacent le mot de passe par une clé cryptographique liée à l'appareil (Touch ID, Face ID, YubiKey). Standard W3C, supporté par Apple, Google, Microsoft.
Les passkeys représentent probablement le plus grand changement dans l'authentification depuis vingt ans : elles visent à remplacer purement et simplement le mot de passe. Le principe repose sur la cryptographie asymétrique — une paire de clés — liée à un appareil et déverrouillée par une biométrie (Touch ID, Face ID) ou un code. C'est un standard du W3C, appelé WebAuthn, désormais supporté par Apple, Google et Microsoft, ce qui en fait une technologie d'avenir et non un gadget expérimental.
Ce qui rend les passkeys révolutionnaires, c'est qu'elles corrigent les deux faiblesses fondamentales du mot de passe. D'abord, elles sont résistantes au phishing par conception : la clé est liée au domaine légitime, si bien qu'un site pirate imitant le tien ne peut tout simplement pas l'utiliser — l'attaque la plus courante devient impossible. Ensuite, il n'y a aucun secret partagé côté serveur : tu ne stockes qu'une clé publique, inutile à un attaquant. Un vol de ta base de données ne lui donne rien, là où une base de mots de passe, même hachés, reste une cible. Le tout avec une expérience utilisateur d'une simplicité désarmante : un regard ou une empreinte, et c'est fait.
Le fonctionnement se déroule en deux phases symétriques : à l'enregistrement, l'appareil génère une paire de clés et envoie la clé publique au serveur ; à l'authentification, le serveur envoie un défi que l'appareil signe avec la clé privée, et que le serveur vérifie avec la clé publique. Côté implémentation, une bibliothèque comme @simplewebauthn gère les subtilités du protocole. En pratique, la bonne stratégie d'adoption est progressive : propose d'abord les passkeys en option, puis par défaut, tout en gardant une méthode de secours (magic link, OAuth) pour la récupération en cas de perte d'appareil — c'est exactement la démarche qu'ont adoptée Apple, Google et GitHub.
Pourquoi c'est révolutionnaire
- Résistant au phishing par design : la clé est liée au domaine, impossible à utiliser sur un site pirate
- Pas de secret partagé côté serveur : seule une clé publique est stockée. Un dump de DB ne donne rien à l'attaquant
- UX simple : Touch ID ou Face ID, c'est tout
- Synchronisé entre appareils via iCloud Keychain, Google Password Manager...
Le flux en 2 phases
- Registration : l'utilisateur·ice crée une passkey, le navigateur génère une paire de clés, envoie la clé publique au serveur
- Authentification : le serveur envoie un challenge, l'appareil signe avec la clé privée, le serveur vérifie avec la clé publique
Lib recommandée
// Côté serveur : @simplewebauthn/server
import {
generateRegistrationOptions,
verifyRegistrationResponse,
generateAuthenticationOptions,
verifyAuthenticationResponse,
} from '@simplewebauthn/server';
// Côté client : @simplewebauthn/browser
import { startRegistration, startAuthentication } from '@simplewebauthn/browser';Stratégie de migration
Chapitre 7
MFA : TOTP et recovery codes
Si tu gardes le mot de passe, ajoute un second facteur. Idéalement TOTP (Time-based One-Time Password) ou passkey, jamais SMS.
Tant que tu conserves le mot de passe comme méthode de connexion, tu devrais proposer un second facteur (MFA). L'idée : même si le mot de passe fuite, un attaquant ne peut pas se connecter sans ce deuxième élément. On combine ainsi « ce que tu sais » (le mot de passe) avec « ce que tu as » (ton téléphone, une clé). Toutes les formes de MFA ne se valent pas, et le choix a de vraies conséquences de sécurité.
La bonne option par défaut est le TOTP : un code à six chiffres qui change toutes les trente secondes, généré par une application (Google Authenticator, Authy, 1Password) à partir d'un secret partagé lors de l'activation. C'est un standard ouvert (RFC 6238), gratuit, qui fonctionne hors ligne et n'implique aucun tiers. À l'activation, tu génères un secret que l'utilisateur·ice scanne sous forme de QR code ; ensuite, à chaque connexion, tu vérifies que le code saisi correspond. À l'inverse, évite le MFA par SMS : il est vulnérable au SIM swap, une attaque d'ingénierie sociale où l'adversaire fait transférer le numéro de la victime sur sa propre carte SIM. Réserve le SMS aux comptes peu sensibles, en dernier recours.
Un aspect trop souvent négligé accompagne le MFA : les codes de récupération. Que se passe-t-il si l'utilisateur·ice perd son téléphone et donc son application d'authentification ? Sans plan, il/elle est définitivement enfermé·e dehors. La solution est de générer, au moment de l'activation, une liste de huit à dix codes de secours à usage unique, à afficher une seule fois en demandant de les conserver précieusement. Comme des mots de passe, ces codes se stockent hachés en base, s'invalident après utilisation, et peuvent être régénérés (ce qui annule les anciens). C'est ce filet qui rend le MFA utilisable au quotidien sans transformer chaque perte de téléphone en catastrophe.
TOTP avec une app authenticator
- Standard RFC 6238
- Compatible Google Authenticator, Authy, 1Password, Bitwarden
- Code à 6 chiffres qui change toutes les 30 secondes
- Tu génères un secret au moment de l'activation, encodé en QR code
import { authenticator } from 'otplib';
import QRCode from 'qrcode';
// 1. Génération du secret à l'activation
const secret = authenticator.generateSecret();
await db.user.update({
where: { id: user.id },
data: { totpSecret: secret, totpEnabled: false },
});
// 2. Affichage QR code à scanner
const otpauth = authenticator.keyuri(user.email, 'MonApp', secret);
const qrDataUrl = await QRCode.toDataURL(otpauth);
// 3. Vérification du code à la connexion
const valid = authenticator.verify({ token, secret });SMS, à éviter
Recovery codes
- Génère 8 à 10 codes au moment de l'activation TOTP
- Affiche-les une seule fois, demande à l'utilisateur·ice de les sauvegarder
- Stocke-les hashés en base (comme des mots de passe)
- Single-use : chaque code n'est utilisable qu'une fois
- Permets de regénérer la liste (invalide tous les anciens)
Chapitre 8
Password reset, rate limiting, gestion de session
Le password reset est la porte d'entrée préférée des attaquants. La gestion de session, c'est ce qui sépare une auth correcte d'une auth solide.
Ce dernier chapitre traite trois sujets qui font la différence entre une auth « qui marche » et une auth réellement solide. Le premier, le password reset, est paradoxalement la porte d'entrée préférée des attaquants : à quoi bon protéger le login si l'on peut contourner le mot de passe par la fonction « mot de passe oublié » ? Un reset se conçoit avec les mêmes exigences qu'un magic link — jeton aléatoire de 32 octets, stocké haché, à usage unique, avec une durée de vie de quinze minutes maximum.
Deux précautions supplémentaires sont propres au reset. La protection anti-énumération : que l'email existe ou non dans ta base, tu dois renvoyer exactement la même réponse (« si cet email existe, un lien a été envoyé »). Sinon, un attaquant teste des milliers d'adresses et découvre lesquelles ont un compte — un renseignement précieux pour la suite de son attaque. Et l'invalidation des sessions après un reset réussi : puisqu'un reset est souvent déclenché parce que le compte a été compromis, laisser les sessions actives reviendrait à garder l'attaquant connecté malgré le changement de mot de passe. On supprime donc toutes les sessions existantes.
Les deux autres piliers sont la limitation de débit et la gestion de session. Le rate limiting sur les points sensibles (login, reset) transforme une attaque par force brute de « des milliers d'essais par seconde » en « cinq essais par quart d'heure » — la différence entre une porte grande ouverte et une porte blindée. Côté sessions, retiens qu'un logout doit supprimer la session côté serveur, pas seulement effacer le cookie : sinon un attaquant ayant volé le cookie reste connecté. Propose aussi une déconnexion globale qui coupe toutes les sessions sur tous les appareils. Ces cinq réflexes — hachage argon2id, cookie bien configuré, rate limiting, jeton de reset à usage unique, invalidation des sessions après un changement sensible — couvrent à eux seuls l'essentiel des risques, comme te le fait pratiquer le scénario ci-dessous.
Password reset sans faille
- Token aléatoire 32 bytes minimum, stocké hashé
- Usage unique et TTL court (15 minutes max)
- Réponse identique que l'email existe ou non (anti-énumération)
- Invalider toutes les sessions actives après reset (sécurité en cas de compte compromis)
- Logger la demande de reset (IP, timestamp)
app.post('/forgot-password', async (req, res) => {
const { email } = req.body;
const user = await db.user.findUnique({ where: { email } });
// Réponse identique même si user n'existe pas (anti-énumération)
if (user) {
const token = crypto.randomBytes(32).toString('base64url');
const tokenHash = await argon2.hash(token);
await db.passwordReset.create({
data: {
tokenHash,
userId: user.id,
expiresAt: new Date(Date.now() + 15 * 60 * 1000),
},
});
await sendResetEmail(user.email, token);
}
// Réponse identique dans les deux cas
res.json({ message: 'Si cet email existe, un lien a été envoyé.' });
});Rate limiting sur les endpoints sensibles
import rateLimit from 'express-rate-limit';
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 min
max: 5, // 5 tentatives par IP
standardHeaders: true,
legacyHeaders: false,
message: 'Trop de tentatives, réessaie dans 15 minutes.',
});
app.post('/login', loginLimiter, handler);
app.post('/forgot-password', rateLimit({ windowMs: 60*60_000, max: 3 }), handler);Logout et déconnexion globale
- Logout = supprime la session côté serveur, pas seulement le cookie. Sinon, un attaquant qui a volé le cookie reste connecté
- Déconnexion globale = supprime toutes les sessions de l'utilisateur·ice (sur tous les appareils). À proposer dans les paramètres
- Invalider toutes les sessions après changement de mot de passe ou email
Réflexes à garder
🧭 Tu implémentes le password reset
Un·e utilisateur·ice a oublié son mot de passe. Tu construis le flow '/forgot-password'. Chaque choix compte : suis le chemin sûr.
L'utilisateur·ice saisit son email. Comment réponds-tu si l'email n'existe pas en base ?
🛠️ Exercice optionnel
Implémenter signup + login sécurisés en Express
Tu vas écrire un mini-backend Express avec deux endpoints : /signup et /login. L'objectif : hash argon2id, session serveur, cookie sécurisé, rate limit sur le login.
Ta mission
- /signup : reçoit
email+password. Valide que le mot de passe fait au moins 12 caractères. Hash avec argon2id. Crée l'utilisateur·ice. - /login : reçoit
email+password. Cherche l'utilisateur. Compare avec argon2.verify. Si OK, crée une session, pose le cookie. - Protection anti-énumération : que l'email existe ou non, ta réponse en cas d'échec doit être identique (et le temps de réponse aussi : utilise une vérification factice si l'utilisateur n'existe pas).
- Rate limit : 5 tentatives de login par IP toutes les 15 minutes.
- Cookie sécurisé :
HttpOnly,Secure,SameSite=lax. - Bonus : ajoute
/logout(supprime la session) et/logout-all(supprime toutes les sessions de l'utilisateur).
Tu bloques ? Des indices, à dévoiler quand tu en as besoin.
Indice 1
Indice masqué.
Indice 2
Indice masqué.
Indice 3
Indice masqué.
✅ QCM de fin de cours
Teste tes acquis
10 questions, plusieurs réponses parfois possibles. Coche tout ce qui te semble juste, puis valide pour voir ton score et les explications.
- 1
Quel algorithme utiliser pour hasher un mot de passe en 2025 ?
- 2
Où stocker un JWT côté client ?
- 3
Quels flags un cookie de session doit-il obligatoirement avoir ?
- 4
Quel flow OAuth2 utiliser pour une SPA ou une app mobile ?
- 5
Quelle est la durée de vie maximale d'un token de password reset ?
- 6
Pourquoi renvoyer la même réponse que l'email existe ou non sur /forgot-password ?
- 7
MFA par SMS, bonne idée ?
- 8
Avantage principal des passkeys ?
- 9
Après un password reset réussi, que faire des sessions actives ?
- 10
Quel rate limit raisonnable sur
/login?
Tu peux laisser des questions sans réponse, elles compteront comme fausses.
🎓 Attestation
Ton attestation de réussite
Termine le QCM avec au moins 80% de bonnes réponses pour débloquer ton attestation de réussite.
Tu veux ce cours pour ton équipe ?
Je peux adapter et animer ce cours pour tes formateur·ices ou tes apprenant·es, en présentiel ou en distanciel. Parlons-en pendant l'audit gratuit.
Réserver un audit gratuit →