This guide will help you learn and build a Nest.js API that shows how to handle Nest.js JWT authentication and secure endpoints to authenticate users. JSON Web Tokens (JWT) are used to represent a piece of protected information transmitting pipeline between a server and client in HTTP protocol. An HTTP request must contain all the information needed to secure the interaction. This is where JWT comes into play. JWT authenticates any incoming requests and provides additional security layers to your API. Let’s dive in and implement JWT authentication in a Nest.js API that follows best practices for API security.
Prerequisites
To follow along with this article, it is helpful to have the following:
- Node.js installed on your computer.
- MongoDB installed on your computer.
- Postman installed on your computer.
- Prior experience working with Nest.js.
Setting up the Nest.js project
To set up the project, create a directory where you want your app to live. Then open a terminal that points to this directory. The first step is to install the Nest.js CLI. If you do not have it installed, run the following command:
npm i -g @nestjs/cli
Check if the CLI has been successfully installed by running this command that checks the currently installed Nest.js CLI on your computer:
nest --version
Once installed, bootstrap the Nest.js project by running the following command:
nest new jwt-api
Once done, proceed to the newly created jwt-api
directory:
cd jwt-api
Then install all the dependencies that Nest.js has bootstrapped for you using the npm install command:
npm install
For our project, we will need to install the following dependencies:
To install all the above dependencies, run this command inside the jwt-api
directory:
npm i --save @nestjs/mongoose @nestjs/passport bcrypt jsonwebtoken passport passport-jwt
Now it is time to handle authentication.
Model Definition for User JWT Authentication Details
Create a models
folder on the project src
directory. Inside the models
folder, create a user.schema.ts
file. Define the schema on the user.schema.ts
file as follows:
import * as mongoose from 'mongoose'; import * as bcrypt from 'bcrypt'; export const userSchema = new mongoose.Schema({ // user schema name: String, email: String, password: { type: String, select: false, required:true }, createdAt: { type:Date, default:Date.now } }); // Pre-save hook to hash the password userSchema.pre('save', async function(next:any){ try{ // check if it is modified if(!this.isModified('password')){ return next(); } // hash the password const hashedPassword = await bcrypt.hash(this.password, 10); // set to the newly hashed password this.password = hashedPassword; // call the nest operation return next(); }catch(error){ return next(error); } })
User Type definition
Create a types
folder inside the project’s src
directory. Inside it, create a user.ts
file. In user.ts
, define the user as follows:
import {Document} from 'mongoose'; export interface User extends Document { name: string; email: string; readonly password: string; createdAt: Date; }
Defining the User Input Interfaces
Create an auth
folder inside the project’s src
folder. Inside it, create an auth.dto.ts
file. The file will define the various interfaces we need when handling user input. In the auth.dto.ts
file, add the following:
export interface LoginDTO { // During login email:string, password:string } export interface RegisterDTO { // During registration name:string, email:string, password:string }
Defining User Service
Inside the project’s src
folder, create a shared
folder. Inside the folder, create a user.service.ts
file. The file will host the functionalities of creating a new user, logging a user in via email, and also finding a user via payload from JWT. In this file:
- Import the necessary modules:
import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; import { Model } from 'mongoose'; import { User } from 'src/types/user'; import { LoginDTO, RegisterDTO } from 'src/auth/auth.dto'; import * as bcrypt from 'bcrypt';
- Define a UserService class:
@Injectable() export class UserService { // define a constructor to inject the model into the service constructor(@InjectModel('User') private readonly userModel: Model<User>) { } }
- Inside the class, define a method for creating a new user:
// create a new user async create(userDTO: RegisterDTO): Promise < User > { // get email from the input const { email } = userDTO; // check a user with that email const user = await this.userModel.findOne({ email }); // Check if user already exists if(user) { // User already exists throw new HttpException('User already exists', HttpStatus.BAD_REQUEST); } // Create the new user const createdUser = new this.userModel(userDTO); // Save the new user await createdUser.save(); // Return the saved user return createdUser; }
- Inside the class, define a method for logging a user in:
async findByLogin(userDTO: LoginDTO): Promise < User > { const { email, password } = userDTO; // Get the email and password. // find user by email const user = await this.userModel.findOne({ email }).select('+password'); if(!user) { // Check if user exists // User not found throw new HttpException('Invalid credentials', HttpStatus.UNAUTHORIZED); } // check if password is correct const passworMatch = await bcrypt.compare(password, user.password); if(!passworMatch) { // Invalid credentials throw new HttpException('Invalid credentials', HttpStatus.UNAUTHORIZED); } // return the user return user; }
- Inside the class, define a method for finding a user via Jwt’s payload:
async findByPayload(payload: any) { // Extract email from payload const { email } = payload; // Get user from the email const user = await this.userModel.findOne({ email }); // return the user return user; }
Define a Shared Module
After creating a user service, we must create a shared module file to host it. Inside the shared folder we created prior, create a shared.module.ts file. Inside the file, add the following definitions:
// import modules import {Module} from '@nestjs/common'; import {MongooseModule} from '@nestjs/mongoose'; import {userSchema} from 'src/models/user.schema'; import {UserService} from './user.service'; @Module({ imports: [ MongooseModule.forFeature([{name: 'User', // model definition schema: userSchema}])], // provider definition providers: [UserService], // export definitions exports: [UserService] }) export class SharedModule {}
Defining an Authentication Service
Inside the src/auth
folder. Create an auth.service.ts
file. The file will define the core functionalities for authentication. Inside the file:
- Import the necessary modules:
import {Injectable} from '@nestjs/common'; import {sign} from 'jsonwebtoken'; import {UserService} from 'src/shared/user.service';
- Define an AuthService class:
export class AuthService { // define user service constructor(private userService: UserService) {} }
- Inside the class, define a method for creating an authentication token:
async signPayload(payload: any) { // token to expire in 12 hours let token = sign(payload, 'secretKey', { expiresIn: '12h' }); return token; }
- Inside the class, define a method for validating a user based on the JWT payload:
async validateUser(payload: any) { return await this.userService.findByPayload(payload); }
Define the Authentication Controller
The authentication controller will host all the authentication routes and the functionality to be handled on each route. On src/auth
directory, create a auth.controller.ts
file. In this file:
- Import the necessary modules:
import {Body,Controller,Post} from '@nestjs/common'; import {AuthService} from './auth.service'; import {LoginDTO,RegisterDTO} from './auth.dto'; import {UserService} from 'src/shared/user.service';
- Define an
AuthController
class:@Controller('auth') export class AuthController { // define the auth and user service. constructor(private authService: AuthService, private userService: UserService) { } }
- Inside the class, define the login route and method:
// login route @Post('login') // find the user based on the input data async login(@Body() userDTO: LoginDTO) { const user = await this.userService.findByLogin(userDTO); // define a payload const payload = { email: user.email, } //get a JWT authentication token from the payload const token = await this.authService.signPayload(payload); // return the user and the token return { user, token } }
- Inside the class, define the registration route and method:
// registration route @Post('register') async register(@Body() userDTO: RegisterDTO) { // Create user based on the input data const user = await this.userService.create(userDTO); // define a payload const payload = { email: user.email, } // get a JWT authentication token from the payload const token = await this.authService.signPayload(payload); // return the user and the token return { user, token } }
Define a JWT Strategy for Handling Route Authentication
Inside the src/auth
directory, create a jwt.strategy.ts
file. In this file, we will implement the functionality of handling route authentication. Route authentication involves sending an authentication token from the headers and then doing a lookup to check whether that token is authorized. Inside the file:
- Import the necessary modules:
import {HttpException,HttpStatus,Injectable} from '@nestjs/common'; import {PassportStrategy} from '@nestjs/passport'; import {ExtractJwt,Strategy,VerifiedCallback} from 'passport-jwt'; import {AuthService} from './auth.service';
- Define the JwtStrategy class:
export class JwtStrategy extends PassportStrategy(Strategy) { // define auth service and extract jwt token from header constructor(private authService: AuthService) { super({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), secretOrKey: 'secretKey', }); } }
- Inside the class, define a validate method:
async validate(payload: any, done: VerifiedCallback){ // Get user from payload const user = await this.authService.validateUser(payload); // If no user return unauthorized response if (!user) { return done(new HttpException('Unauthorized', HttpStatus.UNAUTHORIZED)); } // else, return successful response with the user data return done(null, user, payload.iat); }
Define the Authentication Module
On src/auth
directory, create an auth.module.ts
file. We will define the authentication module with its imports, controllers, and providers in the file. In this file:
- Import the necessary modules:
import {Module} from '@nestjs/common'; import {SharedModule} from 'src/shared/shared.module'; import {AuthController} from './auth.controller'; import {AuthService} from './auth.service'; import {JwtStrategy} from './jwt.strategy';
- Define the module:
@Module({ imports: [SharedModule], controllers: [AuthController], providers: [AuthService, JwtStrategy], })
- Define an
AuthModule
class:export class AuthModule {}
Update the app.module.ts
We will update app.module.ts
to reflect the database connection and also the modules we have implemented. Therefore, on the file, make the following changes:
- Import necessary modules:
import { Module } from '@nestjs/common'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { MongooseModule } from '@nestjs/mongoose'; import { SharedModule } from './shared/shared.module'; import { AuthController } from 'src/auth/auth.controller'; import { AuthService } from 'src/auth/auth.service'; import { AuthModule } from 'src/auth/auth.module';
- Edit the module definition:
@Module({ imports: [ // connect to the database MongooseModule.forRoot('mongodb://localhost/jwt'), // shared module SharedModule, // auth module AuthModule, ], controllers: [AppController, AuthController], providers: [AppService, AuthService], })
Testing
At this step, we have done all the steps. Ensure that you have started the development server and launch postman. Send a registration request (POST) to: http://localhost:3000/auth/register
. On the body part, send a raw JSON payload as below:
{ "name": "Test account", "email": "[email protected]", "password": "test@123" }
Click Send button on your Postman. You should get a similar response to the one below:
Then send a login request (POST) to http://localhost:3000/auth/login
. On the body part, send a raw JSON payload as below:
{ "name": "Test account", "email": "[email protected]" }
Finally, click Send. You should get a similar response to the one above since they both return user data and tokens.
Conclusion
JWT provides a way of representing claims to be transferred between the server and the client for authentication purposes. A server only wants to share data with a trustworthy client.
The server creates a token. This token is returned to the client, and it’s up to the client to store it and send it along as required for any requests to the server. Any time a client makes a request along a secure route, it does just that along the JWT token.
The server then verifies if this token is from who it says it’s coming from and hasn’t been tampered with. If everything is checked as expected, the server sends back a response with the requested data to the trusted client.
I hope you found this Nest.js JWT authentication tutorial helpful. Happy coding!