logomenu

JWT Authentication in Nest.js

This tutorial will help you create a fully working JWT authenticated server using Nest.js. Then we'll go further by adding refresh tokens to the application so that you can easily refresh your access tokens.

Last Updated: December 5, 2021

nest.js

jwt

This tutorial will help you create a fully working JWT authenticated server using Nest.js. Then we'll go further by adding refresh tokens to the application so that you can easily refresh your access tokens.

To get started first you have to understand how the login flow will work. First when a user logs into our server, they will get an access token and a refresh token.

Initial Login Flow

Then on every request that gets made to the server the client passes in that access token so the server knows who is making the request.

On Every Request

However Access tokens are short lived and usually only live 15 min to 1 hr. Once the access token expires then the client calls the server's refresh endpoint, passing in the refresh token, to get a new access token it can use.

Refresh Token Flow

Let's code this up!


Setting Up the Project

Run in the terminal: nest new nestjs-jwt-auth-tutorial to create the project. Then select your prefered package manager. Once that is done we will create 2 resources: users and auth. To do this run nest g resource in the terminal and type in users as the name. Then select REST API and we don't need CRUD endpoints. Repeat these steps for auth as well.


Add Users

Add a new directory inside of the users directory and name it entities. Inside of here create a new file called: user.entity.ts. Then add the following code to it:

export class User { id: number; name: string; email: string; password: string; }

This will be our base user for the project.

Then inside of user.service.ts create some sample users:

private users: User[] = [ { id: 0, name: 'Bob', email: 'bob@gmail.com', password: 'bobPass', }, { id: 1, name: 'John', email: 'john@gmail.com', password: 'johnPass', }, { id: 2, name: 'Gary', email: 'gary@gmail.com', password: 'garyPass', }, ];

To use this users in the rest of the app a couple helper functions must be created. First a findByEmail function which takes in an email and returns the corresponding user:

findByEmail(email: string): Promise<User | undefined> { const user = this.users.find((user) => user.email === email); if (user) { return Promise.resolve(user); } return undefined; }

Then a findOne function which will take in an id of a user and return the corresponding user:

findOne(id: number): Promise<User | undefined> { const user = this.users.find((user) => user.id === id); if (user) { return Promise.resolve(user); } return undefined; }

Building our Auth service

With our user service out of the way we can move on to implementing JWT. The first thing we need to do is create a RefreshToken type so create new directory called entities and add the file: refresh-token.entity.ts. Inside of this file we will add our Refresh Token class:

import { sign } from 'jsonwebtoken'; class RefreshToken { constructor(init?: Partial<RefreshToken>) { Object.assign(this, init); } id: number; userId: number; userAgent: string; ipAddress: string; sign(): string { return sign({ ...this }, process.env.REFRESH_SECRET); } } export default RefreshToken;

The first thing you'll notice is our constructor. I'm using the Partial type to allow easy instatiation of the Refresh Token type. This is needed because we are including a function in the class. This function will sign our Refresh Token object. In order for this function to work we need the package: jsonwebtoken installed as well as its types. We also need to set up our environment. So create a new file in the root of our project called: .env. Then we can add our environment variables to this file. we will 2 variables: REFRESH_SECRET & ACCESS_SECRET. I use this site to generate my keys and if you scroll down we can set the ACCESS_SECRET to a 152 bit key and REFRESH_SECRET to a 256 bit key. So your .env file should look something like this:

REFRESH_SECRET=your-256-bit-key
ACCESS_SECRET=your-152-bit-key

Now in order for Nest.js to use these environment variables you need to install @nestjs/config so run in your terminal: npm i --save @nestjs/config. The inside your app.module.ts file add the following to your imports: ConfigModule.forRoot(). This will read our .env file to the server's environment. You can read more about Config Module here.

Now in AuthService create a array of refresh tokens like so: private refreshTokens: RefreshToken[] = []; in a real app you will want to store these in a database like MongoDB or Postgresql but to simplify our tutorial it will be just an in-memory array.

Login Function

Now we need to work on our login function. Login will look like:

async login( email: string, password: string, values: { userAgent: string; ipAddress: string }, ): Promise<{ accessToken: string; refreshToken: string } | undefined> { // need to import userService const user = await this.userService.findByEmail(email); if (!user) { return undefined; } // verify your user -- use argon2 for password hashing!! if (user.password !== password) { return undefined; } // need to create this method return this.newRefreshAndAccessToken(user, values); }

The first thing that is missing in login is access to our userService. So change the constructor of AuthService to constructor(private readonly userService: UserService) {} Now we need to import UserService in so in auth.module.ts add UsersModule to the imports array and in users.module.ts and UserService to the exports array.

Next we need to create our newRefreshAndAccessToken method which will look like:

private async newRefreshAndAccessToken( user: User, values: { userAgent: string; ipAddress: string }, ): Promise<{ accessToken: string; refreshToken: string }> { const refreshObject = new RefreshToken({ id: this.refreshTokens.length === 0 ? 0 : this.refreshTokens[this.refreshTokens.length - 1].id + 1, ...values, userId: user.id, }); // add refreshObject to your db in real app this.refreshTokens.push(refreshObject); return { refreshToken: refreshObject.sign(), // sign is imported from jsonwebtoken like import { sign, verify } from 'jsonwebtoken'; accessToken: sign( { userId: user.id, }, process.env.ACCESS_SECRET, { expiresIn: '1h', }, ), }; }

This function is taking in a User and creating a new RefreshToken based off that user. It is then returning the signed version of the refreshobject using our sign helper function and is signing an access token as well that will expire in 1 hour. Our access token only consists of a userId as that is all the info we need in this project; however, you could include however much information as you would want.

Refresh Method

Our Refresh Method will take an encrypted refresh token and return a new valid access token for that user.

async refresh(refreshStr: string): Promise<string | undefined> { // need to create this helper function. const refreshToken = await this.retrieveRefreshToken(refreshStr); if (!refreshToken) { return undefined; } const user = await this.userService.findOne(refreshToken.userId); if (!user) { return undefined; } const accessToken = { userId: refreshToken.userId, }; // sign is imported from jsonwebtoken like import { sign, verify } from 'jsonwebtoken'; return sign(accessToken, process.env.ACCESS_SECRET, { expiresIn: '1h' }); }

Now part of refresh is converting a signed refresh token into an actual RefreshToken object so we create a helper function to help with this:

private retrieveRefreshToken( refreshStr: string, ): Promise<RefreshToken | undefined> { try { // verify is imported from jsonwebtoken like import { sign, verify } from 'jsonwebtoken'; const decoded = verify(refreshStr, process.env.REFRESH_SECRET); if (typeof decoded === 'string') { return undefined; } return Promise.resolve( this.refreshTokens.find((token) => token.id === decoded.id), ); } catch (e) { return undefined; } }

Logout

With that out of the way the last thing we need to implement in AuthService is a logout feature.

async logout(refreshStr): Promise<void> { const refreshToken = await this.retrieveRefreshToken(refreshStr); if (!refreshToken) { return; } // delete refreshtoken from db this.refreshTokens = this.refreshTokens.filter( (refreshToken) => refreshToken.id !== refreshToken.id, ); }

Logout works by taking in a refresh token string and deleting that from our in-memory db. This essentially logs out the user as they can no longer get new access tokens.

Now let's use these in our AuthController.

AuthController

Auth Controller is a simple controller that all it is really doing is mapping our service functions to a REST api.

import { Body, Controller, Delete, Ip, Post, Req } from '@nestjs/common'; import { AuthService } from './auth.service'; import RefreshTokenDto from './dto/refresh-token.dto'; import { LoginDto } from './dto/login.dto'; @Controller('auth') export class AuthController { constructor(private readonly authService: AuthService) {} @Post('login') // we need to create the LoginDto. async login(@Req() request, @Ip() ip: string, @Body() body: LoginDto) { // geting the useragent and ip address from @Req decorator and @Ip decorater imported at the top. return this.authService.login(body.email, body.password, { ipAddress: ip, userAgent: request.headers['user-agent'], }); } @Post('refresh'). // we need to create RefreshTokenDto async refreshToken(@Body() body: RefreshTokenDto) { return this.authService.refresh(body.refreshToken); } @Delete('logout') // we need to create RefreshTokenDto async logout(@Body() body: RefreshTokenDto) { return this.authService.logout(body.refreshToken); } }

Create a new directory inside of auth called dto and add login.dto.ts and refresh-token.dto.ts These data transfer objects will be simple input classes that will be the body for the rest request so for login.dto.ts:

import { IsEmail, IsNotEmpty } from 'class-validator'; export class LoginDto { @IsEmail() email: string; @IsNotEmpty() password: string; }

and refresh-token.dto.ts:

import { IsNotEmpty } from 'class-validator'; class RefreshTokenDto { @IsNotEmpty() refreshToken: string; } export default RefreshTokenDto;

Using the JWTs

In order for our server to validate the access tokens on request we need Passport.js. So you will install passport-jwt and @nestjs/passport using npm or yarn.

With Passport installed we can create our first strategy so inside of auth directory create a new directory called strategies and inside of it create jwt.strategy.ts.

Inside of this file add:

import { PassportStrategy } from '@nestjs/passport'; import { ExtractJwt, Strategy } from 'passport-jwt'; export class JwtStrategy extends PassportStrategy(Strategy) { constructor() { super({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), ignoreExpiration: false, secretOrKey: process.env.ACCESS_SECRET, }); } validate(payload) { return { userId: payload.userId, }; } }

You can see inside of our constructor we are telling Passport to get the access token as from the auth header as a bearer token. Then every strategy in passport has a validate function, which in the case of passport-jwt, we are getting the decoded access token object and we are returning what we want passport to set the user header inside of our Express Request object. So for this we are adding the userId. You will see soon how we will use this. In order for it to be use we must add inside of auth.module.ts add JwtStrategy to the providers array.

But next we need to add the guard that will use this strategy so create a new directory called: guards and add jwt-auth.gguard.ts. Inside of that file add:

import { Injectable, UnauthorizedException } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; import { JsonWebTokenError } from 'jsonwebtoken'; @Injectable() export class JwtAuthGuard extends AuthGuard('jwt') { handleRequest(err: any, user: any, info: any, context: any, status: any) { if (info instanceof JsonWebTokenError) { // if the access token jwt is invalid this is the error we will be returning. throw new UnauthorizedException('Invalid JWT'); } return super.handleRequest(err, user, info, context, status); } }

This is a guard that we will use on our endpoints that we want to require an access token. This guard is essentially a middleware for our server that if the access token is invalid it will stop the request and return an error.


Create Authenticated Endpoint

Now to demonstrate this in use we can create an endpoint inside of our users.controller.ts that requires the access token and will return the corresponding user. So UsersController should look like:

import { Controller, Get, Req, UseGuards } from '@nestjs/common'; import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; import { UserService } from './users.service'; @Controller('users') export class UsersController { constructor(private readonly userService: UserService) {} @UseGuards(JwtAuthGuard) @Get('/me') me(@Req() request) { const userId = request.user.userId; return this.userService.findOne(userId); } }

We are getting userId from the Express Request object that our JwtStrategy created and the userId field is set to the userId set in the validate method.


Conclusion

Now we have a fully functioning JWT Authenticated Rest server using Nest.js. Thank you for reading this article and if you enjoyed it share it on social media, or leave a comment below. As always the code is on Github (link is at the top of article).

Comments

Loading...