MongoDB
 sql >> Base de données >  >> NoSQL >> MongoDB

NestJS :comment implémenter l'authentification utilisateur basée sur la session

Introduction

C'est une réalité incontestable que l'authentification est essentielle dans toute application ou système si vous souhaitez sécuriser les données des utilisateurs et permettre un accès sécurisé aux informations. L'authentification est la procédure permettant d'établir ou de démontrer que quelque chose est vrai, légitime ou valide.

Prérequis

Ce tutoriel est une démonstration pratique. Pour suivre, assurez-vous d'avoir les éléments suivants en place :

  • Node.js s'exécute sur votre système, car NestJS est un framework Node.js
  • MongoDB installé

Qu'est-ce que NestJS ?

Nest (NestJS) est un cadre d'application côté serveur Node.js permettant de créer des applications évolutives et efficaces.

Il est écrit en TypeScript et construit sur Express, un cadre très minimaliste qui est excellent en soi mais qui manque de structure. Il combine des paradigmes de programmation tels que la programmation orientée objet, la programmation fonctionnelle et la programmation réactive fonctionnelle.

C'est un framework à utiliser si vous voulez beaucoup de structure sur votre backend. Sa syntaxe et sa structure sont très similaires à AngularJS, un framework frontal. Et il utilise TypeScript, les services et l'injection de dépendances de la même manière qu'AngularJS.

Il utilise des modules et des contrôleurs, et vous pouvez créer des contrôleurs pour un fichier à l'aide de l'interface de ligne de commande.

Les modules NestJS vous permettent de regrouper les contrôleurs et les fournisseurs de services associés dans un seul fichier de code. En termes simples, un module NestJS est un fichier TypeScript avec le @Module annotation (). Ce décorateur informe le framework NestJS des contrôleurs, fournisseurs de services et autres ressources associées qui seront instanciés et utilisés ultérieurement par le code de l'application.

Qu'est-ce que l'authentification basée sur la session ?

L'authentification basée sur la session est une méthode d'authentification de l'utilisateur dans laquelle le serveur crée une session après une connexion réussie, avec l'ID de session stocké dans un cookie ou un stockage local dans votre navigateur.

Lors de demandes ultérieures, votre cookie est validé par rapport à l'ID de session stocké sur le serveur. S'il y a une correspondance, la demande est considérée comme valide et traitée.

Lorsque vous utilisez cette méthode d'authentification, il est essentiel de garder à l'esprit les meilleures pratiques de sécurité suivantes :

  • Générer des identifiants de session longs et aléatoires (128 bits est la longueur recommandée) pour rendre les attaques par force brute inefficaces
  • Évitez de stocker des données sensibles ou spécifiques à l'utilisateur
  • Rendre les communications HTTPS obligatoires pour toutes les applications basées sur des sessions
  • Créer des cookies dotés d'attributs sécurisés et HTTP uniquement

Pourquoi l'authentification basée sur la session ?

L'authentification basée sur la session est plus sécurisée que la plupart des méthodes d'authentification car elle est simple, sécurisée et a une taille de stockage limitée. Il est également considéré comme la meilleure option pour les sites Web du même domaine racine.

Configuration du projet

Commencez la configuration de votre projet en installant Nest CLI globalement. Vous n'avez pas besoin de le faire si NestJS CLI est déjà installé.

La CLI Nest est un outil d'interface de ligne de commande permettant de configurer, de développer et de gérer les applications Nest.

npm i -g @nestjs/cli

Maintenant, configurons votre projet en exécutant la commande suivante :

nest new session-based-auth

La commande ci-dessus crée une application Nest avec certains passe-partout, puis vous invite à choisir votre gestionnaire de packages préféré pour installer les modules requis pour exécuter votre application. Pour la démonstration, ce tutoriel utilise npm . Appuyez sur la touche Entrée pour continuer avec npm .

Si tout s'est bien passé, vous devriez voir une sortie comme celle de la capture d'écran ci-dessous sur votre terminal.

Une fois l'installation terminée, déplacez-vous dans le répertoire de votre projet et exécutez l'application avec la commande ci-dessous :

npm run start:dev

La commande ci-dessus exécute l'application et surveille les modifications. Votre projet src la structure des dossiers doit ressembler à ceci.

└───src
│   └───app.controller.ts
│   └───app.modules.ts
│   └───app.service.ts
│   └───main.ts

Installer les dépendances

Maintenant que votre application est configurée, installons les dépendances nécessaires.

npm install --save @nestjs/passport passport passport-local

La commande ci-dessus installe Passport.js, une bibliothèque d'authentification nest.js populaire.

Installez également les types pour la stratégie avec la commande ci-dessous :

Il contient des définitions de type pour passport-local .

npm install --save-dev @types/passport-local

Configurer la base de données MongoDB dans NestJS

Pour configurer et connecter votre base de données, installez le package Mongoose et le wrapper NestJS avec la commande suivante :

npm install --save @nestjs/mongoose mongoose

Le wrapper Mongoose NestJS vous aide à utiliser Mongoose dans l'application NestJS et offre une prise en charge TypeScript approuvée.

Maintenant, rendez-vous sur votre app.module.ts , et importez la mongoose module de @nestjs/mongoose . Appelez ensuite le forRoot() method, une méthode fournie par le module Mongoose, et transmettez la chaîne d'URL de votre base de données.

Configuration de votre connexion à la base de données dans app.module.ts aide votre application à se connecter à la base de données immédiatement au démarrage du serveur - après avoir exécuté votre application puisqu'il s'agit du premier module à être chargé.

app.module.ts

import { Module } from "@nestjs/common"
import { MongooseModule } from "@nestjs/mongoose"
import { AppController } from "./app.controller"
import { AppService } from "./app.service"

@Module({
  imports: [
    MongooseModule.forRoot(
      "mongodb+srv://<username>:<password>@cluster0.kngtf.mongodb.net/session-auth?retryWrites=true&w=majority"
    ),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

Créer un module d'utilisateurs

Pour des raisons de séparation, pour rendre votre code propre et bien organisé, créez un module spécifiquement pour les utilisateurs utilisant la CLI NestJS en exécutant la commande suivante :

nest g module users

La commande ci-dessus crée un users dossier avec users.module.ts et met à jour app.module.ts

Créez également users.service.ts et users.controller.ts fichiers avec les commandes suivantes :

nest g service users
nest g controller users

Notez que vous pouvez créer vos dossiers et fichiers manuellement sans utiliser la CLI Nest, mais l'utilisation de la CLI met automatiquement à jour les dossiers nécessaires et vous facilite la vie.

Créer un schéma utilisateur

L'étape suivante consiste à créer votre UserSchema, mais d'abord, ajoutez un users.model.ts fichier, où vous allez créer UserSchema

Cela devrait être la forme de notre application src dossier maintenant.

└───src
│   └───users
│   │   └───users.controller.ts
│   │   └───users.model.ts
│   │   └───users.module.ts
│   │   └───users.service.ts
│   └───app.controller.ts
│   └───app.module.ts
│   └───app.service.ts
│   └───main.ts

Pour créer UserSchema , importez tout en tant que mongoose à partir du package mongoose dans users.model.ts . Appelez ensuite le nouveau schéma de mangouste, un plan du modèle utilisateur, et transmettez un objet JavaScript dans lequel vous définirez l'objet utilisateur et les données.

users.model.ts

import * as mongoose from "mongoose"
export const UserSchema = new mongoose.Schema(
  {
    username: {
      type: String,
      required: true,
      unique: true,
    },
    password: {
      type: String,
      required: true,
    },
  },
  { timestamps: true }
)

export interface User extends mongoose.Document {
  _id: string;
  username: string;
  password: string;
}

Créez également une interface pour votre modèle qui étend mongoose, un document qui vous aide à remplir vos collections MongoDB.

Rendez-vous sur votre users.module.ts et importez MongooseModule dans le tableau des importations. Appelez ensuite le forFeature() méthode fournie par MongooseModule , et passez un tableau d'objets qui prend le nom et le schéma.

Cela vous permettra de partager le fichier n'importe où à l'aide de l'injection de dépendances.

users.module.ts

import { Module } from "@nestjs/common"
import { MongooseModule } from "@nestjs/mongoose"
import { UsersController } from "./users.controller"
import { UserSchema } from "./users.model"
import { UsersService } from "./users.service"
@Module({
  imports: [MongooseModule.forFeature([{ name: "user", schema: UserSchema }])],
  controllers: [UsersController],
  providers: [UsersService],
})
export class UsersModule {}

Dans users.module.ts , exportez le UsersService pour vous permettre d'y accéder dans un autre module.

users.module.ts

import { Module } from "@nestjs/common"
import { MongooseModule } from "@nestjs/mongoose"
import { UsersController } from "./users.controller"
import { UserSchema } from "./users.model"
import { UsersService } from "./users.service"
@Module({
  imports: [MongooseModule.forFeature([{ name: "user", schema: UserSchema }])],
  controllers: [UsersController],
  providers: [UsersService],
  exports: [UsersService],
})
export class UsersModule {}

C'est généralement une bonne idée d'encapsuler la logique métier dans une classe distincte. Une telle classe est connue sous le nom de service. Le travail de cette classe est de traiter les requêtes du contrôleur et d'exécuter la logique métier.

Dans users.service.ts fichier, importer Model de mongoose , User de users.model.ts , et InjectModel de @nestjs/mongoose . Ajoutez ensuite une méthode au UsersService classe qui prend un nom d'utilisateur et un mot de passe, et appelle la méthode insertUser() .

users.service.ts

import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { User } from './users.model';
@Injectable()
export class UsersService {
  constructor(@InjectModel('user') private readonly userModel: Model<User>) {}
  async insertUser(userName: string, password: string) {
    const username = userName.toLowerCase();
    const newUser = new this.userModel({
      username,
      password,
    });
    await newUser.save();
    return newUser;
  }
}

Maintenant que le UsersService class est prête, vous devez l'injecter dans votre contrôleur. Mais d'abord, parlons du stockage sécurisé des mots de passe des utilisateurs.

L'aspect le plus critique de la procédure d'enregistrement est le mot de passe des utilisateurs, qui ne doit pas être enregistré en clair. Il est de la responsabilité de l'utilisateur de créer un mot de passe fort, mais il est de votre obligation en tant que développeur de garder ses mots de passe sécurisés. En cas de violation de la base de données, les mots de passe des utilisateurs seraient exposés. Et que se passe-t-il s'il est stocké en texte brut ? Je crois que vous connaissez la réponse. Pour résoudre ce problème, hachez les mots de passe à l'aide de bcrypt.

Alors, installez bcrypt et @types/bcrypt avec la commande suivante :

npm install @types/bcrypt bcrypt

Avec cela à l'écart, configurez votre contrôleur. Tout d'abord, importez votre UsersService classe et tout de bcrypt . Ajoutez ensuite un constructeur et une méthode permettant d'ajouter un utilisateur; il gérera les demandes de publication entrantes, appelez-le addUser , avec un corps de fonction où vous hacherez le mot de passe.

users.controller.ts

import { Body, Controller, Post } from '@nestjs/common';
import { UsersService } from './users.service';
import * as bcrypt from 'bcrypt';
@Controller('users')
export class UsersController {
  constructor(private readonly usersService: UsersService) {}
  //post / signup
  @Post('/signup')
  async addUser(
    @Body('password') userPassword: string,
    @Body('username') userName: string,
  ) {
    const saltOrRounds = 10;
    const hashedPassword = await bcrypt.hash(userPassword, saltOrRounds);
    const result = await this.usersService.insertUser(
      userName,
      hashedPassword,
    );
    return {
      msg: 'User successfully registered',
      userId: result.id,
      userName: result.username
    };
  }
}

L'enregistrement se fait dans le app.module.ts fichier, qui est obtenu en ajoutant le UsersModule au @Module() tableau des importations du décorateur dans app.module.ts .

app.module.ts

import { Module } from "@nestjs/common"
import { MongooseModule } from "@nestjs/mongoose"
import { AppController } from "./app.controller"
import { AppService } from "./app.service"
import { UsersModule } from "./users/users.module"

@Module({
  imports: [
    MongooseModule.forRoot(
      "mongodb+srv://<username>:<password>@cluster0.kngtf.mongodb.net/session-auth?retryWrites=true&w=majority"
    ),
    UsersModule,
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

Toutes nos félicitations! Vous avez terminé l'enregistrement. Vous pouvez maintenant enregistrer un utilisateur avec un nom d'utilisateur et un mot de passe.

Maintenant, avec l'enregistrement terminé, ajoutez un getUser fonction à votre UsersService avec le findOne méthode pour trouver un utilisateur par nom d'utilisateur.

users.service.ts

import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { User } from './users.model';
@Injectable()
export class UsersService {
  constructor(@InjectModel('user') private readonly userModel: Model<User>) {}
  async insertUser(userName: string, password: string) {
    const username = userName.toLowerCase();
    const newUser = new this.userModel({
      username,
      password,
    });
    await newUser.save();
    return newUser;
  }
  async getUser(userName: string) {
    const username = userName.toLowerCase();
    const user = await this.userModel.findOne({ username });
    return user;
  }
}

Créer un module d'authentification

Comme pour les utilisateurs, créez un module et un service d'authentification spécifiquement pour toutes les authentifications/vérifications. Pour ce faire, exécutez les commandes suivantes :

nest g module auth
nest g service auth

Ce qui précède créera un dossier auth, auth.module.ts , et auth.service.ts , et mettez à jour le auth.module.ts et app.module.ts fichiers.

À ce stade, la forme de votre application src dossier doit ressembler à ceci.

└───src
│   └───auth
│   │   └───auth.module.ts
│   │   └───auth.service.ts
│   └───users
│   │   └───users.controller.ts
│   │   └───users.model.ts
│   │   └───users.module.ts
│   │   └───users.service.ts
│   └───app.controller.ts
│   └───app.module.ts
│   └───app.service.ts
│   └───main.ts

La commande de génération ci-dessus mettra à jour votre app.module.ts , et il ressemblera à l'extrait de code ci-dessous :

app.module.ts

import { Module } from "@nestjs/common"
import { MongooseModule } from "@nestjs/mongoose"
import { AppController } from "./app.controller"
import { AppService } from "./app.service"
import { UsersModule } from "./users/users.module"
import { AuthModule } from './auth/auth.module';


@Module({
  imports: [UsersModule, AuthModule, MongooseModule.forRoot(
    //database url string
    'mongodb://localhost:27017/myapp'
    )],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

Authentifier les utilisateurs

Accédez à votre auth.module.ts fichier et ajoutez UsersModule dans le tableau des importations pour permettre l'accès au UsersService exporté depuis le users.module.ts fichier.

auth.module.ts

import { Module } from "@nestjs/common"
import { UsersModule } from "src/users/users.module"
import { AuthService } from "./auth.service"

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

Dans votre auth.service.ts fichier, appelez le constructeur afin que vous puissiez injecter le UsersService , et ajoutez une méthode de validation qui prendra un nom d'utilisateur et un mot de passe.

Pour ajouter des validations de base, vérifiez si l'utilisateur existe dans la base de données et comparez le mot de passe donné avec celui de votre base de données pour vous assurer qu'il correspond. S'il existe, retourner l'utilisateur dans le request.user objet — sinon, renvoie null.

auth.service.ts

    import { Injectable, NotAcceptableException } from '@nestjs/common';
    import { UsersService } from 'src/users/users.service';
    import * as bcrypt from 'bcrypt';

    @Injectable()
    export class AuthService {
      constructor(private readonly usersService: UsersService) {}
      async validateUser(username: string, password: string): Promise<any> {
        const user = await this.usersService.getUser(username);
        const passwordValid = await bcrypt.compare(password, user.password)
        if (!user) {
            throw new NotAcceptableException('could not find the user');
          }
        if (user && passwordValid) {
          return {
            userId: user.id,
            userName: user.username
          };
        }
        return null;
      }
    }

Pour aller plus loin, créez un nouveau fichier et nommez-le local.strategy.ts . Ce fichier représentera la stratégie de Passport.js , que vous avez installé précédemment, c'est la local strategy . Et à l'intérieur, passez la stratégie, qui est la Strategy de passport-local .

Créez un constructeur et injectez le AuthService , appelez le super() méthode; assurez-vous d'appeler le super() méthode.

local.strategy.ts

    import { Injectable, UnauthorizedException } from '@nestjs/common';
    import { PassportStrategy } from '@nestjs/passport';
    import { Strategy } from 'passport-local';
    import { AuthService } from './auth.service';
    @Injectable()
    export class LocalStrategy extends PassportStrategy(Strategy) {
      constructor(private readonly authService: AuthService) {
        super();
      }
      async validate(username: string, password: string): Promise<any> {
        const userName = username.toLowerCase();
        const user = await this.authService.validateUser(userName, password);
        if (!user) {
          throw new UnauthorizedException();
        }
        return user;
      }
    }

Retournez à votre auth.module.ts dossier. Ajoutez ensuite PassportModule aux importations et LocalStrategy aux fournisseurs.

auth.module.ts

import { Module } from "@nestjs/common"
import { PassportModule } from "@nestjs/passport"
import { UsersModule } from "src/users/users.module"
import { AuthService } from "./auth.service"
import { LocalStrategy } from "./local.strategy"

@Module({
  imports: [UsersModule, PassportModule],
  providers: [AuthService, LocalStrategy],
})
export class AuthModule {}

Maintenant, ajoutez la route de connexion à votre users.controller.ts :

users.controller.ts

    import {
      Body,
      Controller,
      Post,
      Request,
    } from '@nestjs/common';
    import * as bcrypt from 'bcrypt';
    import { UsersService } from './users.service';
    @Controller('users')
    export class UsersController {
      constructor(private readonly usersService: UsersService) {}
      //post / signup
      @Post('/signup')
      async addUser(
        @Body('password') userPassword: string,
        @Body('username') userName: string,
      ) {
        const saltOrRounds = 10;
        const hashedPassword = await bcrypt.hash(userPassword, saltOrRounds);
        const result = await this.usersService.insertUser(
          userName,
          hashedPassword,
        );
        return {
          msg: 'User successfully registered',
          userId: result.id,
          userName: result.username
        };
      }
      //Post / Login
      @Post('/login')
      login(@Request() req): any {
        return {User: req.user,
                msg: 'User logged in'};
      }
    }

Maintenant que vous avez tout mis en place, vous ne pouvez toujours pas connecter un utilisateur car il n'y a rien pour déclencher la route de connexion. Ici, utilisez des gardes pour y parvenir.

Créez un fichier et nommez-le local.auth.guard.ts , puis une classe LocalAuthGuard qui étend AuthGuard depuis NestJS/passport , où vous fournirez le nom de la stratégie et passerez le nom de votre stratégie, local .

local.auth.guard.ts.

import { Injectable } from "@nestjs/common"
import { AuthGuard } from "@nestjs/passport"
@Injectable()
export class LocalAuthGuard extends AuthGuard("local") {}

Ajoutez le UseGuard décorateur à votre route de connexion dans le users.controller.ts fichier, et transmettez le LocalAuthGuard .

users.controller.ts

    import {
      Body,
      Controller,
      Post,
      UseGuards,
      Request,
    } from '@nestjs/common';
    import * as bcrypt from 'bcrypt';
    import { LocalAuthGuard } from 'src/auth/local.auth.guard';
    import { UsersService } from './users.service';
    @Controller('users')
    export class UsersController {
      constructor(private readonly usersService: UsersService) {}
      //post / signup
      @Post('/signup')
      async addUser(
        @Body('password') userPassword: string,
        @Body('username') userName: string,
      ) {
        const saltOrRounds = 10;
        const hashedPassword = await bcrypt.hash(userPassword, saltOrRounds);
        const result = await this.usersService.insertUser(
          userName,
          hashedPassword,
        );
        return {
          msg: 'User successfully registered',
          userId: result.id,
          userName: result.username
        };
      }
      //Post / Login
      @UseGuards(LocalAuthGuard)
      @Post('/login')
      login(@Request() req): any {
        return {User: req.user,
                msg: 'User logged in'};
      }
    }

Enfin, vous pouvez connecter un utilisateur avec un nom d'utilisateur et un mot de passe enregistrés.

Protéger les routes d'authentification

Vous avez configuré avec succès l'authentification des utilisateurs. Désormais, protégez vos itinéraires contre les accès non autorisés en limitant l'accès aux seuls utilisateurs authentifiés. Accédez à votre users.controller.ts file, et ajoutez une autre route - nommez-la 'protected' et faites-lui renvoyer le req.user objet.

users.controller.ts

    import {
      Body,
      Controller,
      Get,
      Post,
      UseGuards,
      Request,
    } from '@nestjs/common';
    import * as bcrypt from 'bcrypt';
    import { LocalAuthGuard } from 'src/auth/local.auth.guard';
    import { UsersService } from './users.service';
    @Controller('users')
    export class UsersController {
      constructor(private readonly usersService: UsersService) {}
      //signup
      @Post('/signup')
      async addUser(
        @Body('password') userPassword: string,
        @Body('username') userName: string,
      ) {
        const saltOrRounds = 10;
        const hashedPassword = await bcrypt.hash(userPassword, saltOrRounds);
        const result = await this.usersService.insertUser(
          userName,
          hashedPassword,
        );
        return {
          msg: 'User successfully registered',
          userId: result.id,
          userName: result.username
        };
      }
      //Post / Login
      @UseGuards(LocalAuthGuard)
      @Post('/login')
      login(@Request() req): any {
        return {User: req.user,
                msg: 'User logged in'};
      }
    // Get / protected
      @Get('/protected')
      getHello(@Request() req): string {
        return req.user;
      }
    }

La route protégée dans le code ci-dessus renverra un objet vide au lieu de renvoyer les détails de l'utilisateur lorsqu'un utilisateur connecté lui fait une demande car il a déjà perdu la connexion.

Pour que tout soit trié, c'est là qu'intervient l'authentification basée sur la session.

Dans l'authentification basée sur la session, lorsqu'un utilisateur se connecte, l'utilisateur est enregistré dans une session afin que toute demande ultérieure de l'utilisateur après la connexion récupère les détails de la session et accorde à l'utilisateur un accès facile. La session expire lorsque l'utilisateur se déconnecte.

Pour démarrer l'authentification basée sur la session, installez express-session et les types NestJS à l'aide de la commande suivante :

npm install express-session @types/express-session

Lorsque l'installation est terminée, accédez à votre main.ts file, la racine de votre application, et faites-y les configurations.

Importez tout depuis passport et express-session , puis ajoutez l'initialisation du passeport et la session du passeport.

Il est préférable de conserver votre clé secrète dans vos variables d'environnement.

main.ts

import { NestFactory } from "@nestjs/core"
import { AppModule } from "./app.module"
import * as session from "express-session"
import * as passport from "passport"
async function bootstrap() {
  const app = await NestFactory.create(AppModule)
  app.use(
    session({
      secret: "keyboard",
      resave: false,
      saveUninitialized: false,
    })
  )
  app.use(passport.initialize())
  app.use(passport.session())

  await app.listen(3000)
}
bootstrap()

Ajouter un nouveau fichier, authenticated.guard.ts , dans votre auth dossier. Et créez un nouveau Guard qui vérifie s'il existe une session pour l'utilisateur faisant la demande - nommez-le authenticatedGuard .

authenticated.guard.ts

import { CanActivate, ExecutionContext, Injectable } from "@nestjs/common"

@Injectable()
export class AuthenticatedGuard implements CanActivate {
  async canActivate(context: ExecutionContext) {
    const request = context.switchToHttp().getRequest()
    return request.isAuthenticated()
  }
}

Dans le code ci-dessus, la demande est obtenue à partir du contexte et vérifiée si elle est authentifiée. isAuthenticated() vient de passport.js automatiquement ; ça dit. "Hey ! Existe-t-il une session pour cet utilisateur ? Si oui, continuez."

Pour déclencher la connexion, dans votre users.controller.ts fichier :

  • importer authenticated de authenticated.guard.ts;
  • ajoutez le useGuard décorateur au protected itinéraire; et,
  • transmettre AuthenticatedGuard .

users.controller.ts

    import {
      Body,
      Controller,
      Get,
      Post,
      UseGuards,
      Request,
    } from '@nestjs/common';
    import * as bcrypt from 'bcrypt';
    import { AuthenticatedGuard } from 'src/auth/authenticated.guard';
    import { LocalAuthGuard } from 'src/auth/local.auth.guard';
    import { UsersService } from './users.service';
    @Controller('users')
    export class UsersController {
      constructor(private readonly usersService: UsersService) {}
      //signup
      @Post('/signup')
      async addUser(
        @Body('password') userPassword: string,
        @Body('username') userName: string,
      ) {
        const saltOrRounds = 10;
        const hashedPassword = await bcrypt.hash(userPassword, saltOrRounds);
        const result = await this.usersService.insertUser(
          userName,
          hashedPassword,
        );
        return {
          msg: 'User successfully registered',
          userId: result.id,
          userName: result.username
        };
      }
      //Post / Login
      @UseGuards(LocalAuthGuard)
      @Post('/login')
      login(@Request() req): any {
        return {User: req.user,
                msg: 'User logged in'};
      }
      //Get / protected
      @UseGuards(AuthenticatedGuard)
      @Get('/protected')
      getHello(@Request() req): string {
        return req.user;
      }
    }

À ce stade, cela échoue toujours car vous n'avez configuré que express-session mais ne l'a pas implémenté.

Lorsqu'un utilisateur se connecte, vous devez enregistrer l'utilisateur dans une session afin que l'utilisateur puisse accéder à d'autres routes avec la session.

Une chose à garder à l'esprit est que par défaut, le express-session bibliothèque stocke la session dans la mémoire du serveur Web.

Avant d'entrer dans la session, vous devez sérialiser l'utilisateur. À la sortie de la session, désérialisez l'utilisateur.

Donc, créez un nouveau fichier dans le dossier auth pour le sérialiseur et le désérialiseur, nommez-le session.serializer.ts .

À ce stade, la forme de notre application src dossier devrait ressembler à ceci.

    └───src
    │   └───auth
    │   │   └───auth.module.ts
    │   │   └───auth.service.ts
    │   │   └───authenticated.guard.ts
    │   │   └───local.auth.guard.ts
    │   │   └───local.strategy.ts
    │   │   └───session.serializer.ts
    │   └───users
    │   │   └───users.controller.ts
    │   │   └───users.model.ts
    │   │   └───users.module.ts
    │   │   └───users.service.ts
    │   └───app.controller.ts
    │   └───app.module.ts
    │   └───app.service.ts
    │   └───main.ts

session.serializer.ts

import { Injectable } from "@nestjs/common"
import { PassportSerializer } from "@nestjs/passport"

@Injectable()
export class SessionSerializer extends PassportSerializer {
  serializeUser(user: any, done: (err: Error, user: any) => void): any {
    done(null, user)
  }
  deserializeUser(
    payload: any,
    done: (err: Error, payload: string) => void
  ): any {
    done(null, payload)
  }
}

Retournez à votre auth.module.ts fichier, fournissez le SessionSerializer , et ajoutez le register méthode au PassportModule .

auth.module.ts

import { Module } from "@nestjs/common"
import { PassportModule } from "@nestjs/passport"
import { UsersModule } from "src/users/users.module"
import { AuthService } from "./auth.service"
import { LocalStrategy } from "./local.strategy"
import { SessionSerializer } from "./session.serializer"

@Module({
  imports: [UsersModule, PassportModule.register({ session: true })],
  providers: [AuthService, LocalStrategy, SessionSerializer],
})
export class AuthModule {}

Ajoutez des codes dans le LocalAuthGuard dans le local.auth.guard.ts fichier.

Call the login method in super and pass in the request to trigger the actual login by creating a session. If you want to use sessions, you must remember to trigger the super.login() .

local.auth.guard.ts

    import { ExecutionContext, Injectable } from '@nestjs/common';
    import { AuthGuard } from '@nestjs/passport';
    @Injectable()
    export class LocalAuthGuard extends AuthGuard('local') {
      async canActivate(context: ExecutionContext) {
        const result = (await super.canActivate(context)) as boolean;
        const request = context.switchToHttp().getRequest();
        await super.logIn(request);
        return result;
      }
    }

If you log in now, you will see the session ID stored in a cookie, which is just a key to the session store, and the cookie gets saved in the browser. The cookie is automatically attached to the rest of the request.

Now that the session is working, you can access the protected route; it will return the expected user’s details.

Logout Users

As mentioned earlier, once a user logs out, you destroy all sessions.

To log out a user, go to the users.controller.ts file, add a logout route, and call the req.session.session() méthode. You can return a message notifying that the user’s session has ended.

    import {
      Body,
      Controller,
      Get,
      Post,
      UseGuards,
      Request,
    } from '@nestjs/common';
    import * as bcrypt from 'bcrypt';
    import { AuthenticatedGuard } from 'src/auth/authenticated.guard';
    import { LocalAuthGuard } from 'src/auth/local.auth.guard';
    import { UsersService } from './users.service';
    @Controller('users')
    export class UsersController {
      constructor(private readonly usersService: UsersService) {}
      //signup
      @Post('/signup')
      async addUser(
        @Body('password') userPassword: string,
        @Body('username') userName: string,
      ) {
        const saltOrRounds = 10;
        const hashedPassword = await bcrypt.hash(userPassword, saltOrRounds);
        const result = await this.usersService.insertUser(
          userName,
          hashedPassword,
        );
        return {
          msg: 'User successfully registered',
          userId: result.id,
          userName: result.username
        };
      }
      //Post / Login
      @UseGuards(LocalAuthGuard)
      @Post('/login')
      login(@Request() req): any {
        return {User: req.user,
                msg: 'User logged in'};
      }
       //Get / protected
      @UseGuards(AuthenticatedGuard)
      @Get('/protected')
      getHello(@Request() req): string {
        return req.user;
      }
       //Get / logout
      @Get('/logout')
        logout(@Request() req): any {
          req.session.destroy();
          return { msg: 'The user session has ended' }
        }
    }

So, once you log out, it returns a message notifying you that the user session has ended. The code for this tutorial is hosted here on my Github repository.

Test Your Application

You have successfully implemented user signup, authentication, and protected the route to enable authorized access only.

It’s time to test the application. If everything is in order, your server should be running. Else, restart your server with the following command:

npm run start:dev

Head over to your Postman. And let’s finally test our application.

Sign Up As a User

Log In As a User

Request the Protected Route

User Logout

Alternatively, Implement User Authentication with LoginRadius

LoginRadius provides a variety of registration and authentication services to assist you in better connecting with your consumers.

On any web or mobile application, LoginRadius is the developer-friendly Identity Platform that delivers a complete set of APIs for authentication, identity verification, single sign-on, user management, and account protection capabilities like multi-factor authentication.

To implement LoginRadius in your NestJS application, follow this tutorial:NestJS User Authentication with LoginRadius API.

Conclusion

Toutes nos félicitations! In this tutorial, you've learned how to implement session-based authentication in a NestJS application with the MongoDB database. You've created and authenticated a user and protected your routes from unauthorized access.

You can access the sample code used in this tutorial on GitHub.

Remarque : Session storage is saved by default in 'MemoryStore,' which is not intended for production use. So, while no external datastore is required for development, once in production, a data store such as Redis or another is suggested for stability and performance. You can learn more about session storage here.