Exercices â Module 06
Authentication, Guards & JWT · 5 exercices pratiques
Guard JWT avec routes publiques
CrĂ©ez un JwtAuthGuard global qui protĂšge toutes les routes par dĂ©faut. Les routes annotĂ©es avec @Public() doivent ĂȘtre accessibles sans token.
Voir la solution
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
constructor(private reflector: Reflector) { super(); }
canActivate(context: ExecutionContext) {
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);
if (isPublic) return true;
return super.canActivate(context);
}
handleRequest(err: any, user: any) {
if (err || !user) throw err || new UnauthorizedException();
return user;
}
}
// Enregistrement global
app.useGlobalGuards(new JwtAuthGuard(reflector));
// ou via providers :
{ provide: APP_GUARD, useClass: JwtAuthGuard }
RBAC avec RolesGuard
Implémentez un systÚme RBAC complet avec les rÎles USER, MODERATOR, ADMIN. Créez le décorateur @Roles() et le RolesGuard. Testez avec un controller admin.
Voir la solution
export enum Role { USER = 'user', MODERATOR = 'moderator', ADMIN = 'admin' }
export const Roles = (...roles: Role[]) => SetMetadata('roles', roles);
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const roles = this.reflector.getAllAndOverride<Role[]>('roles', [
context.getHandler(), context.getClass()
]);
if (!roles?.length) return true;
const { user } = context.switchToHttp().getRequest();
// Hiérarchie : admin > moderator > user
const hierarchy = { admin: 3, moderator: 2, user: 1 };
const userLevel = hierarchy[user?.role] || 0;
return roles.some(r => hierarchy[r] <= userLevel);
}
}
@Controller('admin')
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(Role.ADMIN)
export class AdminController {
@Get('users') getAllUsers() { ... }
@Delete('users/:id')
@Roles(Role.ADMIN) // Redondant ici, juste pour illustrer
deleteUser(@Param('id') id: string) { ... }
}
AuthController complet
Créez un AuthController avec les routes : POST /auth/register, POST /auth/login, POST /auth/refresh, POST /auth/logout, GET /auth/me (protégée).
Voir la solution
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
@Post('register')
@Public()
@HttpCode(HttpStatus.CREATED)
register(@Body() dto: RegisterDto) {
return this.authService.register(dto);
}
@Post('login')
@Public()
@UseGuards(LocalAuthGuard)
login(@CurrentUser() user: User) {
return this.authService.login(user);
}
@Post('refresh')
@Public()
@UseGuards(RefreshTokenGuard)
refresh(@CurrentUser() user: any) {
return this.authService.refreshTokens(user.id, user.refreshToken);
}
@Post('logout')
@HttpCode(HttpStatus.NO_CONTENT)
logout(@CurrentUser('id') userId: number) {
return this.authService.logout(userId);
}
@Get('me')
getMe(@CurrentUser() user: User) {
return user;
}
}
Guard de ressource (ownership)
Créez un guard ResourceOwnerGuard qui vérifie que l'utilisateur connecté est bien le propriétaire de la ressource (ex: un post). Les admins peuvent bypasser cette vérification.
Voir la solution
@Injectable()
export class PostOwnerGuard implements CanActivate {
constructor(private postsService: PostsService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const user = request.user;
const postId = +request.params.id;
// Les admins ont tous les droits
if (user.role === 'admin') return true;
const post = await this.postsService.findOne(postId);
return post.authorId === user.id;
}
}
// Usage
@Put(':id')
@UseGuards(JwtAuthGuard, PostOwnerGuard)
update(@Param('id', ParseIntPipe) id: number, @Body() dto: UpdatePostDto) {
return this.postsService.update(id, dto);
}
Rotation des refresh tokens
Implémentez la rotation des refresh tokens : à chaque refresh, l'ancien token est invalidé et un nouveau couple access/refresh est émis. Stockez le hash du refresh token en base de données.
Voir la solution
// user.entity.ts â ajouter la colonne
@Column({ nullable: true, select: false })
refreshTokenHash: string;
// auth.service.ts
async refreshTokens(userId: number, oldRefreshToken: string) {
const user = await this.usersService.findOneWithRefreshToken(userId);
if (!user?.refreshTokenHash) {
throw new ForbiddenException('Access denied');
}
// Vérifier que l'ancien token correspond
const matches = await bcrypt.compare(oldRefreshToken, user.refreshTokenHash);
if (!matches) {
// Possible rĂ©utilisation de token â rĂ©voquer tous les tokens
await this.usersService.revokeRefreshToken(userId);
throw new ForbiddenException('Token reuse detected â all sessions invalidated');
}
// Générer et stocker les nouveaux tokens
const tokens = await this.generateTokens(user);
await this.usersService.updateRefreshToken(userId, tokens.refreshToken);
return tokens;
}
async updateRefreshToken(userId: number, token: string | null) {
const hash = token ? await bcrypt.hash(token, 10) : null;
await this.usersRepo.update(userId, { refreshTokenHash: hash });
}