Cours NestJS · Module 4: Authentification
PROJET : Système d'authentification pour la légion navale romaine
Dans cette leçon4
Amiral de la sécurité ! Le consul Caesar.js te confie le plus grand défi - créer un système d'authentification complet pour toute la flotte de navires de guerre romains. Ce sera un véritable Fort Knox des mers, combinant toutes les compétences que tu as acquises dans les modules précédents !
Spécification du projet
Exigences fonctionnelles
Ton système doit prendre en charge :
- Inscription et connexion
- Email/mot de passe avec validation de la robustesse du mot de passe
- Vérification de l'email
- Connexion sociale (Google, GitHub, Facebook)
- Authentification à deux facteurs (2FA)
- Gestion des sessions
- JWT Access Tokens (15 min)
- Refresh Tokens (7 jours) avec rotation
- Prise en charge multi-appareils
- Gestion des sessions
- Sécurité
- Limitation de débit (rate limiting)
- Hachage des mots de passe (bcrypt)
- Verrouillage du compte après des tentatives échouées
- Détection d'activité suspecte
- Administration
- Contrôle d'accès basé sur les rôles
- Gestion des utilisateurs
- Journalisation d'audit
- Surveillance de la sécurité
Implémentation
1. Contrôleur d'authentification principal
1// auth.controller.ts
2@Controller('auth')
3@ApiTags('Authentication')
4export class AuthController {
5 constructor(
6 private authService: AuthService,
7 private passwordService: PasswordService,
8 private refreshTokenService: RefreshTokenService,
9 private twoFactorService: TwoFactorService,
10 private securityService: SecurityService,
11 ) {}
12
13 @Post('register')
14 @Throttle(3, 300) // 3 tentatives par 5 minutes
15 @ApiOperation({ summary: 'Enregistrer un nouveau légionnaire' })
16 async register(
17 @Body() registerDto: RegisterDto,
18 @Headers('user-agent') userAgent: string,
19 @Ip() ipAddress: string,
20 ) {
21 // Vérifier si l'IP est sur liste noire
22 await this.securityService.checkIpBlacklist(ipAddress);
23
24 // Inscription
25 const user = await this.authService.register(registerDto);
26
27 // Journalisation de sécurité
28 await this.securityService.logSecurityEvent({
29 type: 'user_registered',
30 userId: user.id,
31 ipAddress,
32 userAgent,
33 details: { email: user.email },
34 });
35
36 return {
37 message: 'Le légionnaire a été enregistré ! Consulte ton email pour activer ton compte.',
38 user: {
39 id: user.id,
40 email: user.email,
41 emailVerified: user.emailVerified,
42 },
43 };
44 }
45
46 @Post('login')
47 @Throttle(5, 300) // 5 tentatives par 5 minutes
48 @UseGuards(LocalAuthGuard)
49 @ApiOperation({ summary: 'Connexion du légionnaire' })
50 async login(
51 @Request() req,
52 @Body() loginDto: LoginDto,
53 @Headers('user-agent') userAgent: string,
54 @Ip() ipAddress: string,
55 ) {
56 const user = req.user;
57
58 // Vérifier si le compte est verrouillé
59 await this.authService.checkAccountLockout(user.id);
60
61 // Vérifier si le 2FA est requis
62 if (user.twoFactorEnabled) {
63 if (!loginDto.twoFactorCode) {
64 return {
65 requiresTwoFactor: true,
66 tempToken: await this.twoFactorService.generateTempToken(user.id),
67 };
68 }
69
70 const isValid2FA = await this.twoFactorService.verifyCode(
71 user.id,
72 loginDto.twoFactorCode,
73 );
74
75 if (!isValid2FA) {
76 await this.securityService.logSecurityEvent({
77 type: 'invalid_2fa_attempt',
78 userId: user.id,
79 ipAddress,
80 userAgent,
81 });
82
83 throw new UnauthorizedException('Code 2FA invalide !');
84 }
85 }
86
87 // Générer les tokens
88 const tokens = await this.authService.generateAuthTokens(
89 user,
90 userAgent,
91 ipAddress,
92 );
93
94 // Mettre à jour les données de connexion
95 await this.authService.updateLoginInfo(user.id, ipAddress);
96
97 return {
98 message: 'Bienvenue dans le système, légionnaire !',
99 user: {
100 id: user.id,
101 email: user.email,
102 firstName: user.firstName,
103 lastName: user.lastName,
104 role: user.role,
105 emailVerified: user.emailVerified,
106 twoFactorEnabled: user.twoFactorEnabled,
107 },
108 tokens,
109 };
110 }
111
112 @Post('logout')
113 @UseGuards(JwtAuthGuard)
114 async logout(
115 @Request() req,
116 @Body() logoutDto: LogoutDto,
117 @Ip() ipAddress: string,
118 ) {
119 if (logoutDto.allDevices) {
120 await this.refreshTokenService.revokeAllUserTokens(
121 req.user.id,
122 'user_logout_all',
123 );
124 } else if (logoutDto.refreshToken) {
125 await this.refreshTokenService.revokeTokenByValue(
126 logoutDto.refreshToken,
127 'user_logout',
128 );
129 }
130
131 return {
132 message: logoutDto.allDevices
133 ? 'Déconnecté de tous les appareils'
134 : 'Déconnecté avec succès',
135 };
136 }
137
138 @Post('forgot-password')
139 @Throttle(3, 3600) // 3 tentatives par heure
140 async forgotPassword(
141 @Body() forgotDto: ForgotPasswordDto,
142 @Ip() ipAddress: string,
143 ) {
144 await this.passwordService.sendPasswordResetEmail(
145 forgotDto.email,
146 ipAddress,
147 );
148
149 return {
150 message: 'Si le compte existe, des instructions ont été envoyées par email.',
151 };
152 }
153}2. Service d'authentification principal
1// auth.service.ts
2@Injectable()
3export class AuthService {
4 constructor(
5 @InjectRepository(User) private usersRepository: Repository<User>,
6 private passwordService: PasswordService,
7 private refreshTokenService: RefreshTokenService,
8 private emailService: EmailService,
9 private jwtService: JwtService,
10 ) {}
11
12 async register(registerDto: RegisterDto): Promise<User> {
13 // Vérifier si l'utilisateur existe déjà
14 const existingUser = await this.usersRepository.findOne({
15 where: { email: registerDto.email },
16 });
17
18 if (existingUser) {
19 throw new ConflictException('Un légionnaire avec cet email existe déjà dans la légion !');
20 }
21
22 // Hacher le mot de passe
23 const hashedPassword = await this.passwordService.hashPassword(
24 registerDto.password,
25 );
26
27 // Créer l'utilisateur
28 const user = this.usersRepository.create({
29 email: registerDto.email,
30 firstName: registerDto.firstName,
31 lastName: registerDto.lastName,
32 password: hashedPassword,
33 role: 'legionary', // Rôle par défaut
34 emailVerified: false,
35 createdAt: new Date(),
36 });
37
38 const savedUser = await this.usersRepository.save(user);
39
40 // Envoyer l'email de vérification
41 await this.sendEmailVerification(savedUser);
42
43 return savedUser;
44 }
45
46 async validateUser(email: string, password: string): Promise<User | null> {
47 // Trouver l'utilisateur
48 const user = await this.usersRepository.findOne({
49 where: { email },
50 select: ['id', 'email', 'password', 'firstName', 'lastName', 'role',
51 'emailVerified', 'twoFactorEnabled', 'accountLocked', 'lockoutUntil'],
52 });
53
54 if (!user) {
55 return null;
56 }
57
58 // Vérifier le verrouillage du compte
59 if (user.accountLocked && user.lockoutUntil && user.lockoutUntil > new Date()) {
60 throw new UnauthorizedException(
61 `Compte verrouillé jusqu'à ${user.lockoutUntil.toLocaleString()}`
62 );
63 }
64
65 // Vérifier le mot de passe
66 const isPasswordValid = await this.passwordService.verifyPassword(
67 password,
68 user.password,
69 );
70
71 if (!isPasswordValid) {
72 await this.handleFailedLogin(user.id);
73 return null;
74 }
75
76 // Réinitialiser le compteur de tentatives échouées
77 await this.resetFailedLoginAttempts(user.id);
78
79 // Retirer le mot de passe de l'objet
80 delete user.password;
81 return user;
82 }
83
84 async generateAuthTokens(
85 user: User,
86 userAgent: string,
87 ipAddress: string,
88 ): Promise<{ accessToken: string; refreshToken: string }> {
89 // Access Token
90 const accessTokenPayload = {
91 sub: user.id,
92 email: user.email,
93 role: user.role,
94 emailVerified: user.emailVerified,
95 };
96
97 const accessToken = this.jwtService.sign(accessTokenPayload, {
98 expiresIn: '15m',
99 });
100
101 // Refresh Token
102 const refreshToken = await this.refreshTokenService.generateRefreshToken(
103 user.id,
104 userAgent,
105 ipAddress,
106 );
107
108 return { accessToken, refreshToken };
109 }
110
111 private async handleFailedLogin(userId: number): Promise<void> {
112 const maxAttempts = 5;
113 const lockoutDuration = 30; // minutes
114
115 // Récupérer le nombre de tentatives échouées des 15 dernières minutes
116 const since = new Date(Date.now() - 15 * 60 * 1000);
117 const failedAttempts = await this.getFailedAttemptsCount(userId, since);
118
119 if (failedAttempts >= maxAttempts) {
120 const lockoutUntil = new Date(Date.now() + lockoutDuration * 60 * 1000);
121
122 await this.usersRepository.update(userId, {
123 accountLocked: true,
124 lockoutUntil,
125 failedLoginAttempts: failedAttempts,
126 });
127
128 console.warn(`Le compte ${userId} a été verrouillé après ${failedAttempts} tentatives échouées`);
129 }
130 }
131
132 private async resetFailedLoginAttempts(userId: number): Promise<void> {
133 await this.usersRepository.update(userId, {
134 accountLocked: false,
135 lockoutUntil: null,
136 failedLoginAttempts: 0,
137 });
138 }
139
140 async updateLoginInfo(userId: number, ipAddress: string): Promise<void> {
141 await this.usersRepository.update(userId, {
142 lastLoginAt: new Date(),
143 lastLoginIp: ipAddress,
144 });
145 }
146}Tâches à réaliser
Partie 1 : Implémentation de base (4-6 heures)
- Créer toutes les entités avec leurs relations
- Implémenter l'inscription avec validation de l'email
- Ajouter la connexion avec JWT
- Implémenter les refresh tokens
- Ajouter la vérification de l'email
Partie 2 : Sécurité (3-4 heures)
- Ajouter la limitation de débit
- Implémenter le verrouillage de compte
- Ajouter la réinitialisation du mot de passe
- Implémenter la surveillance de la sécurité
- Ajouter la journalisation d'audit
Partie 3 : Authentification à deux facteurs (2-3 heures)
- Implémenter le TOTP (Google Authenticator)
- Ajouter des codes de secours
- Créer des QR codes pour la configuration
- Ajouter la gestion du 2FA dans le panneau utilisateur
Partie 4 : Connexion sociale (2-3 heures)
- Intégrer Google OAuth
- Ajouter GitHub OAuth
- Implémenter la liaison de comptes sociaux
- Ajouter la gestion des comptes connectés
Partie 5 : Administration et surveillance (2-3 heures)
- Créer un panneau d'administration
- Ajouter la gestion des utilisateurs
- Implémenter des rapports de sécurité
- Ajouter la surveillance de l'activité en temps réel
Partie 6 : Tests (2-3 heures)
- Écrire des tests unitaires pour tous les services
- Ajouter des tests d'intégration
- Créer des tests e2e pour les flux principaux
- Ajouter des tests de performance
Critères d'évaluation
Fonctionnalité - Toutes les exigences implémentées correctement Sécurité - Meilleures pratiques de sécurité appliquées Code - Code propre et bien organisé suivant les bonnes pratiques Tests - Couverture de tests élevée, différents types de tests Documentation - Documentation complète de l'API et instructions d'installation Performance - Optimisation des requêtes et des performances Surveillance - Journalisation et surveillance de la sécurité
C'est le plus grand défi de tout le cours NestJS ! Bonne chance, amiral de la sécurité. Ta flotte compte sur un système de protection impénétrable !
Code de cette leçon : src/auth/complete-auth-system.ts
1// PROJET : système d'authentification complet
2// Combine tous les éléments en un système fonctionnel
3import { Module, Controller, Post, Get, Body, UseGuards, Request } from '@nestjs/common';
4import { Injectable, UnauthorizedException } from '@nestjs/common';
5import { JwtModule, JwtService } from '@nestjs/jwt';
6import { PassportModule } from '@nestjs/passport';
7
8// ============================================
9// 1. AuthService - le cœur du système
10// ============================================
11@Injectable()
12class AuthService {
13 private users: any[] = [];
14
15 constructor(private jwtService: JwtService) {}
16
17 // TODO: Implémente l'inscription
18 async register(username: string, password: string) {
19 // TODO: Vérifie si l'utilisateur existe déjà
20 // TODO: Hache le mot de passe (bcrypt)
21 // TODO: Enregistre l'utilisateur
22 // TODO: Renvoie une confirmation
23 return { message: 'TODO: implémentation' };
24 }
25
26 // TODO: Implémente la connexion
27 async login(username: string, password: string) {
28 // TODO: Trouve l'utilisateur
29 // TODO: Vérifie le mot de passe
30 // TODO: Génère un token JWT
31 // TODO: Renvoie { accessToken, user }
32 return { accessToken: '', user: {} };
33 }
34
35 // TODO: Implémente la validation du token
36 async validateToken(payload: any) {
37 // TODO: Trouve l'utilisateur via payload.sub
38 // TODO: Renvoie l'utilisateur ou null
39 return null;
40 }
41}
42
43// ============================================
44// 2. AuthController - les endpoints
45// ============================================
46@Controller('auth')
47class AuthController {
48 constructor(private authService: AuthService) {}
49
50 @Post('register')
51 async register(@Body() body: { username: string; password: string }) {
52 return this.authService.register(body.username, body.password);
53 }
54
55 @Post('login')
56 async login(@Body() body: { username: string; password: string }) {
57 return this.authService.login(body.username, body.password);
58 }
59
60 // TODO: Ajoute @UseGuards(JwtAuthGuard)
61 @Get('profile')
62 getProfile(@Request() req: any) {
63 return { user: req.user };
64 }
65}
66
67// ============================================
68// 3. AuthModule - configuration
69// ============================================
70@Module({
71 imports: [
72 PassportModule.register({ defaultStrategy: 'jwt' }),
73 JwtModule.register({
74 secret: 'spqr-secret-imperium',
75 signOptions: { expiresIn: '24h' },
76 }),
77 ],
78 providers: [AuthService],
79 controllers: [AuthController],
80 exports: [AuthService],
81})
82class AuthModule {}
83
84console.log("=== Système d'authentification complet ===");
85console.log('1. Inscription : POST /auth/register');
86console.log('2. Connexion : POST /auth/login -> token JWT');
87console.log('3. Profil : GET /auth/profile (protégé)');
88console.log('On combine : AuthService + JwtStrategy + Guards');
89Tu as repéré une erreur dans cette leçon ?