How to Build Nest.js JWT Authentication API

352 VIEWS

·

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": "[email protected]"
}

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!


Joseph is fluent in Fullstack web development and has a lot of passion for DevOps engineering. He loves to write code and document them in blogs to share ideas.


Discussion

Click on a tab to select how you'd like to leave your comment

Leave a Comment

Your email address will not be published.

Menu
Skip to toolbar