Lesson 3
Session-Based Authentication with Passport.js in NestJS
Introduction to Session-Based Authentication with Passport.js

Welcome back to the Securing and Testing Your MVC NestJS App course! In the previous lesson, we covered session management with Express in NestJS, focusing on tracking user activity and maintaining state across multiple requests. Now, we are building on that foundation by adding user authentication using Passport.js.

This lesson will focus on implementing user authentication with Passport.js, a widely-used library that integrates easily with NestJS. By the end, you’ll have a strong foundation for securing your apps with session-based authentication.

What You'll Learn

In this lesson, we will cover the following key topics:

  • Setting up the Users module and controllers.
  • Implementing authentication with Passport.js.
  • Creating a local strategy for user validation.
  • Adding session-based authentication using Passport.js in NestJS.
  • Building authentication guards and services.

By the end of this lesson, you'll have implemented session-based authentication and learned how to secure your application using Passport.js.

Project Setup and Installing Dependencies

Before we begin, make sure your NestJS project is set up. We will also need to install several dependencies that will allow us to implement authentication using Passport.js and manage user sessions:

Bash
1npm install passport passport-local @nestjs/passport express-session bcrypt @types/express-session

Here’s what each dependency is for:

  • passport: The core Passport.js middleware used to manage authentication.
  • passport-local: A Passport strategy for local username and password authentication.
  • @nestjs/passport: NestJS integration for Passport.
  • express-session: Middleware for managing user sessions.
  • bcrypt: A library for hashing and comparing passwords.
  • @types/express-session: TypeScript definitions for working with Express sessions.

Once these dependencies are installed, we can move on to setting up the authentication-related components.

Creating the Users Module, Service, and Controller

Now, let's create a Users module that will handle user-related operations such as registration, login, and fetching user data. We will use the NestJS CLI to generate the necessary components:

Bash
1nest generate module users 2nest generate service users 3nest generate controller users

This will generate the users module, service, and controller files in your project, setting up the basic structure needed to handle user-related operations such as registration, login, and data retrieval.

Since we’ll be using the same User entity from previous lessons, we won’t go into its implementation again, but here's a quick reminder:

TypeScript
1import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'; 2 3@Entity() 4export class User { 5 @PrimaryGeneratedColumn() 6 id: number; 7 8 @Column({ unique: true }) 9 username: string; 10 11 @Column() 12 password: string; // This will store the hashed password 13}
UsersService and UsersController Overview

We’ll be using UsersService and UsersController to handle user-related operations like registration and login.

  • UsersService will be responsible for interacting with the database, handling user creation, fetching users, and hashing passwords using bcrypt.
  • UsersController will expose the necessary routes for registration and login and will use UsersService to handle user operations.

These will be the foundation for managing users and authentication in our application.

Creating the Authentication Module

Next, we need to create the AuthModule to manage authentication in our application. The AuthModule will contain the authentication logic, including Passport.js setup and session handling.

To generate the AuthModule and service, run the following CLI commands:

Bash
1nest generate module auth 2nest generate service auth

In the AuthModule, we import the UsersModule, set up Passport.js, and define the authentication service.

TypeScript
1import { Module } from '@nestjs/common'; 2import { UsersModule } from '../users/users.module'; 3import { AuthService } from './auth.service'; 4import { PassportModule } from '@nestjs/passport'; 5import { LocalStrategy } from './local.strategy'; 6import { SessionSerializer } from './session.serializer'; 7 8@Module({ 9 imports: [UsersModule, PassportModule], 10 providers: [AuthService, LocalStrategy, SessionSerializer], 11}) 12export class AuthModule {}

The AuthModule imports the UsersModule to access user-related operations and the PassportModule to integrate Passport.js. The providers array includes the AuthService for handling authentication logic, the LocalStrategy for username/password authentication, and the SessionSerializer for handling session data.

Creating Guards for Authentication

Guards determine whether a user is allowed to access certain routes. We’ll implement two key guards: the LocalAuthGuard for handling login authentication, ensuring that users can only log in with valid credentials, and the AuthenticatedGuard, which checks if a user’s session is active, allowing access to protected routes only for authenticated users.

LocalAuthGuard

The LocalAuthGuard uses Passport’s local strategy to authenticate users during login. This guard handles the username/password verification process and logs the user into a session if the credentials are valid.

TypeScript
1import { AuthGuard } from '@nestjs/passport'; 2import { ExecutionContext, Injectable } from '@nestjs/common'; 3 4@Injectable() 5export class LocalAuthGuard extends AuthGuard('local') { 6 async canActivate(context: ExecutionContext) { 7 const result = (await super.canActivate(context)) as boolean; 8 const request = context.switchToHttp().getRequest(); 9 await super.logIn(request); // This ensures the user is logged in and the session is created 10 return result; 11 } 12}

Here’s what’s happening:

  • AuthGuard: The AuthGuard class is a Passport.js feature that we extend to create our own guard. By passing 'local' to AuthGuard, we tell Passport.js to use the local strategy, which handles username and password validation.
  • super.canActivate(context): Calls the built-in method to activate the guard, triggering Passport’s authentication mechanism. The result is a boolean indicating whether the user is authenticated.
  • super.logIn(request): This ensures that the user is logged into the session if the authentication is successful.

Once the LocalAuthGuard is in place, we can apply it to the login route to handle authentication.

AuthenticatedGuard

The AuthenticatedGuard checks whether a user is authenticated by verifying their session data. This guard is used to protect routes that should only be accessible to authenticated users.

TypeScript
1import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; 2 3@Injectable() 4export class AuthenticatedGuard implements CanActivate { 5 async canActivate(context: ExecutionContext) { 6 const request = context.switchToHttp().getRequest(); 7 return request.isAuthenticated(); // Check if the session contains an authenticated user 8 } 9}

Here’s what the AuthenticatedGuard does:

  • isAuthenticated(): This method checks if the user is authenticated based on the session data. If the user is not authenticated, the guard prevents access to the route.

You can apply this guard to any route you want to protect, ensuring only authenticated users can access it.

Adding the Local Strategy

The LocalStrategy is a Passport.js strategy that verifies the username and password when a user attempts to log in.

TypeScript
1import { Strategy } from 'passport-local'; 2import { PassportStrategy } from '@nestjs/passport'; 3import { Injectable, UnauthorizedException } from '@nestjs/common'; 4import { AuthService } from './auth.service'; 5import { User } from '../users/user.entity'; 6 7@Injectable() 8export class LocalStrategy extends PassportStrategy(Strategy) { 9 constructor(private authService: AuthService) { 10 super(); // Calls the parent Strategy constructor 11 } 12 13 async validate(username: string, password: string): Promise<User> { 14 const user = await this.authService.validateUser(username, password); 15 if (!user) { 16 throw new UnauthorizedException('Invalid credentials'); 17 } 18 return user; 19 } 20}

The validate method in the LocalStrategy checks if the provided username and password are valid. If the credentials are incorrect, it throws an UnauthorizedException. If valid, the user object is returned.

Session Serializer

To maintain user sessions, we need to serialize and deserialize user data. Passport.js provides a mechanism to do this through the SessionSerializer.

TypeScript
1import { Injectable } from '@nestjs/common'; 2import { PassportSerializer } from '@nestjs/passport'; 3 4@Injectable() 5export class SessionSerializer extends PassportSerializer { 6 serializeUser(user: any, done: (err: Error, user: any) => void): any { 7 done(null, user); 8 } 9 10 deserializeUser(payload: any, done: (err: Error, payload: string) => void): any { 11 done(null, payload); 12 } 13}

The serializeUser method stores user information in the session, while the deserializeUser method retrieves the session data when the user makes subsequent requests. This ensures that the user remains logged in across multiple requests.

Why Session-Based Authentication Matters

Session-based authentication is crucial for several reasons:

  • Security: It protects sensitive routes in your application by ensuring that only authenticated users can access them.
  • User Experience: Users can log in once and stay logged in across multiple requests without re-entering their credentials.
  • Scalability: It allows your application to scale by managing user sessions efficiently, especially in applications with personalized features.

Now that you've successfully implemented session-based authentication using Passport.js, you've built a solid foundation for securing your applications. Great job on getting through this lesson! You're well on your way to mastering authentication in NestJS.

Let’s move to the practice section and put everything you’ve learned into action!

Enjoy this lesson? Now it's time to practice with Cosmo!
Practice is how you turn knowledge into actual skills.