Introduction:
In this tutorial, we will dive into the process of setting up a secure authentication system in your NestJS application. We'll be using Passport, a popular authentication library that simplifies the authentication process. Our primary focus will be on setting up local authentication, which involves users logging in with their email and password credentials. We'll also integrate JSON Web Tokens (JWT) to secure our application.
Step 1: Setting Up the JWT Module
Before we start implementing authentication, let's ensure we have a clean setup for the JWT module. Make sure you have the necessary dependencies installed and properly configured.
Step 2: Creating Authentication Strategies
Passport has the concept of strategies in NestJS, where each strategy represents a different type of authentication method. We will start by creating a local strategy for email and password-based logins. This strategy will extend the Passport strategy, so ensure you import it correctly.
In your auth folder, create a new "strategies" folder to organize your authentication strategies. The first strategy we're creating is the local strategy, responsible for handling email and password-based authentication. This strategy should be an injectable class.
Define the Local Strategy:
So let's keep things organized in our auth folder.
Let's create a new strategies folder use this command to create new directory inside our auth folder:
mkdir apps/auth/src/strategies
Then create our first strategy file:
touch apps/auth/src/strategies/local.strategy.ts
This will be the local strategy and this is going to be the strategy that allows us to log in with a user's email and password, which essentially will start off the authentication flow.
Populate the local.strategy.ts
file with the following content
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-local';
import { UsersService } from '../users/users.service';
@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(private userService: UsersService) {
super({
usernameField: 'email', // Use the email field as the username
});
}
async validate(email: string, password: string): Promise<any> {
try {
return await this.userService.verifyUser(email, password); //We will define 'verifyUser' in the user service later; for now, let's just declare it
} catch (error) {
throw new UnauthorizedException(error);
}
}
}
since we are using user service here make sure that in our user.service.module.ts we add the userService in our export array like this exports: [UsersService],
Step 3: Hashing Passwords with Bcrypt
In your user service, it's crucial to hash passwords before storing them in the database. We'll use the Bcrypt library for secure password hashing. Start by installing Bcrypt as a dependency.
Install Bcrypt:
npm install bcryptjs
npm install --save-dev @types/bcryptjs
update your user.service.ts with the following content
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { UserRepository } from './user.repository';
import { CreateUserDTO } from './dto/create-user.dto';
import * as bcryptjs from 'bcryptjs';
@Injectable()
export class UsersService {
constructor(private readonly userRepository: UserRepository) {}
async create(createUserDto: CreateUserDTO) {
return this.userRepository.create({
...createUserDto,
password: await bcryptjs.hash(createUserDto.password, 10),
});
}
async verifyUser(email: string, password: string) {
const user = await this.userRepository.findOne({ email });
const passwordIsValid = await bcryptjs.compare(password, user.password);
if (!passwordIsValid) {
throw new UnauthorizedException('Invalid credentials');
}
return user;
}
}
What we did was hash the password before saving it in the database, and we declared 'verifyUser' because we used it in our 'local.strategy.ts' file.
Step 4: Create the LocalAuthGuard
let's create new folder inside out auth folder by running the following command in your terminal:
mkdir apps/auth/src/guards
inside that folder we will create our local guard file by running the following command on your terminal:
touch apps/auth/src/guards/local.auth-guard.ts
populate local.auth-guard.ts
with the following content
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class LocalAuthGuard extends AuthGuard('local') {}
Code Explanation
export class LocalAuthGuard extends AuthGuard('local') {}: This class, LocalAuthGuard, extends the AuthGuard class and specifies the strategy 'local'. In Nest.js and Passport.js, the strategy name is used to determine which authentication strategy to apply. In this case, it's associated with a local authentication strategy, which involves verifying a user's credentials against a local database
Step 5: Creating a Current User Decorator
To easily access the currently authenticated user in your routes, create a decorator called "CurrentUser." This decorator will extract the user from the request.
run the following command to create a current-user.decorator.ts
file inside our auth folder
touch apps/auth/src/current-user.decorator.ts
populate it with the following content
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { UserDocument } from './users/models/user.schema';
const getCurrentUserByContex = (ctx: ExecutionContext): UserDocument => {
return ctx.switchToHttp().getRequest().user;
};
export const CurrentUser = createParamDecorator(
(_data: unknown, ctx: ExecutionContext) => getCurrentUserByContex(ctx),
);
Code Explanation:
const getCurrentUserByContext = (ctx: ExecutionContext): UserDocument => { ... }: This function takes an ExecutionContext object as a parameter and is used to retrieve the currently authenticated user from the request context. It does so by calling ctx.switchToHttp().getRequest().user, assuming that the user object is stored in the request object under the key "user." The function is typed to return a UserDocument.
export const CurrentUser = createParamDecorator((_data: unknown, ctx: ExecutionContext) => getCurrentUserByContext(ctx));: This code exports a custom parameter decorator called CurrentUser. The createParamDecorator function from Nest.js is used to create the decorator. It takes a callback function that receives two arguments: _data (unused) and ctx, the execution context. Inside this function, it calls getCurrentUserByContext(ctx) to retrieve the current user object, and this user object can be injected into the controller method when @CurrentUser() is used as a parameter.
Step 6 Create Token Payload Interface
On the next step, we will supply our JWT with a token payload, and since we are using TypeScript, it is recommended that we create an interface for our payload. Run the following command to create an 'interface' folder inside our 'auth/src' folder
mkdir apps/auth/src/interface
run this command to create a tokenPayload file inside the interface folder
touch apps/auth/src/interface/token-payload.interface.ts
populate it with the following code
export interface Tokenpayload {
userId: string;
}
Step 6: Implementing the Login Service and routes
Setup Login Service
Now, let's implement the login service in your auth service so that we can use it in our auth controller
update your auth.service.ts
with the following content
import { Injectable } from '@nestjs/common';
import { UserDocument } from './users/models/user.schema';
import { Response } from 'express';
import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
import { Tokenpayload } from './interface/token-payload.interface';
@Injectable()
export class AuthService {
constructor(
private readonly configService: ConfigService,
private readonly jwtService: JwtService,
) {}
async login(user: UserDocument, response: Response) {
const tokenPayload: Tokenpayload = {
userId: user._id.toString(),
};
// Calculate the token expiration time by adding seconds to the current date
const expires = new Date();
expires.setSeconds(
expires.getSeconds() + this.configService.get('JWT_EXPIRATION'),
);
const token = this.jwtService.sign(tokenPayload);
response.cookie('Authentication', token, {
expires: expires,
httpOnly: true,
});
}
}
Code Explanation
async login(user: UserDocument, response: Response) { ... }: This method is used to handle user login. It takes two parameters: user, which is of type UserDocument, and response, which is an Express Response object. In this method:
It creates a **tokenPayload **object, which typically includes user-specific data. In this case, it only contains the user's _id.
It calculates the token expiration time by adding the configured JWT expiration time to the current time.
It signs the JWT using the JwtService provided by Nest.js.
It sets the JWT as a cookie named 'Authentication' in the response, with an expiration date and the httpOnly flag, which is a common practice for securely storing JWTs.
Setup Login Routes
update your auth.controller.ts
with the following content
import { Controller, Post, Res, UseGuards } from '@nestjs/common';
import { AuthService } from './auth.service';
import { LocalAuthGuard } from './guards/local.auth-guard';
import { CurrentUser } from './current-user.decorator';
import { UserDocument } from './users/models/user.schema';
import { Response } from 'express';
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@UseGuards(LocalAuthGuard)
@Post('login')
async login(
@CurrentUser() user: UserDocument,
@Res({ passthrough: true }) response: Response,
) {
await this.authService.login(user, response);
response.send(user);
}
}
Code Explanation
@UseGuards(LocalAuthGuard): This decorator specifies that the login method should use the LocalAuthGuard guard. This guard likely checks the user's credentials before allowing access to the method.
async login(@CurrentUser() user: UserDocument, @Res({ passthrough: true }) response: Response) { ... }: This is the login method, which is responsible for handling user login requests.
It uses the** @CurrentUser** decorator to inject the currently authenticated user (of type UserDocument) and the Express Response object into the method.
In the auth module, add the local strategy as a provider:
update your auth.module.ts with the following content
import { Module } from '@nestjs/common';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { UsersModule } from './users/users.module';
import { LoggerModule } from '@app/common';
import { JwtModule } from '@nestjs/jwt';
import { ConfigModule, ConfigService } from '@nestjs/config';
import * as Joi from 'joi';
import { LocalStrategy } from './strategies/local.strategy';
@Module({
imports: [
UsersModule,
LoggerModule,
ConfigModule.forRoot({
isGlobal: true,
validationSchema: Joi.object({
JWT_SECRET: Joi.string().required(),
JWT_EXPIRATION: Joi.string().required(),
PORT: Joi.number().required(), //we wil setup this env later
}),
}),
JwtModule.registerAsync({
useFactory: (configService: ConfigService) => ({
secret: configService.get<string>('JWT_SECRET'),
signOptions: {
expiresIn: `${configService.get<string>('JWT_EXPIRATION')}s`,
},
}),
inject: [ConfigService],
}),
],
controllers: [AuthController],
providers: [AuthService, LocalStrategy],
})
export class AuthModule {}
Step 7: Testing the Authentication System
Now that your authentication system is in place, test it by creating a user and logging in using Postman or your preferred API testing tool. Ensure you receive a JWT token in the response and that it's properly stored as an HTTP-only cookie.
run docker-compose up
to start our application
Preconditions:
- Your NestJS application with the authentication system is up and running.
- You have an API testing tool like Postman or an equivalent ready for testing.
Steps:
1. User Registration:
Request: Make a POST request to the registration endpoint to create a new user.
URL:http://localhost:3001/users (or your registration endpoint URL)
Headers: Set the Content-Type to application/json.
Body: Include a JSON payload with the user's registration details, including email and password. For example:
{
"email": "user@example.com",
"password": "securePassword123@"
}
Expected Response: You should receive a response indicating the successful creation of the user. Ensure that the password is securely hashed before storing it in the database.
2. User Login:
Request: Make a POST request to the login endpoint to authenticate the newly registered user.
URL: http://localhost:3001/auth/login (or your login endpoint URL)
Headers: Set the Content-Type to application/json.
Body: Include a JSON payload with the user's login credentials (email and password).
{
"email": "user@example.com",
"password": "securePassword123@"
}
Expected Response: You should receive a response with a JWT token, and this token should also be securely stored as an HTTP-only cookie in the response. The response should also include the user's details. The JWT token can be used for subsequent authenticated requests.
Conclusion:
In this tutorial, we've covered the essential steps to set up a secure authentication system in your NestJS application using Passport and JWT. With local email and password-based authentication, you can now build a robust and secure user authentication process for your application. This foundation can be extended to support various authentication methods and user roles as your project evolves.
Top comments (0)