Exercices — Module 06

Authentication, Guards & JWT · 5 exercices pratiques

Ex 01 ⭐ Facile

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 }
Ex 02 ⭐ Facile

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) { ... }
}
Ex 03 ⭐⭐ Moyen

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;
  }
}
Ex 04 ⭐⭐ Moyen

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);
}
Ex 05 ⭐⭐⭐ Difficile

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 });
}
← Cours ▶ Mini-projet Module 07 →