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.
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.
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:
Bash1npm 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.
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:
Bash1nest 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:
TypeScript1import { 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}
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.
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:
Bash1nest generate module auth 2nest generate service auth
In the AuthModule, we import the UsersModule, set up Passport.js, and define the authentication service.
TypeScript1import { 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.
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.
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.
TypeScript1import { 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'
toAuthGuard
, 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.
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.
TypeScript1import { 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.
The LocalStrategy is a Passport.js strategy that verifies the username and password when a user attempts to log in.
TypeScript1import { 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.
To maintain user sessions, we need to serialize and deserialize user data. Passport.js provides a mechanism to do this through the SessionSerializer.
TypeScript1import { 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.
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!