How to implement NestJS Passport Authentication using Local Strategy?
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 thepassport
packages. The third package depends on the strategy we are trying to implement. For the local strategy, we installpassport-local
package. Similarly, for the JWT strategy, we use thepassport-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 likepassport-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.