DEV Community

Cover image for A Step-by-Step Guide to Implement JWT Authentication in NestJS using Passport
camillefauchier
camillefauchier

Posted on

A Step-by-Step Guide to Implement JWT Authentication in NestJS using Passport

In the domain of computer security, authentication and authorization are fundamental processes. Authentication, the initial step, validates a user's identity through methods like passwords or biometrics. Once authenticated, authorization comes into play, determining the user's permissions and access rights. It's crucial to note that authentication precedes authorization, establishing the user's identity before enforcing access control measures.

The purpose of this article is to provide a step-by-step guide for implementing authentication system in a NestJS project using the Passport middleware module.

Our authentication system will embody the key attributes of an exemplary system: it will be secure, easily extensible, and simple, as every good authentication system should be.

If you want to look at the full code you can check out my repo on Github.

Dependencies installation

  • passport (^0.7.0) is an authentication middleware for Node.js, widely used and extensible.
  • passport-local ( ^1.0.0) is a Passport strategy for authentication with an email and password.
  • passport-jwt (^4.0.1)is a Passport strategy for authentication with a JSON Web Token (JWT).
  • @nestjs/passport (^10.0.3) is a Passport integration for NestJS.
  • @nestjs/jwt (^10.2.0) is used to handle JWT tokens in NestJS. JWT (JSON Web Tokens) is a compact and safe way to transmit information between parties as a JSON object. this information can be verified and trusted because it is digitally signed.
  • bcrypt (5.1.1)is a library for hashing passwords.
npm install @nestjs/passport passport passport-local passport-jwt @nestjs/jwt bcrypt
Enter fullscreen mode Exit fullscreen mode

Why you should use Passport Module to implement your authentication service
For implementing authentication in your NestJs App I highly recommend relying on the Passport module. It enhances security through its robust strategies like JWT and OAuth, aligning with industry best practices. Among other things, it supports a plug-and-play architecture, enabling the addition of new authentication methods or the replacement of existing ones without major code modifications. This means you can effortlessly integrate custom authentication strategies, ensuring scalability and adaptability for your app's future needs. In other words, Passport ensures a smooth developer experience, making it easy for you to understand and implement authentication mechanisms.

User Module

Define a model (e.g., a class or an interface) for your users, including necessary fields such as email, password, etc. The user service responsible for all interactions with the database regarding users includes methods to create, find, update, and delete users. However, notice that the signup method must be handled by the authentication service.

Authentication Module

Your authentication module should be (at least) responsible for the following crucial tasks:

  1. User Identity verification
  2. Password Hashing and Storage
  3. Token Generation and Validation

Authentication Service

Create an AuthService service that will inject necessary services, such as UsersService for interacting with the user database and JwtService for handling JWT tokens.

AuthService Methods:

  • validateUser: Takes a username and password as input and checks if the user exists and if the password is correct.
  • login: Takes user data as input and generates a JWT token
  • register: Checks if a user with the provided email already exists.If a user with the provided email already exists, throws a BadRequestException, if not, hash and salt the user's password using bcrypt, store the new user in the database and log the user in.
//auth.service.ts
import { BadRequestException, Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import * as bcrypt from 'bcrypt';
import { User } from 'src/users/users.entity';
import { AccessToken } from './types/AccessToken';
import { UsersService } from 'src/users/users.service';
import { RegisterRequestDto } from './dtos/register-request.dto';

@Injectable()
export class AuthService {
  constructor(
    private usersService: UsersService,
    private jwtService: JwtService,
  ) {}

  async validateUser(email: string, password: string): Promise<User> {
    const user: User = await this.usersService.findOneByEmail(email);
    if (!user) {
      throw new BadRequestException('User not found');
    }
    const isMatch: boolean = bcrypt.compareSync(password, user.password);
    if (!isMatch) {
      throw new BadRequestException('Password does not match');
    }
    return user;
  }

  async login(user: User): Promise<AccessToken> {
    const payload = { email: user.email, id: user.id };
    return { access_token: this.jwtService.sign(payload) };
  }

  async register(user: RegisterRequestDto): Promise<AccessToken> {
    const existingUser = this.usersService.findOneByEmail(user.email);
    if (existingUser) {
      throw new BadRequestException('email already exists');
    }
    const hashedPassword = await bcrypt.hash(user.password, 10);
    const newUser: User = { ...user, password: hashedPassword };
    await this.usersService.create(newUser);
    return this.login(newUser);
  }
}
Enter fullscreen mode Exit fullscreen mode

Why is hashing and salting passwords mandatory?
A salt is simply a random data used as an additional input to the hashing function to safeguard your password. The random string from the salt makes the hash unpredictable.
A password hash involves converting the password into an alphanumeric string using specialized algorithms.
Hashing and salting are irreversible and ensure that even if someone gains access to the hashed passwords, they will not be able to decrypt them to recover the original passwords.
Hystorically bcrypt is recognized as the best hashing algorithm. However, in terms of robustness against all the new cryptographic attacks targeting hashing algorithms, the current clear winner is argon2. However, since the “youth" (2015) of this algorithm, I chose to use bcrypt

Passport Configuration

Create a Local Strategy for authentication with username and password.

You need to create a LocalStrategy that extends PassportStrategy.

This authentication method is primarily used for authenticating users based on local credentials such as usernames and passwords. By default the strategy looks for a username field, in the code below we override usernameField to look for an email field since we want to login with an email.

  • validate method : Requires the server to check the provided username and password against the authentication service.
//local.strategy.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-local';
import { AuthenticationService } from '../authentication.service';
import { User } from '../../app/users/users.entity';

@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
  constructor(private authService: AuthenticationService) {
    super({
      usernameField: 'email',
    });
  }

  async validate(email: string, password: string): Promise<User> {
    const user = await this.authService.validateUser(email, password);
    if (!user) {
      throw new UnauthorizedException();
    }
    return user;
  }
}
Enter fullscreen mode Exit fullscreen mode

Create a JWT Strategy to validate JWT tokens

The JwtStrategy validates the token sent by the user. It extracts the user ID from the token and looks it up in the database.

This authentication method is used for authenticating users based on the presence and validity of a JSON Web Token (JWT). It extracts the user ID from the token and looks it up in the database.

  • validate method: Involves checking the digital signature of the JWT to ensure its integrity and validating claims to determine the user's identity and permissions. The validate method receives the decoded data from the JWT token and returns user data if the token is valid. You can add additional logic, e.g., to fetch additional user information from the database.
//jwt.strategy.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-local';
import { AuthenticationService } from '../authentication.service';
import { User } from '../../app/users/users.entity';

@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
  constructor(private authService: AuthenticationService) {
    super({
      usernameField: 'email',
    });
  }

  async validate(email: string, password: string): Promise<User> {
    const user = await this.authService.validateUser(email, password);
    if (!user) {
      throw new UnauthorizedException();
    }
    return user;
  }
}
Enter fullscreen mode Exit fullscreen mode

Authentication Controller

Create endpoints for signup (register) and login (login) that both return JWT tokens. For the login endpoint, the@UseGuards decorator is applied to the login method, indicating that the AuthGuard named 'local' should be used, that will trigger the LocalStrategy previously implemented.

// auth.controller.ts
import {
  BadRequestException,
  Body,
  Controller,
  Post,
  Request,
  UseGuards,
} from '@nestjs/common';
import { AuthService } from './auth.service';

import { AuthGuard } from '@nestjs/passport';
import { RegisterRequestDto } from './dtos/register-request.dto';
import { LoginResponseDTO } from './dtos/login-response.dto';
import { RegisterResponseDTO } from './dtos/register-response.dto';

@Controller('auth')
export class AuthController {
  constructor(private authService: AuthService) {}

  @UseGuards(AuthGuard('local'))
  @Post('login')
  async login(@Request() req): Promise<LoginResponseDTO | BadRequestException> {
    return this.authService.login(req.user);
  }

  @Post('register')
  async register(
    @Body() registerBody: RegisterRequestDto,
  ): Promise<RegisterResponseDTO | BadRequestException> {
    return await this.authService.register(registerBody);
  }
}
Enter fullscreen mode Exit fullscreen mode

Passing the Access Token to the client
In this tutorial we pass the access token to the client in the response body for simplicity reasons, but it can also be done with a cookie. Since cookies are automatically included in every HTTP request, it simplifies the process of including the access token without explicit action from the client.
You can read this article Cookie-Based Authentication vs Token-Based Authentication to choose the most adapted method to your case.

Authentication Module Configuration

Your AuthModule should import the JwtModule to handle JSON Web Token (JWT) generation and verification.

The registerAsync method allows for asynchronous configuration, facilitating the use of the ConfigService to retrieve the JWT secret from environment variables stored in a .env file.

Storing sensitive configurations in a .env file as environment variables is preferred because it enhances security and avoids accidental exposure in version control systems (⚠️ don’t forget to add it in the .gitignore ). This practice facilitates easy configuration management across different environments while preserving confidentiality and minimizing the risk of inadvertent information disclosure.

// auth.module.ts
import { Module } from '@nestjs/common';
import { UsersModule } from 'src/users/users.module';
import { AuthService } from './auth.service';
import { JwtModule } from '@nestjs/jwt';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { LocalStrategy } from './strategy/local.strategy';
import { AuthController } from './auth.controller';
import { JwtStrategy } from './strategy/jwt.strategy';

@Module({
  imports: [
    UsersModule,
    JwtModule.registerAsync({
      imports: [ConfigModule],
      useFactory: async (configService: ConfigService) => ({
        secret: configService.get<string>('JWT_SECRET'),
        signOptions: {
          expiresIn: parseInt(
            configService.getOrThrow<string>(
              'ACCESS_TOKEN_VALIDITY_DURATION_IN_SEC',
            ),
          ),
        },
      }),
      inject: [ConfigService],
    }),
  ],
  controllers: [AuthController],
  providers: [AuthService, LocalStrategy, JwtStrategy],
  exports: [AuthService, JwtModule],
})
export class AuthModule {}
Enter fullscreen mode Exit fullscreen mode

Routes security: implementing a whitelisting strategy

Why should you use a whitelisting strategy ?
To ensure the security of your app, the best practice is to choose a whitelisting strategy. Whitelisting, involves explicitly allowing only approved elements or actions, providing precise control over permissions in a system. It is considered a better practice than blacklisting as it reduces the attack surface by permitting only known and necessary entities but also minimizes developers' errors resulting from oversights or thoughtlessness.

In our context, to implement whitelisting you basically just have to create a guard for JWT-based authentication, and a decorator to designate public routes exempt from JWT authentication. This strategy offers a granular and flexible approach to securing specific endpoints.

Create a guard and a decorator

First, create the following decorator:

//public.decorator.ts 
import { SetMetadata } from '@nestjs/common';

export const Public = () => SetMetadata('isPublic', true);
Enter fullscreen mode Exit fullscreen mode

Then you can add the following JwtGuard class:

//jwt.guard.ts
import { ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';

@Injectable()
export class JwtGuard extends AuthGuard('jwt') {
  constructor(private reflector: Reflector) {
    super();
  }
  canActivate(
    context: ExecutionContext,
  ): Promise<boolean> | boolean | Observable<boolean> {
    const isPublic = this.reflector.getAllAndOverride('isPublic', [
      context.getHandler(),
      context.getClass(),
    ]);
    if (isPublic) return true;
    return super.canActivate(context);
  }
}
Enter fullscreen mode Exit fullscreen mode
  • Class constructor: takes a single parameter - an instance of the Reflector class. The Reflector class is part of the core NestJS library and is used to retrieve metadata associated with classes, methods, and their decorators.
  • canActivate Method: The canActivate method is a crucial part of the JwtGuard class. It is invoked during the request lifecycle to determine whether the current request should be allowed or denied based on the provided JWT.
    • The first step is to check if the route or endpoint is marked as public using the @Public decorator previously created.
    • The Reflector is used to retrieve metadata labeled as 'isPublic' from the handler and class associated with the current execution context.
    • If the route is marked as public, the method returns true, indicating that no further authentication checks are needed.
    • If the route is not marked as public, the super.canActivate(context) call invokes the corresponding method in the parent class (AuthGuard). This method performs the actual JWT validation and authentication checks.

Configure the AppModule

Within the providers array, the provider for the JWT guard is registered using the APP_GUARD token. This provider specifies the use of the JwtGuard class as the global guard for the entire application.

//app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UsersModule } from './users/users.module';
import { ConfigModule } from '@nestjs/config';
import { AuthModule } from './auth/auth.module';
import { JwtStrategy } from './auth/strategy/jwt.strategy';
import { APP_GUARD } from '@nestjs/core';
import { JwtGuard } from './auth/guards/jwt.guard';

@Module({
  imports: [
    ConfigModule.forRoot({ isGlobal: true }),
        ...
    UsersModule,
    AuthModule,
  ],
  controllers: [AppController],
  providers: [
    AppService,
    {
      provide: APP_GUARD,
      useClass: JwtGuard,
    },
    JwtStrategy,
  ],
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

Now if the routes are not marked with @Public(), every incoming request undergoes JWT authentication.

For example, in the GET endpoint below, the global guard validates and decodes the JWT access token and attaches the user information to the request object, making it accessible in the controller methods.

//app.controller.ts
import { Controller, Get, Request } from '@nestjs/common';
import { AppService } from './app.service';
import { AccessTokenPayload } from './auth/types/AccessTokenPayload';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  async getHello(@Request() req): Promise<string> {
    const accessTokenPayload: AccessTokenPayload =
      req.user as AccessTokenPayload;
    return await this.appService.getHello(accessTokenPayload.userId);
  }
Enter fullscreen mode Exit fullscreen mode

Specify public routes

You have now the option to apply the @Public()decorator to methods or classes that don't require JWT authentication. In our scenario, we apply this decorator to the AuthController.

//auth.controller.ts
@Public()
@Controller('auth')
export class AuthController {
  //...
}
Enter fullscreen mode Exit fullscreen mode

Create custom decorators

In order to make your code more readable and transparent, you can create your own custom decorator and reuse it across all of your controllers. It’s a very good practice to avoid code duplication.

Depending on the specific requirements of your project, (e.g. Role-based access control), you can create numerous and various decorators and guards to address diverse authorization scenarios. For this article we will only create a  @User() decorator that extracts the user information from the request object in the NestJS execution context.

// user.decorator.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common';

export const User = createParamDecorator(
  (data: unknown, ctx: ExecutionContext) => {
    const request = ctx.switchToHttp().getRequest();
    return request.user;
  },
);
Enter fullscreen mode Exit fullscreen mode

Then you just have to pass it to your controller to retrieve the user information encapsulated in your request.

//app.controller.ts
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
import { User } from './auth/decorators/user.decorator';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  async getHello(@User() user): Promise<string> {
    return await this.appService.getHello(user.id);
  }
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

Dependancies schema of the authentication system

In conclusion, crafting a secure and efficient authentication system in a NestJS application is a meticulous but easy process. We have integrated Passport to handle various authentication strategies, used JWT tokens to enhance security, and the global guards to ensure consistent authentication across the entire project. By prioritizing security, simplicity, and extensibility in our implementation, we have established a foundation that not only safeguards user data but also allows for seamless integration of future features.

By following this step-by-step guide, you can establish a robust authentication system in your NestJS applications, promoting secure user interactions and data protection. The provided code snippets serve as a foundation for building upon and adapting to specific project requirements (Role-based access control, OAuth).

However, I did not get into refresh token even though they are an essential component of secure authentication systems, particularly in scenarios where long-lived sessions are required. A deep dive into implementing refresh tokens in a NestJS context could be valuable for you if you aim to build robust and secure authentication systems.

Sources

Top comments (0)