Authentification
L’authentification est une partie essentielle de la plupart des applications. Il existe de nombreuses approches et stratégies pour gérer l’authentification. L’approche choisie pour un projet dépend de ses exigences spécifiques. Ce chapitre présente plusieurs approches d’authentification qui peuvent être adaptées à diverses exigences.
Développons nos besoins. Pour ce cas d’utilisation, les clients commenceront par s’authentifier avec un nom d’utilisateur et un mot de passe. Une fois authentifié, le serveur émettra un JWT qui peut être envoyé comme un bearer token dans un en-tête d’autorisation sur les requêtes suivantes pour prouver l’authentification. Nous créerons également une route protégée qui est accessible uniquement aux requêtes contenant un JWT valide.
Création d’un module d’authentification
Nous commencerons par générer un AuthModule
et à l’intérieur, un AuthService
et un AuthController
. Nous utiliserons le AuthService
pour implémenter la logique d’authentification, et le AuthController
pour exposer les points de terminaison d’authentification.
$ nest g module auth$ nest g controller auth$ nest g service auth
En implémentant le AuthService
, nous trouverons utile d’encapsuler les opérations utilisateur dans un UsersService
, alors générons maintenant ce module et ce service :
$ nest g module users$ nest g service users
Remplacez le contenu par défaut de ces fichiers générés comme montré ci-dessous. Pour notre application d’exemple, le UsersService
maintient simplement une liste d’utilisateurs en mémoire, et une méthode de recherche pour en récupérer un par nom d’utilisateur. Dans une véritable application, c’est ici que vous construiriez votre modèle utilisateur et la couche de persistance, en utilisant la bibliothèque de votre choix (ex. : TypeORM, Sequelize, Mongoose, etc.).
users/users.service.ts
import { Injectable } from '@nestjs/common';
// Ceci devrait être une vraie classe/interface représentant une entité utilisateurexport type User = any;
@Injectable()export class UsersService { private readonly users = [ { userId: 1, username: 'john', password: 'changeme', }, { userId: 2, username: 'maria', password: 'guess', }, ];
async findOne(username: string): Promise<User | undefined> { return this.users.find(user => user.username === username); }}
users/users.module.ts
import { Module } from '@nestjs/common';import { UsersService } from './users.service';
@Module({ providers: [UsersService], exports: [UsersService],})export class UsersModule {}
Mise en œuvre du point de terminaison “Connexion”
Notre AuthService
a pour tâche de récupérer un utilisateur et de vérifier le mot de passe. Nous créons une méthode signIn()
à cet effet. Dans le code ci-dessous, nous utilisons un opérateur de décomposition pratique d’ES6 pour supprimer la propriété mot de passe de l’objet utilisateur avant de le renvoyer. C’est une pratique courante lors du retour d’objets utilisateur, car vous ne voulez pas exposer des champs sensibles comme les mots de passe ou d’autres clés de sécurité.
auth/auth.service.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';import { UsersService } from '../users/users.service';
@Injectable()export class AuthService { constructor(private usersService: UsersService) {}
async signIn(username: string, pass: string): Promise<any> { const user = await this.usersService.findOne(username); if (user?.password !== pass) { throw new UnauthorizedException(); } const { password, ...result } = user; // TODO: Générer un JWT et le retourner ici // au lieu de l'objet utilisateur return result; }}
Maintenant, nous mettons à jour notre AuthModule
pour importer le UsersModule
.
auth/auth.module.ts
import { Module } from '@nestjs/common';import { AuthService } from './auth.service';import { AuthController } from './auth.controller';import { UsersModule } from '../users/users.module';
@Module({ imports: [UsersModule], providers: [AuthService], controllers: [AuthController],})export class AuthModule {}
Avec cela en place, ouvrons le AuthController
et ajoutons une méthode signIn()
à celui-ci. Cette méthode sera appelée par le client pour authentifier un utilisateur. Elle recevra le nom d’utilisateur et le mot de passe dans le corps de la requête, et renverra un jeton JWT si l’utilisateur est authentifié.
auth/auth.controller.ts
import { Body, Controller, HttpCode, HttpStatus, Post } from '@nestjs/common';import { AuthService } from './auth.service';
@Controller('auth')export class AuthController { constructor(private authService: AuthService) {}
@HttpCode(HttpStatus.OK) @Post('login') async signIn(@Body() signInDto: Record<string, any>) { return this.authService.signIn(signInDto.username, signInDto.password); }}
Jeton JWT
Nous sommes prêts à passer à la partie JWT de notre système d’authentification. Révisons et affinons nos exigences :
- Permettre aux utilisateurs de s’authentifier avec un nom d’utilisateur/mot de passe, en renvoyant un JWT à utiliser dans des appels suivants aux API protégées. Nous sommes bien partis pour répondre à cette exigence. Pour la compléter, nous devrons écrire le code qui émet un JWT.
- Créer des routes API qui sont protégées en fonction de la présence d’un JWT valide en tant que bearer token.
Nous devrons installer un package supplémentaire pour prendre en charge nos exigences JWT :
$ npm install --save @nestjs/jwt
Pour garder nos services proprement modularisés, nous gérerons la génération du JWT dans le authService
. Ouvrez le fichier auth.service.ts
dans le dossier auth
, injectez le JwtService
, et mettez à jour la méthode signIn
pour générer un jeton JWT comme montré ci-dessous :
auth/auth.service.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';import { UsersService } from '../users/users.service';import { JwtService } from '@nestjs/jwt';
@Injectable()export class AuthService { constructor(private usersService: UsersService, private jwtService: JwtService) {}
async signIn(username: string, pass: string): Promise<{ access_token: string }> { const user = await this.usersService.findOne(username); if (user?.password !== pass) { throw new UnauthorizedException(); } const payload = { sub: user.userId, username: user.username }; return { access_token: await this.jwtService.signAsync(payload), }; }}
Nous utilisons la bibliothèque @nestjs/jwt
, qui fournit une fonction signAsync()
pour générer notre JWT à partir d’un sous-ensemble des propriétés de l’objet user
, que nous retournons ensuite sous forme d’objet simple avec une seule propriété access_token
.
Nous devons maintenant mettre à jour le AuthModule
pour importer les nouvelles dépendances et configurer le JwtModule
.
Tout d’abord, créez constants.ts
dans le dossier auth
, et ajoutez le code suivant :
auth/constants.ts
export const jwtConstants = { secret: 'NE PAS UTILISER CETTE VALEUR. À LA PLACE, CRÉEZ UNE CLÉ COMPLÈXE ET GARDEZ-LA EN SÉCURITÉ EN DEHORS DU CODE SOURCE.',};
Nous utiliserons ceci pour partager notre clé entre les étapes de signature et de vérification JWT.
Maintenant, ouvrez auth.module.ts
dans le dossier auth
et mettez-le à jour pour qu’il ressemble à ceci :
auth/auth.module.ts
import { Module } from '@nestjs/common';import { AuthService } from './auth.service';import { UsersModule } from '../users/users.module';import { JwtModule } from '@nestjs/jwt';import { AuthController } from './auth.controller';import { jwtConstants } from './constants';
@Module({ imports: [ UsersModule, JwtModule.register({ global: true, secret: jwtConstants.secret, signOptions: { expiresIn: '60s' }, }), ], providers: [AuthService], controllers: [AuthController], exports: [AuthService],})export class AuthModule {}
Nous configurons le JwtModule
à l’aide de register()
, en transmettant un objet de configuration. Voir ici pour plus d’informations sur le module JwtModule
de Nest et ici pour plus de détails sur les options de configuration disponibles.
Allons-y et testons nos routes à nouveau en utilisant cURL. Vous pouvez tester avec n’importe lequel des objets user
codés en dur dans le UsersService
.
$ # POST to /auth/login$ curl -X POST http://localhost:3000/auth/login -d '{"username": "john", "password": "changeme"}' -H "Content-Type: application/json"{ "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." }$ # Note : JWT ci-dessus tronqué
Mise en œuvre du garde d’authentification
Nous pouvons maintenant répondre à notre exigence finale : protéger les points de terminaison en exigeant qu’un JWT valide soit présent dans la requête. Nous ferons cela en créant un AuthGuard
que nous pourrons utiliser pour protéger nos routes.
auth/auth.guard.ts
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';import { JwtService } from '@nestjs/jwt';import { jwtConstants } from './constants';import { Request } from 'express';
@Injectable()export class AuthGuard implements CanActivate { constructor(private jwtService: JwtService) {}
async canActivate(context: ExecutionContext): Promise<boolean> { const request = context.switchToHttp().getRequest(); const token = this.extractTokenFromHeader(request); if (!token) { throw new UnauthorizedException(); } try { const payload = await this.jwtService.verifyAsync(token, { secret: jwtConstants.secret, }); request['user'] = payload; } catch { throw new UnauthorizedException(); } return true; }
private extractTokenFromHeader(request: Request): string | undefined { const [type, token] = request.headers.authorization?.split(' ') ?? []; return type === 'Bearer' ? token : undefined; }}
Nous pouvons maintenant implémenter notre route protégée et enregistrer notre AuthGuard
pour la protéger.
Ouvrez le fichier auth.controller.ts
et mettez-le à jour comme suit :
auth.controller.ts
import { Body, Controller, Get, HttpCode, HttpStatus, Post, Request, UseGuards } from '@nestjs/common';import { AuthGuard } from './auth.guard';import { AuthService } from './auth.service';
@Controller('auth')export class AuthController { constructor(private authService: AuthService) {}
@HttpCode(HttpStatus.OK) @Post('login') async signIn(@Body() signInDto: Record<string, any>) { return this.authService.signIn(signInDto.username, signInDto.password); }
@UseGuards(AuthGuard) @Get('profile') getProfile(@Request() req) { return req.user; }}
Nous appliquons le AuthGuard
que nous venons de créer à la route GET /profile
afin qu’elle soit protégée.
Assurez-vous que l’application est en cours d’exécution et testez les routes en utilisant cURL
.
$ # GET /profile$ curl http://localhost:3000/auth/profile{ "statusCode": 401, "message": "Unauthorized" }
$ # POST /auth/login$ curl -X POST http://localhost:3000/auth/login -d '{"username": "john", "password": "changeme"}' -H "Content-Type: application/json"{ "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2Vybm..." }
$ # GET /profile en utilisant le access_token renvoyé de l'étape précédente comme code de porteur$ curl http://localhost:3000/auth/profile -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2Vybm..."{ "sub": 1, "username": "john", "iat": .., "exp": ..}
Notez que dans le AuthModule
, nous configurons le JWT pour avoir une expiration de 60 secondes
. C’est une expiration trop courte, et traiter les détails de l’expiration et du rafraîchissement des jetons dépasse le cadre de cet article. Cependant, nous choisissons cela pour démontrer une qualité importante des JWT. Si vous attendez 60 secondes après vous être authentifié avant de tenter une demande GET /auth/profile
, vous recevrez une réponse 401 Unauthorized
. Cela est dû au fait que @nestjs/jwt
vérifie automatiquement le JWT pour son heure d’expiration, vous épargnant ainsi la peine de le faire dans votre application.
Nous avons maintenant terminé notre mise en œuvre de l’authentification JWT. Les clients JavaScript (tels qu’Angular/React/Vue) et d’autres applications JavaScript peuvent maintenant s’authentifier et communiquer en toute sécurité avec notre serveur API.
Activer l’authentification globalement
Si la grande majorité de vos points de terminaison doivent être protégés par défaut, vous pouvez enregistrer le garde d’authentification en tant que garde global et au lieu d’utiliser le décorateur @UseGuards()
en haut de chaque contrôleur, vous pourriez simplement signaler quelles routes devraient être publiques.
Tout d’abord, enregistrez le AuthGuard
en tant que garde global en utilisant la construction suivante (dans n’importe quel module, par exemple, dans le AuthModule
) :
providers: [ { provide: APP_GUARD, useClass: AuthGuard, },],
Avec cela en place, Nest liera automatiquement AuthGuard
à tous les points de terminaison.
Nous devons maintenant fournir un mécanisme pour déclarer les routes comme publiques. Pour cela, nous pouvons créer un décorateur personnalisé en utilisant la fonction de fabrique de décorateurs SetMetadata
.
import { SetMetadata } from '@nestjs/common';
export const IS_PUBLIC_KEY = 'isPublic';export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
Dans le fichier ci-dessus, nous avons exporté deux constantes. L’une étant notre clé de métadonnées nommée IS_PUBLIC_KEY
, et l’autre étant notre nouveau décorateur lui-même que nous allons appeler Public
(vous pouvez également l’appeler SkipAuth
ou AllowAnon
, peu importe ce qui convient à votre projet).
Maintenant que nous avons un décorateur @Public()
personnalisé, nous pouvons l’utiliser pour décorer n’importe quelle méthode, comme suit :
@Public()@Get()findAll() { return [];}
Enfin, nous avons besoin que le AuthGuard
retourne true
lorsque les métadonnées "isPublic"
sont trouvées. Pour cela, nous utiliserons la classe Reflector
.
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';import { Reflector } from '@nestjs/core';import { jwtConstants } from './constants';
@Injectable()export class AuthGuard implements CanActivate { constructor(private jwtService: JwtService, private reflector: Reflector) {}
async canActivate(context: ExecutionContext): Promise<boolean> { const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [ context.getHandler(), context.getClass(), ]); if (isPublic) { return true; }
const request = context.switchToHttp().getRequest(); const token = this.extractTokenFromHeader(request); if (!token) { throw new UnauthorizedException(); }
try { const payload = await this.jwtService.verifyAsync(token, { secret: jwtConstants.secret, }); request['user'] = payload; } catch { throw new UnauthorizedException(); } return true; }
private extractTokenFromHeader(request: Request): string | undefined { const [type, token] = request.headers.authorization?.split(' ') ?? []; return type === 'Bearer' ? token : undefined; }}
Intégration de Passport
Passport est la bibliothèque d’authentification node.js la plus populaire, bien connue de la communauté et utilisée avec succès dans de nombreuses applications de production. C’est simple d’intégrer cette bibliothèque avec une application Nest en utilisant le module @nestjs/passport
.
Pour apprendre comment vous pouvez intégrer Passport avec NestJS, consultez ce chapitre.
Exemple
Vous pouvez trouver une version complète du code dans ce chapitre ici.