Authentification avec NestJS
JWT + Passport = authentification robuste et scalable
NestJS s'appuie sur Passport.js pour gérer les stratégies d'authentification. La stratégie JWT est la plus utilisée pour les APIs REST.
npm install @nestjs/passport passport passport-local passport-jwt
npm install @nestjs/jwt
npm install bcrypt
npm install -D @types/passport-local @types/passport-jwt @types/bcrypt
Passport.js â concepts
// auth.module.ts
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { ConfigModule, ConfigService } from '@nestjs/config';
@Module({
imports: [
UsersModule,
PassportModule,
JwtModule.registerAsync({
imports: [ConfigModule],
useFactory: (config: ConfigService) => ({
secret: config.get<string>('JWT_SECRET'),
signOptions: { expiresIn: '15m' },
}),
inject: [ConfigService],
}),
],
providers: [AuthService, LocalStrategy, JwtStrategy],
exports: [AuthService, JwtModule],
})
export class AuthModule {}
LocalStrategy â login email/password
AuthService
@Injectable()
export class AuthService {
constructor(
private usersService: UsersService,
private jwtService: JwtService,
) {}
async validateUser(email: string, password: string): Promise<User | null> {
const user = await this.usersService.findByEmail(email);
if (!user) return null;
const isValid = await bcrypt.compare(password, user.password);
if (!isValid) return null;
return user;
}
async login(user: User) {
const payload = { sub: user.id, email: user.email, role: user.role };
return {
accessToken: this.jwtService.sign(payload),
user: { id: user.id, name: user.name, email: user.email, role: user.role },
};
}
}
LocalStrategy
import { Strategy } from 'passport-local';
import { PassportStrategy } from '@nestjs/passport';
@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(private authService: AuthService) {
super({ usernameField: 'email' }); // Par défaut c'est 'username'
}
async validate(email: string, password: string): Promise<User> {
const user = await this.authService.validateUser(email, password);
if (!user) {
throw new UnauthorizedException('Identifiants invalides');
}
return user; // Stocké dans request.user
}
}
Controller de login
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
@Post('login')
@UseGuards(AuthGuard('local')) // Déclenche LocalStrategy.validate()
async login(@Request() req) {
return this.authService.login(req.user);
}
@Post('register')
register(@Body() dto: CreateUserDto) {
return this.usersService.create(dto);
}
}
JwtStrategy â routes protĂ©gĂ©es
import { ExtractJwt, Strategy } from 'passport-jwt';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private config: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: config.get<string>('JWT_SECRET'),
});
}
async validate(payload: any) {
// payload = décodé du JWT { sub, email, role, iat, exp }
// La valeur retournée est stockée dans request.user
return { id: payload.sub, email: payload.email, role: payload.role };
}
}
Utilisation dans un controller
@Controller('users')
export class UsersController {
// Route protĂ©gĂ©e â nĂ©cessite un JWT valide
@Get('me')
@UseGuards(AuthGuard('jwt'))
getMe(@Request() req) {
return req.user;
}
// Route publique
@Get('count')
count() {
return this.usersService.count();
}
}
Guards personnalisés
Guard JWT réutilisable
// jwt-auth.guard.ts
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
canActivate(context: ExecutionContext) {
return super.canActivate(context);
}
handleRequest(err: any, user: any, info: any) {
if (err || !user) {
throw err || new UnauthorizedException('Token invalide ou expiré');
}
return user;
}
}
// Guard global avec routes publiques
@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);
}
}
RĂŽles & RBAC
RolesGuard
// roles.decorator.ts
export const ROLES_KEY = 'roles';
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);
// roles.guard.ts
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride<string[]>(ROLES_KEY, [
context.getHandler(),
context.getClass(),
]);
if (!requiredRoles) return true; // Pas de restriction de rĂŽle
const { user } = context.switchToHttp().getRequest();
return requiredRoles.includes(user?.role);
}
}
// Controller avec rĂŽles
@Controller('admin')
@UseGuards(JwtAuthGuard, RolesGuard)
export class AdminController {
@Get('users')
@Roles('admin')
getAllUsers() { ... }
@Delete('users/:id')
@Roles('admin', 'super-admin')
deleteUser(@Param('id') id: string) { ... }
}
Décorateurs d'authentification
// Public decorator â marquer une route comme publique
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
// CurrentUser decorator â extraire l'utilisateur de la requĂȘte
export const CurrentUser = createParamDecorator(
(data: keyof User | undefined, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
const user = request.user;
return data ? user?.[data] : user;
},
);
// Usage
@Get('me')
@UseGuards(JwtAuthGuard)
getMe(@CurrentUser() user: User) {
return user;
}
@Get('my-email')
@UseGuards(JwtAuthGuard)
getEmail(@CurrentUser('email') email: string) {
return { email };
}
@Get('public')
@Public()
getPublicData() {
return { message: 'Accessible sans token' };
}
Refresh Tokens
@Injectable()
export class AuthService {
async login(user: User) {
const tokens = await this.generateTokens(user);
await this.saveRefreshToken(user.id, tokens.refreshToken);
return tokens;
}
async generateTokens(user: User) {
const payload = { sub: user.id, email: user.email, role: user.role };
const [accessToken, refreshToken] = await Promise.all([
this.jwtService.signAsync(payload, {
secret: this.config.get('JWT_SECRET'),
expiresIn: '15m',
}),
this.jwtService.signAsync(payload, {
secret: this.config.get('JWT_REFRESH_SECRET'),
expiresIn: '7d',
}),
]);
return { accessToken, refreshToken };
}
async refreshTokens(userId: number, refreshToken: string) {
const user = await this.usersService.findOne(userId);
const storedHash = await this.getStoredRefreshToken(userId);
if (!storedHash) throw new ForbiddenException('AccÚs refusé');
const matches = await bcrypt.compare(refreshToken, storedHash);
if (!matches) throw new ForbiddenException('Token invalide');
return this.generateTokens(user);
}
async saveRefreshToken(userId: number, token: string) {
const hash = await bcrypt.hash(token, 10);
await this.usersService.updateRefreshToken(userId, hash);
}
async logout(userId: number) {
await this.usersService.updateRefreshToken(userId, null);
}
}