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.
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.
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.
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...