How to implement NestJS Passport Authentication using Local Strategy?

How to implement NestJS Passport Authentication using Local Strategy?

Photo by FLY:D on Unsplash

In this post, we will learn how to implement NestJS Passport Authentication using the passport local strategy.

Authentication is a key aspect of any production-level application. While there are many ways to handle authentication, one of the more popular ways is to use Passport.

In case you are new to NestJS, I will recommend you to go through this quick Introduction to NestJS. However, if you simply want to learn how to get started with authentication in NestJS, then you can continue ahead with this post.

1 - What is Passport?

Passport is a popular NodeJS library. It comes along with several strategies that can be used for various authentication options. You can think of these strategies as helpers for different approaches to authenticate users. For our example, we will use the local strategy. This strategy makes use of username and password based authentication.

But what exactly does Passport do?

  • It authenticates a user based on credentials. These credentials can be username/password, JWT tokens or any other identity token.
  • Passport can also manage authentication state by issuing something like a JWT token with expiry duration.
  • It attaches information about the user to the Request object so that we can get a handle to the user making the request.

To make things easy to integrate Passport with NestJS, the framework's developers have provided the @nestjs/passport module. This module wraps the entire passport usage pattern into standard NestJS constructs. In other words, it makes life easy for developers to enable authentication for their applications.

2 - Install Packages

To enable the local strategy, we need to install some packages using the below commands:

$ npm install --save @nestjs/passport passport passport-local
$ npm install --save-dev @types/passport-local

Passport provides the passport-local strategy package that implements the username/password authentication. Also, we install the @nestjs/passport package and the base passport package. Finally, the @types/passport-local package helps us with Typescript types during development.

For any strategy, we need to always install the @nestjs/passport and the passport packages. The third package depends on the strategy we are trying to implement. For the local strategy, we install passport-local package. Similarly, for the JWT strategy, we use the passport-jwt package. Even the types package depends on the chosen strategy.

3 - Creating the Modules

As a first step, let’s divide our application into appropriate modules. We will be creating an auth-module and users-module. Basically, the auth-module will contain the logic for authenticating the user and the users-module will contain the user information. It is good practice to structure your application into modules based on their respective functionalities.

With this view, we will create both the modules and services using the Nest CLI commands.

$ nest g module auth
$ nest g service auth
$ nest g module users
$ nest g service users

Next step is to build our passport strategy. Configuring any strategy has two typical steps as below:

  • First step is to specify a set of options that are specific to a particular strategy. For example, the JWT strategy needs a secret to sign the tokens.
  • Second step is to create a verify callback function. In other words, we have to tell passport how to verify whether a user's credentials are actually valid. Passport expects this callback to return the full user object if the validation is successful. Also, it should return null if validation fails. Failure can mean that the user does not exist. For a strategy like passport-local, it can also mean that the password is invalid.

The NestJS passport package helps with the above two steps by providing helper classes.

4 - Implementing the User Service

Let’s first create the user service as below.

users.service.ts

import { Injectable } from '@nestjs/common';

export type User = any;

@Injectable()
export class UsersService {

    private readonly users = [
        {
            userId: 1,
            username: 'John Marston',
            password: 'rdr1',
        },
        {
            userId: 2,
            username: 'Arthur Morgan',
            password: 'rdr2',
        },
    ]

    async findOne(username: string): Promise<User | undefined> {
        return this.users.find(user => user.username === username)
    }
}

For our demo example, the user service will simply contain a list of hardcoded valid users. Here, we have kept two users in our users array.

However, for a real application, we would create a user database and fetch the users from the appropriate table. NestJS can connect with various database solutions (SQL as well as NoSQL). In case you are interested, you can read more about NestJS TypeORM MySQL integration. Or you can also refer to NestJS MongoDB integration.

In the users.service.ts, the findOne() method simply fetches the user from the users array based on the input username.

Also, we need to update the exports array of the UsersModule to configure the UsersService as a Provider.

users.module.ts

import { Module } from '@nestjs/common';
import { UsersService } from './users.service';

@Module({
  providers: [UsersService],
  exports: [UsersService]
})
export class UsersModule {}

5 - Implementing the Auth Service

In the next step, we will implement the AuthService in the AuthModule.

auth.service.ts

import { Injectable } from '@nestjs/common';
import { UsersService } from 'src/users/users.service';

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

    async validateUser(username: string, password: string): Promise<any> {
        const user = await this.usersService.findOne(username);

        if (user && user.password === password) {
            const {password, ...result} = user
            return result
        }
        return null
    }
}

Basically, the job of auth service is to retrieve the user from the UsersService and verify the password. To accomplish this, we create the validateUser() method. We fetch the user by using the UsersService and return the user object as output. However, before returning, we strip the password property out of the object so that it does not accidentally get leaked to the client side.

As a note of caution, please be aware that this is just a demo application. In a real application, we will not be storing the passwords in plain text. Instead, we will use a library such as bcrypt to hash the passwords and then store them. Also, we will compare the hashed version of the incoming password to the stored passwords. This way we will never need to deal with passwords in plain text format.

Finally, we update the AuthModule to import the UsersModule. Without this step, we will not be able to use the UsersService in the AuthModule.

auth.module.ts

import { Module } from '@nestjs/common';
import { UsersModule } from 'src/users/users.module';
import { AuthService } from './auth.service';

@Module({
  imports: [UsersModule],
  providers: [AuthService]
})
export class AuthModule {}

6 - Implement Passport Local Strategy

This is a key step in implementing the NestJS Passport Authentication.

Basically, we need to implement the passport local strategy. To do so, we create a file named local.strategy.ts in the auth folder.

local.strategy.ts

import { Injectable, UnauthorizedException } from "@nestjs/common";
import { PassportStrategy } from "@nestjs/passport";
import { Strategy } from "passport-local";
import { AuthService } from "./auth.service";

@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
    constructor(private authService: AuthService) {
        super()
    }

    async validate(username: string, password: string): Promise<any> {
        const user = await this.authService.validateUser(username, password);

        if (!user) {
            throw new UnauthorizedException();
        }

        return user;
    }
}

Basically, we create a class LocalStrategy that extends the PassportStrategy class. We also pass the attribute Strategy in the class definition. Note here that the Strategy is imported from passport-local and not passport package.

In the constructor, we simply call the super() method. The local strategy only expects the username and password fields and therefore, any other configuration options are not needed. However, if needed, we can pass extra properties while calling super().

Next, we implement the validate() method as part of the PassportStrategy class definition. For every strategy, Passport will call the verify() function. In NestJS, this function is implemented with the validate() method. Based on the strategy, it expects some arguments. For example, in the local strategy, it expects the username and password attributes.

From a logical point of view, this method is quite simple. It simply calls the validateUser() method from the Auth service. If a valid user is found, it returns the same as output. Otherwise, it throws an exception. The work of determining whether a user is valid is done within the AuthService.

Lastly, we need to update the AuthModule as below to use the passport module.

auth.module.ts

import { Module } from '@nestjs/common';
import { PassportModule } from '@nestjs/passport';
import { UsersModule } from 'src/users/users.module';
import { AuthService } from './auth.service';
import { LocalStrategy } from './local.strategy';

@Module({
  imports: [UsersModule, PassportModule],
  providers: [AuthService, LocalStrategy]
})
export class AuthModule {}

7 - Creating the Login Route

Now, we can create the login route. Basically, this route will be used to authenticate a user.

See below:

app.controller.ts

import { Controller, Post, Request, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Controller()
export class AppController {

  @UseGuards(AuthGuard('local'))
  @Post('login')
  async login(@Request() req) {
    return req.user;
  }
}

As you can see, we use the standard @Controller() decorator. Also, we use @Post() for the login() request handler.

Important thing to note here is the @UseGuards(AuthGuard(‘local’)) decorator. Basically, the AuthGuard is a special guard provided by the @nestjs/passport package. This guard was provisioned when we extended the passport-local strategy.

This built-in guard invokes the Passport strategy when a request is received and starts the entire authentication process. In other words, based on the strategy in the AuthGuard (in this case, local), this guard will retrieve the credentials, run the verify function and finally, create the user property.

The user can now simply authenticate by using the /login route as below:

$ curl -X POST http://localhost:3000/auth/login -d '{"username": "John Marston", "password": "rdr1"}' -H "Content-Type: application/json

In case the user is valid, we receive the user object in response. Else, we receive the HTTP Status 401 or Unauthorized.

8 - Conclusion

With this, we have successfully learnt how to implement NestJS Passport Authentication using the passport local strategy.

We started by installing the required packages and then building the modules for retrieving users and authenticating them. Lastly, we implemented the login route to trigger the passport authentication process.

The code for this post is available on Github .

In case you are also interested in the JWT strategy, please refer to this post about NestJS JWT Authentication.

If you have any comments or queries, please feel free to mention in the comments section below.

Did you find this article valuable?

Support Saurabh Dashora by becoming a sponsor. Any amount is appreciated!