In this post, we will learn how to implement NestJS JWT Authentication using Passport JWT Strategy.
JWT stands for JSON Web Tokens. Basically, these tokens are issued by the server after user authentication and can be used for further requests as long as the token is valid. Using JWT effectively can make our applications stateless from an authentication point of view.
We will be using the NestJS JWT Authentication using Local Strategy as the base for this application. So I will recommend you to go through that post and then continue with this one.
1 – The Requirements
Below are the two high-level requirements for our NestJS JWT Authentication demo.
- Let the users authenticate with a username/password. Upon authentication, return a JWT. This JWT can be used for subsequent calls to the application.
- Next, we will implement an API route that can be accessed only with the help of a valid JWT.
See below illustration that explains the concept.
2 – Installation of Packages
To get started with NestJS JWT, we need to install the below packages.
$ npm install --save @nestjs/jwt passport-jwt
$ npm install --save-dev @types/passport-jwt
The @nestjs/jwt package helps with JWT manipulation. The passport-jwt package implements the JWT strategy. Also, the @types/passport-jwt package provides the type definitions to make development easy.
3 – Generating the JWT
The first step is for us to be able to generate a JWT and return it as response of the /login route of our application.
To achieve the same, we will first create a loginWithCredentials() method in the auth service. See below:
import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { UsersService } from 'src/users/users.service';
@Injectable()
export class AuthService {
constructor(private usersService: UsersService, private jwtTokenService: JwtService){}
async validateUserCredentials(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;
}
async loginWithCredentials(user: any) {
const payload = { username: user.username, sub: user.userId };
return {
access_token: this.jwtTokenService.sign(payload),
};
}
}
The loginWithCredentials() method accepts the user object. Using this object, it creates a payload and passes the same to the sign() function of the JWTService instance. Basically, the sign() function generates a JWT. We return the same as output of the loginWithCredentials() function.
Point to note here is that we use the property name sub for the userId. This is based on JWT standards. Also, note that we injected the JWTService in the constructor. Only then we were able to use it for calling the sign() function. You can learn more about services in this detailed post on NestJS Providers.
Next, we need to make a couple of configurations. Firstly, signing a JWT needs a secret key. This key is extremely important because we will use it for both signing and verifying purposes. We setup the key as below:
export const jwtConstants = {
secret: 'secretKey',
}
INFO
We should not expose the secret key publicly. This is a demo application and hence we are going for simplicity. In a real application, this key should be protected by using better mechanisms such as environment variables, secret vault or configuration services based on proper access control.
The second step configuration is inside the AuthModule.
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { UsersModule } from 'src/users/users.module';
import { AuthService } from './auth.service';
import { jwtConstants } from './constants';
import { LocalStrategy } from './local.strategy';
@Module({
imports: [
UsersModule,
PassportModule,
JwtModule.register({
secret: jwtConstants.secret,
signOptions: {expiresIn: '60s'}
})],
providers: [AuthService, LocalStrategy],
exports: [AuthService]
})
export class AuthModule {}
Here, we are basically registering the JwtModule. While doing so, we specify the secret key and the signOptions. The expiresIn property means that the JWT issued by our server will have an expiration time of 60 seconds. In other words, after 60 seconds, the token will become invalid and a fresh token will be needed.
4 – Updating the Login Route
At this point, we have the necessary setup to sign and issue a JWT. However, you might be wondering how the loginWithCredentials() method in the AuthService will receive the user object.
That is taken care of in the below code where we implement a route for login. If you wish to know more about implementing controllers, please refer to this detailed post on NestJS Controllers.
import { Controller, Get, Post, Request, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { AuthService } from './auth/auth.service';
@Controller()
export class AppController {
constructor(private authService: AuthService){}
@UseGuards(AuthGuard('local'))
@Post('login')
async login(@Request() req) {
return this.authService.loginWithCredentials(req.user);
}
}
This is where the previous post becomes useful. If you remember, we used the built-in AuthGuard to trigger our passport authentication process. As part of the process, the incoming username and password are passed on the validate() method in the auth service. If it is a valid user, the validate() method returns the user object after stripping the password.
Only in case we have a valid user, the request handler/controller loginWithCredentials() method gets invoked. At that point, the req parameter will contain the user property after authentication. We simply pass the user property to the loginWithCredentials() function of the auth service. The auth service returns the signed JWT and the request handler returns the same to the client. In this way, the cycle is completed and the client has a valid JWT.
At this point, we can start our application and hit the /login route with a valid username and password. If everything is fine, we will receive the JWT token as the access token.
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IkpvaG4gTWFyc3RvbiIsInN1YiI6MSwiaWF0IjoxNjM5OTY5OTI3LCJleHAiOjE2Mzk5Njk5ODd9.X8jNA14s-zfpOYEwwVzKHOHJ_Q1ULpTsgO3OFuHEfMU"
}
Basically, we can confidently say that our authentication process is working.
5 – Implementing the JWT Passport Strategy
Now, we can implement the JWT Passport Strategy. We need to do this to be able to address our second requirement of protecting endpoints using JWT.
We will create a new file jwt.strategy.ts within the auth folder and place the below code.
import { PassportStrategy } from "@nestjs/passport";
import { ExtractJwt, Strategy } from "passport-jwt";
import { jwtConstants } from './constants';
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: jwtConstants.secret
})
}
async validate(payload: any) {
return {userId: payload.sub, username: payload.username}
}
}
Basically, here we extend the PassportStrategy class and specify that we want to extend the Strategy from passport-jwt package.
Next, we initialize the strategy by passing some parameters in the super() call. Below are the parameters in detail:
- jwtFromRequest – This parameter specifies the method using which we will extract the JWT from the request. Basically, we use the fromAuthHeaderAsBearerToken() method from the ExtractJwt package. This is because it is standard practice to pass the JWT token as the bearer token in the authorization header while making API requests.
- ignoreExpiration – We set this property as false. This basically means that we want to block requests with expired tokens. If a call is made with an expired token, our application will return 401 or Unauthorized response.
- secretOrKey – This is basically the secret key from the constants file. Ideally, this should pem-encoded publish key. Also, it should not be exposed publicly.
Next, we implement the validate() function. In the function, we simply return the user object. This might seem confusing as we are not processing anything in the function. This is because for the JWT strategy, passport first verifies the signature of the JWT and decodes the JSON object. Only then does it actually call the validate() function and passes the decoded JSON as payload. Basically, this ensures that if the validate() function is called, it means the JWT is also valid.
In the validate() function, we simply return the user object. Passport attaches this user object to the Request object.
6 – Update the Auth Module
Finally, we add the JwtStrategy as a provider to the Auth module. See below example:
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { UsersModule } from 'src/users/users.module';
import { AuthService } from './auth.service';
import { jwtConstants } from './constants';
import { LocalStrategy } from './local.strategy';
import { JwtStrategy } from './jwt.strategy';
@Module({
imports: [
UsersModule,
PassportModule,
JwtModule.register({
secret: jwtConstants.secret,
signOptions: {expiresIn: '60s'}
})],
providers: [AuthService, LocalStrategy, JwtStrategy],
exports: [AuthService]
})
export class AuthModule {}
The above change enables the use of JwtStrategy in our application context.
7 – Creating the Auth Guard
Finally, we create an Auth Guard that uses the JWT strategy. Our JwtAuthGuard extends the AuthGuard provided by the @nestjs/passport package. We reference the guard by its default name i.e. jwt. See below:
import { Injectable } from "@nestjs/common";
import { AuthGuard } from "@nestjs/passport";
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}
Now, we can simply use this guard in one of the routes that we want to protect.
import { Controller, Get, Post, Request, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { AuthService } from './auth/auth.service';
import { JwtAuthGuard } from './auth/jwt-auth.guard';
@Controller()
export class AppController {
constructor(private authService: AuthService){}
@UseGuards(AuthGuard('local'))
@Post('login')
async login(@Request() req) {
return this.authService.loginWithCredentials(req.user);
}
@UseGuards(JwtAuthGuard)
@Get('user-info')
getUserInfo(@Request() req) {
return req.user
}
}
Here, the request handler getUserInfo() uses the JwtAuthGuard. Basically, this means that unless there is a valid JWT in the Authorization header of the incoming HTTP request, the endpoint will not provide a valid response.
We can now test our application by first generating a token using the /login route.
$ curl -X POST http://localhost:3000/login -d '{"username": "John Marston", "password": "rdr1"}' -H "Content-Type: application/json"
Once we obtain the token, we can call the /user-info route. See below example:
curl http://localhost:3000/user-info -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IkpvaG4gTWFyc3RvbiIsInN1YiI6MSwiaWF0IjoxNjM5OTY1NTMzLCJleHAiOjE2Mzk5NjU1OTN9..."
Also, if we wait for 60 seconds and then use the same token, we get 401 error.
Conclusion
With this, we have successfully learnt how to implement NestJS JWT Authentication using the JWT passport strategy.
The code for this post is available on Github.
If you have any comments or queries, please mention in the comments section below.
5 Comments
duran · June 13, 2022 at 2:12 pm
Salut, comment personnaliser l’erreur 401 quand le token est invalide ?
Anonymous · June 13, 2022 at 2:13 pm
Hi, how to customize the 401 error when the token is invalid?
Saurabh Dashora · June 18, 2022 at 6:23 am
Check into using something like an Exception Filter.
shezy · June 17, 2022 at 5:26 am
how i can logout i have problem while logging out. I tried so many time with different ways but not i get what i want
Saurabh Dashora · June 18, 2022 at 6:32 am
Hi, generally token-based authentication is stateless. As such, the server need not keep track of which users are authenticated. This means that we need not do anything specifically to logout the user. The token should be removed on the client side so that it is not part of the requests. Also, you can keep the refresh time of the token very short if you want to keep the session time less. At least, this is the preferred way.
In case you still want to keep track of logged out users, you need to probably think about maintaining a list of restricted tokens in database.