DEV Community

Jayvee Ramos
Jayvee Ramos

Posted on

9. Setting up local authentication

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
Enter fullscreen mode Exit fullscreen mode

Then create our first strategy file:

touch apps/auth/src/strategies/local.strategy.ts
Enter fullscreen mode Exit fullscreen mode

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);
    }
  }
}


Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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;
  }
}

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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') {}

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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),
);

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

run this command to create a tokenPayload file inside the interface folder

touch apps/auth/src/interface/token-payload.interface.ts
Enter fullscreen mode Exit fullscreen mode

populate it with the following code

export interface Tokenpayload {
  userId: string;
}

Enter fullscreen mode Exit fullscreen mode

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,
    });
  }
}


Enter fullscreen mode Exit fullscreen mode

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);
  }
}

Enter fullscreen mode Exit fullscreen mode

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 {}

Enter fullscreen mode Exit fullscreen mode

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:

  1. Your NestJS application with the authentication system is up and running.
  2. 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@"
}

Enter fullscreen mode Exit fullscreen mode

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@"
}

Enter fullscreen mode Exit fullscreen mode

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)