Ajouter un deuxième verrou : implémenter la double authentification
L'authentification à deux facteurs est indispensable pour tout produit gérant des données sensibles. Voici comment nous avons implémenté TOTP avec QR codes, codes de secours et récupération de compte — et les décisions de chiffrement qui l'ont rendue prête pour la production.
L'Authentification à Deux Facteurs (2FA) ajoute une couche de sécurité critique en demandant aux utilisateurs de fournir deux formes d'identification : quelque chose qu'ils savent (mot de passe) et quelque chose qu'ils ont (un code basé sur le temps). Ce guide couvre l'implémentation de la 2FA basée sur TOTP avec NestJS, la génération de QR codes, la vérification des tokens et la gestion des codes de sauvegarde — suivant la même approche utilisée par GitHub, Google et AWS.
Prérequis
Avant d'implémenter la 2FA, assurez-vous d'avoir :
- Application NestJS avec l'authentification déjà configurée
- Prisma (ou un autre ORM) pour l'accès à la base de données
- Node.js 18+ pour le support du module crypto
Installez les dépendances requises :
npm install otplib qrcode
npm install -D @types/qrcode
otplib génère et vérifie les codes TOTP. qrcode crée des QR codes scannables pour les applications d'authentification.
Qu'est-ce que la 2FA ?
L'Authentification à Deux Facteurs protège contre les violations de mots de passe en exigeant un deuxième facteur que les attaquants n'ont généralement pas : un code basé sur le temps généré par une application d'authentification sur le téléphone de l'utilisateur.
Types de 2FA
| Méthode | Comment ça Fonctionne | Niveau de Sécurité |
|---|---|---|
| SMS | Code envoyé par SMS | Faible (attaques de SIM swapping) |
| Code envoyé dans la boîte mail | Modéré (risque de compromission email) | |
| TOTP (Basé sur le Temps) | L'application d'authentification génère le code | Fort (hors ligne, non interceptable) |
| Clé Matérielle | Appareil physique USB/NFC (YubiKey) | Le plus fort (résistant au phishing) |
Nous implémenterons TOTP (Time-based One-Time Password) — le standard industriel utilisé par Google Authenticator, Authy et 1Password.
Comment Fonctionne TOTP
PHASE DE CONFIGURATION PHASE DE CONNEXION
┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐
│Serveur │ │Utilisat│ │Serveur │ │Utilisat│
└───┬────┘ └───┬────┘ └───┬────┘ └───┬────┘
│ │ │ │
│ 1. Générer │ │ 1. Mot de passe ✓│
│ secret │ │ │
│ │ │ 2. Demander code ▶│
│ 2. Créer QR ─────▶│ │ │
│ │ │ 3. │ Ouvrir App
│ 3. │ Scanner QR │ │ Code: 123456
│ │ │ 4. Soumettre ◀───│
│ 4. Vérifier ◀────│ │ │
│ Activer 2FA │ │ 5. Vérifier & │
└──────────────────┘ │ Accorder accès▶│
└──────────────────┘
TOTP = HMAC-SHA1(Secret, floor(UnixTime / 30s)) → code 6 chiffres
L'Algorithme TOTP
TOTP est défini dans RFC 6238. Le serveur et l'application d'authentification partagent un secret et utilisent le temps actuel pour générer des codes correspondants :
- Compteur de Temps : Diviser l'horodatage Unix actuel par 30 secondes
- HMAC : Hacher le compteur avec le secret partagé en utilisant HMAC-SHA1
- Tronquer : Extraire un code à 6 chiffres du hash
Parce que les deux côtés utilisent le même secret et le même temps, ils génèrent des codes identiques. La fenêtre de 30 secondes offre une tolérance pour la dérive d'horloge.
Pourquoi TOTP est Sécurisé
| Propriété | Pourquoi c'est Important |
|---|---|
| Génération hors ligne | Pas de réseau nécessaire — codes générés localement sur l'appareil |
| Limité dans le temps | Les codes expirent après 30 secondes, empêchant les attaques de rejeu |
| Secret jamais transmis | Partagé une seule fois lors de la configuration via QR code |
| Cryptographiquement robuste | HMAC-SHA1 empêche la prédiction de code sans le secret |
Implémentation Backend (NestJS)
Dépendances
npm install otplib qrcode
npm install -D @types/qrcode
Schéma de Base de Données
model User {
id String @id @default(cuid())
email String @unique
password String
// Champs 2FA
twoFactorEnabled Boolean @default(false)
twoFactorSecret String? // Secret base32 chiffré
backupCodes String[] // Tableau de codes de sauvegarde chiffrés
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
TwoFactorService
import { Injectable } from '@nestjs/common';
import { authenticator } from 'otplib';
import * as QRCode from 'qrcode';
import { PrismaService } from './prisma.service';
import { randomBytes } from 'crypto';
import { encrypt, decrypt } from './crypto.util';
@Injectable()
export class TwoFactorService {
constructor(private prisma: PrismaService) {
// Configurer les paramètres TOTP
authenticator.options = {
window: 1, // Autoriser 1 pas de temps avant/après (±30 secondes de tolérance)
step: 30, // Pas de temps de 30 secondes
};
}
generateSecret(): string {
return authenticator.generateSecret();
}
async generateQRCode(email: string, secret: string): Promise<string> {
const appName = 'MyApp';
const otpauthUrl = authenticator.keyuri(email, appName, secret);
return QRCode.toDataURL(otpauthUrl);
}
verifyToken(secret: string, token: string): boolean {
try {
return authenticator.verify({ token, secret });
} catch {
return false;
}
}
generateBackupCodes(count: number = 10): string[] {
return Array.from({ length: count }, () =>
randomBytes(4).toString('hex').toUpperCase()
);
}
async enableTwoFactor(userId: string, secret: string) {
const backupCodes = this.generateBackupCodes();
await this.prisma.user.update({
where: { id: userId },
data: {
twoFactorEnabled: true,
twoFactorSecret: encrypt(secret),
backupCodes: backupCodes.map(code => encrypt(code)),
},
});
return backupCodes; // Montrer à l'utilisateur une seule fois
}
async disableTwoFactor(userId: string) {
await this.prisma.user.update({
where: { id: userId },
data: {
twoFactorEnabled: false,
twoFactorSecret: null,
backupCodes: [],
},
});
}
async verifyBackupCode(userId: string, code: string): Promise<boolean> {
const user = await this.prisma.user.findUnique({
where: { id: userId },
});
if (!user?.backupCodes.length) return false;
const decryptedCodes = user.backupCodes.map(c => decrypt(c));
const codeIndex = decryptedCodes.indexOf(code.toUpperCase());
if (codeIndex === -1) return false;
// Supprimer le code de sauvegarde utilisé
const updatedCodes = [...user.backupCodes];
updatedCodes.splice(codeIndex, 1);
await this.prisma.user.update({
where: { id: userId },
data: { backupCodes: updatedCodes },
});
return true;
}
}
TwoFactorController
import { Controller, Post, Get, Body, UseGuards, Req } from '@nestjs/common';
import { TwoFactorService } from './two-factor.service';
import { AuthGuard } from './auth.guard';
import { decrypt } from './crypto.util';
@Controller('2fa')
@UseGuards(AuthGuard)
export class TwoFactorController {
constructor(
private twoFactorService: TwoFactorService,
private prisma: PrismaService
) {}
@Get('setup')
async setupTwoFactor(@Req() req: any) {
const secret = this.twoFactorService.generateSecret();
const qrCode = await this.twoFactorService.generateQRCode(
req.user.email,
secret
);
// Stocker temporairement jusqu'à vérification
req.session.tempTwoFactorSecret = secret;
return { qrCode, secret };
}
@Post('enable')
async enableTwoFactor(@Req() req: any, @Body() body: { token: string }) {
const tempSecret = req.session.tempTwoFactorSecret;
if (!tempSecret) {
return { success: false, error: 'Configuration non initiée' };
}
if (!this.twoFactorService.verifyToken(tempSecret, body.token)) {
return { success: false, error: 'Token invalide' };
}
const backupCodes = await this.twoFactorService.enableTwoFactor(
req.user.id,
tempSecret
);
delete req.session.tempTwoFactorSecret;
return { success: true, backupCodes };
}
@Post('disable')
async disableTwoFactor(@Req() req: any, @Body() body: { token: string }) {
const user = await this.prisma.user.findUnique({
where: { id: req.user.id },
});
if (!user?.twoFactorEnabled) {
return { success: false, error: '2FA non activée' };
}
const secret = decrypt(user.twoFactorSecret);
if (!this.twoFactorService.verifyToken(secret, body.token)) {
return { success: false, error: 'Token invalide' };
}
await this.twoFactorService.disableTwoFactor(req.user.id);
return { success: true };
}
}
Flux d'Authentification avec 2FA
@Post('login')
async login(@Body() body: { email: string; password: string }) {
const user = await this.authService.validateUser(body.email, body.password);
if (!user) {
throw new UnauthorizedException('Identifiants invalides');
}
// Vérifier si la 2FA est activée
if (user.twoFactorEnabled) {
return {
requiresTwoFactor: true,
tempUserId: user.id,
message: 'Entrez votre code 2FA',
};
}
// Pas de 2FA - générer JWT et connecter
const token = this.authService.generateJWT(user);
return { token, user };
}
@Post('verify-2fa')
async verifyTwoFactor(
@Body() body: { tempUserId: string; token: string; isBackupCode?: boolean }
) {
const user = await this.prisma.user.findUnique({
where: { id: body.tempUserId },
});
if (!user || !user.twoFactorEnabled) {
throw new UnauthorizedException('Requête invalide');
}
let isValid = false;
if (body.isBackupCode) {
isValid = await this.twoFactorService.verifyBackupCode(user.id, body.token);
} else {
const secret = this.twoFactorService.decrypt(user.twoFactorSecret);
isValid = this.twoFactorService.verifyToken(secret, body.token);
}
if (!isValid) {
throw new UnauthorizedException('Code 2FA invalide');
}
const token = this.authService.generateJWT(user);
return { token, user };
}
Implémentation Frontend (React)
Composant de Configuration 2FA
import { useState } from 'react';
export function TwoFactorSetup() {
const [qrCode, setQrCode] = useState<string | null>(null);
const [secret, setSecret] = useState<string | null>(null);
const [verificationCode, setVerificationCode] = useState('');
const [backupCodes, setBackupCodes] = useState<string[]>([]);
const [step, setStep] = useState<'init' | 'verify' | 'complete'>('init');
const initSetup = async () => {
const res = await fetch('/api/2fa/setup', {
headers: { Authorization: `Bearer ${token}` },
});
const data = await res.json();
setQrCode(data.qrCode);
setSecret(data.secret);
setStep('verify');
};
const verifyAndEnable = async () => {
const res = await fetch('/api/2fa/enable', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
body: JSON.stringify({ token: verificationCode }),
});
const data = await res.json();
if (data.success) {
setBackupCodes(data.backupCodes);
setStep('complete');
} else {
alert(data.error);
}
};
if (step === 'init') {
return (
<div className="max-w-md mx-auto p-6">
<h2 className="text-2xl font-bold mb-4">Activer la 2FA</h2>
<p className="mb-4">Ajoutez une couche de sécurité supplémentaire à votre compte.</p>
<button onClick={initSetup} className="btn-primary">Commencer</button>
</div>
);
}
if (step === 'verify') {
return (
<div className="max-w-md mx-auto p-6">
<h2 className="text-2xl font-bold mb-4">Scanner le QR Code</h2>
<img src={qrCode!} alt="QR Code" className="mx-auto mb-4" />
<p className="text-sm text-gray-600 mb-2">Saisie manuelle : <code>{secret}</code></p>
<input
type="text"
value={verificationCode}
onChange={(e) => setVerificationCode(e.target.value)}
placeholder="Entrez le code à 6 chiffres"
className="input w-full mb-4"
maxLength={6}
/>
<button onClick={verifyAndEnable} className="btn-primary w-full">
Vérifier et Activer
</button>
</div>
);
}
return (
<div className="max-w-md mx-auto p-6">
<h2 className="text-2xl font-bold mb-4">Codes de Sauvegarde</h2>
<p className="text-sm text-yellow-800 mb-4 p-3 bg-yellow-50 rounded">
Sauvegardez ces codes en lieu sûr. Chacun peut être utilisé une fois si vous perdez votre appareil.
</p>
<div className="grid grid-cols-2 gap-2 mb-4">
{backupCodes.map((code, i) => (
<code key={i} className="p-2 bg-gray-100 rounded text-center">{code}</code>
))}
</div>
<button
onClick={() => navigator.clipboard.writeText(backupCodes.join('\n'))}
className="btn-secondary w-full"
>
Copier Tous les Codes
</button>
</div>
);
}
Conclusion
L'Authentification à Deux Facteurs est essentielle pour protéger les comptes utilisateurs. La 2FA basée sur TOTP offre une sécurité robuste sans dépendre des SMS ou des emails, la rendant résistante aux attaques de SIM swapping et d'interception.
Liste de Vérification d'Implémentation
- Générer des secrets : Utiliser
otplibpour créer des secrets encodés en base32 - Configuration du QR code : Générer des codes scannables avec le format URI
otpauth:// - Vérifier avant d'activer : Demander aux utilisateurs d'entrer un code valide avant d'activer la 2FA
- Codes de sauvegarde : Générer des codes de récupération à usage unique lors de la configuration
Avec la 2FA activée, même si un attaquant obtient le mot de passe d'un utilisateur, il ne peut pas accéder au compte sans le code basé sur le temps de l'application d'authentification de l'utilisateur.
Explorer le Code
Consultez le dépôt ft_transcendence sur GitHub pour voir ces patterns dans un vrai jeu multijoueur.
Écrit par

Tech Lead et Ingénieur Full Stack pilotant une équipe de 5 ingénieurs chez Fygurs (Paris, Remote) sur un SaaS cloud-native Azure. Diplômé de 1337 Coding School (42 Network / UM6P). Écrit sur l'architecture, l'infrastructure cloud et le leadership technique.