Archives Wild Code School
NestJS

5. NestJS : Authentification

Quête 2 sur 2

Tu vas apprendre comment créer une authentification JWT avec le framework NestJS.

Objectifs

  • Créer un module d'authentification
  • Créer une stratégie
  • Utiliser les Guards

Pré-requis

Tu as un module users avec une entité User et un UserService fonctionnel. Tu as normalement déjà tout cela dans les quêtes précédentes avant la mise en pratique.

Dépendances à installer

Tu vas utiliser PassportJS pour gérer l'authentification.

shell
npm install --save @nestjs/passport passport
npm install --save @nestjs/jwt passport-jwt
npm install --save-dev @types/passport-jwt

Il existe plusieurs types de stratégies d'authentification : password-local, passport-jwt Nous allons utiliser la stratégie passport-jwt pour faire une authentification avec un token jwt.

Module d'authentification

Tu vas tout d'abord générer un module pour l'authentification. C'est un module comme un autre. Il contiendra un service : le service d'authentification.

shell
nest g module auth
nest g service auth

AuthService

Le role du AuthService sera de récupérer un utilisateur suivant un email et un mot de passe. Tu dois d'abord créer une méthode dans ton UsersService pour récupérer un utilisateur par rapport à son email. Tu compareras ensuite si les mots de passe correspondent.

Tu vas créer une méthode getUserByEmail(email: string) qui te renverra l'utilisateur de la base de données.

typescript
// src/users/users.service.ts

...

@Injectable()
export class UsersService {

    constructor(@InjectRepository(User) private usersRepository: Repository<User>) { }

    ...

    async getUserByEmail(email: string): Promise<User | undefined> {
      return this.usersRepository.findOneBy({ email: email });
    }

    ...
}

Ton UsersService fait partie du module user et ton AuthService fait parti du module auth. Ce sont 2 modules différents. Pour utiliser ton UsersService dans ton module AuthService tu dois exporter ton UsersService ainsi que la configuration TypeORM de l'entité User qui correspond au repository de ton entité User.

typescript
// src/users/users.module.ts

...

@Module({
  ...
  exports: [UsersService, TypeOrmModule.forFeature([User])],
})
export class UsersModule {}

Tu dois ensuite importer le UsersModule dans le module AuthModule pour pouvoir utiliser le UserService:

typescript
// src/auth/auth.module.ts
...
import { UsersModule } from 'src/users/users.module';

@Module({
  imports: [
    UsersModule,
    ...
  ],
  ...
})
export class AuthModule {}


Il faut maintenant compléter le AuthService. Tu vas avoir besoin d'une méthode de validation qui prend en paramètre un utilisateur, d'une autre méthode pour gérer les éventuels codes d'erreur et d'une méthode pour enregistrer un nouvel utilisateur.

Il te faudra également une méthode pour hasher un mot de passe.

typescript
// src/auth/auth.service.ts

import { User } from './../users/user.entity';
import { JwtService } from '@nestjs/jwt';
import { UsersService } from './../users/users.service';
import { Injectable } from '@nestjs/common';
import * as crypto from 'crypto';

@Injectable()
export class AuthService {
  constructor(
    private readonly usersService: UsersService,
    private readonly jwtService: JwtService
    ) {}

  /**
   * Validate the user in parameter
   */
  async validate(email: string): Promise<any> {
    return this.usersService.getUserByEmail(email);
  }

  /**
   * Try to login the user in parameter.
   * Return an access token if login is successfull otherwise return
   * a 404 status
   * @param user 
   */
  public async login(user: User): Promise<any | { status: number}> {
    return this.validate(user.email).then(userData => {
      // user not found
      if (!userData || userData.password != this.hash(user.password)) {
        return { status: 404 };
      }

      // user found
      // The access token will be composed by the email
      const payload = `${userData.email}`;
      const accessToken = this.jwtService.sign(payload);

      return {
        expires_in: 3600, // 1hour
        access_token: accessToken,
      }
    });
  }

  public async register(user: User): Promise<any> {
    user.password = this.hash(user.password);

    return this.usersService.saveUser(user);
  }

  /**
   * Hash the password in parameter.
   */
  private hash(password): string{
   return crypto.createHmac('sha256', password).digest('hex');
  }
}

AuthController

Tu dois maintenant créer le endpoint d'authentification à l'aide d'un controller.

shell
nest g controller auth

Ton controller doit répondre à 2 routes. Une route pour s'authentifier :

shell
POST /auth/login
{
    "email": "dupont@example.com",
    "password": "azerty"
}

Cette route te renvoie un accessToken :

json
{
    "expires_in": 3600,
    "access_token": "eyJhbGciOiJIUzI1NiJ9.ZHVwb250QGV4YW1wbGUuY29tMQ.lfi7yjNQR1SosZ-irzHCFSvwz13iJzX8icBHp0LW88U"
}

Une autre route pour s'enregistrer :

shell
POST /auth/register
{
    "firstname": "Pierre",
    "lastname": "Dupont",
    "email": "dupont@example.com",
    "password": "azerty"
}

Il te suffit d'appeler les méthodes login(user: User) et register(user: User) de ton AuthService.

typescript
//src/auth/auth.controller.ts

import { Controller, Post, Body } from '@nestjs/common';
import { AuthService } from './auth.service';
import { User } from '../users/user.entity';

@Controller('auth')
export class AuthController {
  constructor(private readonly authService: AuthService) {}

  @Post('login')
  async login(@Body() user: User): Promise<any> {
    return this.authService.login(user);
  }

  @Post('register')
  async register(@Body() user: User): Promise<any> {
    return this.authService.register(user);
  }
}

Tu définis que chacune de tes routes prend un user de type User. La sérialisation sera automatique.

JSON Web Token

Vu que tu utilises une authentification JWT, tu dois configurer une clé secrète qui servira à crypter l'access token. Tu la stockeras dans un fichier de constants.

typescript
// src/app/auth/constants.ts

export const jwtConstants = {
  secret: 'secret123456789',
};

Tu dois ensuite l'importer dans ton AuthModule.

typescript
// src/app/auth/auth.module.ts

import { JwtModule } from '@nestjs/jwt';
import { jwtConstants } from './constants';
...

@Module({
  imports: [
    ...
    JwtModule.register({
      secret: jwtConstants.secret
    })
  ],
  ...
})
export class AuthModule {}

AuthGuard

Strategy

Maintenant que tu sais comment récupérer un access token, il te faut sécuriser les routes que tu souhaites. Pour cela, tu dois utiliser un Guard et plus particulièrement le AuthGuard mis à disposition par Nest.

L'AuthGuard prend en paramètre une stratégie. Une stratégie est une classe de service qui hérite de PassportStrategy(Strategy).

Tu vas créer un fichier jwt.strategy.ts dans le dossier de auth.

typescript
// src/app/auth/jwt.strategy.ts

import { User } from './../users/user.entity';
import { AuthService } from './auth.service';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { jwtConstants } from './constants';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(private readonly authService: AuthService) {
    // Configure the strategy
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: jwtConstants.secret,
    });
  }

  async validate(email: string) {
    const userFound = await this.authService.validate(email);
    if (!userFound) {
      throw new UnauthorizedException();
    }
    return userFound;
  }
}

La stratégie est configurée dans le constructor. En résumé, on définit que cette stratégie concerne des requêtes qui auront un access token. L'access token sera automatiquement décodé à l'aide de la clé secrète (qui est la même que précédemment).

Quand tu appliqueras cette stratégie sur une route, l'access token sera décodé et son contenu sera passé à la méthode validate(email: string). Oui, souviens toi, le contenu de l'access token n'est rien d'autre que l'email de l'utilisateur connecté. Tu l'as défini précédemment dans le AuthService.

Une stratégie étant un service, tu dois l'ajouter dans les providers de ton AuthModule.

typescript
import { JwtStrategy } from './jwt.strategy';
...

@Module({
  ...
  providers: [UsersService, AuthService, JwtStrategy],
  ...
})
export class AuthModule {}

Guard

La dernière étape consiste à sécuriser une route. Par exemple, la route /hello dans le AppController.

typescript
import { Controller, Get, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @UseGuards(AuthGuard('jwt'))
  @Get('/hello')
  getHello(): string {
    return this.appService.getHello();
  }
}

Challenge

Sécurisation

Reprends l'application de blog que tu as réalisée dans la quête précédente et sécurise les routes de création, modification et suppression. Seul un utilisateur connecté pourra interagir avec les articles.

Critères de validation

  • La liste des articles est publique.
  • Les endpoints sont sécurisés par la JwtStrategy