How to Create a Simple Authentication System Using NestJS

Learn how to create a simple authentication system in NestJS using Passport.

How to Create an Authentication System Using NestJS

In this tutorial, I'll walk you through the process of setting up a basic authentication system using NestJS, Passport, and the passport-local strategy. We'll then add a refresh token endpoint to keep your users securely logged in while keeping your JWT token expiration short. Let's get started!

Prerequisites

Before we dive into the code, make sure you have these packages installed:

  • @nestjs/passport
  • passport
  • passport-local

You can install them using npm like this:

npm install @nestjs/passport passport passport-local

Setting Up NestJS

First, let's set up our NestJS project. If you haven't created one yet, you can do so with the Nest CLI:

nest new auth-system
cd auth-system

Create the Auth Module

We'll start by creating an AuthModule that will house all our authentication logic.

nest generate module auth

Create the User Service

Next, let's create a service to manage our users. For simplicity, we'll use an in-memory array of users.

nest generate service users

In users.service.ts, add the following code:

import { Injectable } from "@nestjs/common";
import { User } from "./user.interface";

@Injectable()
export class UsersService {
  // Mock list of users
  private readonly users: User[] = [
    { userId: 1, username: "raymond", password: "passwordForRaymond" },
    { userId: 2, username: "liz", password: "passwordForLiz" },
    { userId: 3, username: "donald", password: "passwordForDonald" },
  ];

  async findOne(username: string): Promise<User | undefined> {
    return this.users.find((user) => user.username === username);
  }
}

Create the Auth Service

Now, let's create a service to handle our authentication logic.

nest generate service auth

In auth.service.ts, add the following code:

import { Injectable } from "@nestjs/common";
import { UsersService } from "../users/users.service";
import { JwtService } from "@nestjs/jwt";

@Injectable()
export class AuthService {
  constructor(
    private usersService: UsersService,
    private jwtService: JwtService
  ) {}

  async validateUser(username: string, pass: string): Promise<any> {
    const user = await this.usersService.findOne(username);
    if (user && user.password === pass) {
      const { password, ...result } = user;
      return result;
    }
    return null;
  }

  async login(user: any) {
    const payload = { username: user.username, sub: user.userId };
    return {
      access_token: this.jwtService.sign(payload),
    };
  }

  // Add a method for refreshing tokens
  async refresh(token: string) {
    const decoded = this.jwtService.verify(token);
    const payload = { username: decoded.username, sub: decoded.sub };
    return {
      access_token: this.jwtService.sign(payload),
    };
  }
}

Setting Up Local Strategy

We'll use the passport-local strategy for username and password authentication.

nest generate class auth/local.strategy

In local.strategy.ts, add the following code:

import { Strategy } from "passport-local";
import { PassportStrategy } from "@nestjs/passport";
import { Injectable, UnauthorizedException } from "@nestjs/common";
import { AuthService } from "./auth.service";

@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
  constructor(private authService: AuthService) {
    super();
  }

  async validate(username: string, password: string): Promise<any> {
    const user = await this.authService.validateUser(username, password);
    if (!user) {
      throw new UnauthorizedException();
    }
    return user;
  }
}

Setting Up Local Auth Guard

We'll need a guard to handle the local strategy.

nest generate class auth/local-auth.guard

In local-auth.guard.ts, add the following code:

import { Injectable } from "@nestjs/common";
import { AuthGuard } from "@nestjs/passport";

@Injectable()
export class LocalAuthGuard extends AuthGuard("local") {}

Setting Up Auth Controller

Now, let's create a controller to handle authentication requests.

nest generate controller auth

In auth.controller.ts, add the following code:

Notice that we added @UseGuards(LocalAuthGuard) to the login endpoint which triggers our local auth strategy.

import { Controller, Request, Post, UseGuards } from "@nestjs/common";
import { AuthService } from "./auth.service";
import { LocalAuthGuard } from "./local-auth.guard";

@Controller("auth")
export class AuthController {
  constructor(private authService: AuthService) {}

  @UseGuards(LocalAuthGuard)
  @Post("login")
  async login(@Request() req) {
    return this.authService.login(req.user);
  }

  @Post("refresh")
  async refresh(@Request() req) {
    const { refresh_token } = req.body;
    return this.authService.refresh(refresh_token);
  }
}

Adding JWT Support

Let's add JWT support to our application. First, install the necessary packages:

npm install @nestjs/jwt passport-jwt

Update auth.module.ts to include JWT configuration:

import { Module } from "@nestjs/common";
import { JwtModule } from "@nestjs/jwt";
import { PassportModule } from "@nestjs/passport";
import { AuthService } from "./auth.service";
import { LocalStrategy } from "./local.strategy";
import { UsersModule } from "../users/users.module";
import { AuthController } from "./auth.controller";

@Module({
  imports: [
    UsersModule,
    PassportModule,
    JwtModule.register({
      secret: "secretKey", // Use a more secure key in production
      signOptions: { expiresIn: "60s" }, // Token expiration time
    }),
  ],
  providers: [AuthService, LocalStrategy],
  controllers: [AuthController],
})
export class AuthModule {}

Putting It All Together

Make sure to import the AuthModule in your main application module (app.module.ts).

import { Module } from "@nestjs/common";
import { AuthModule } from "./auth/auth.module";
import { UsersModule } from "./users/users.module";

@Module({
  imports: [AuthModule, UsersModule],
})
export class AppModule {}

Testing the Authentication System

Start your NestJS application:

npm run start

You can test your authentication system using tools like Postman or Insomnia. Here's how to do it:

  1. Login: Send a POST request to http://localhost:3000/auth/login with a JSON body containing username and password.
curl -XPOST -H "Content-type: application/json" -d '{
  "username": "raymond",
  "password": "passwordForRaymond"
}' 'http://localhost:3000/auth/login'
  1. Refresh Token: Send a POST request to http://localhost:3000/auth/refresh with a JSON body containing refresh_token.
curl -XPOST -H "Content-type: application/json" -d '{
  "refresh_token": "your_refresh_token_here"
}' 'http://localhost:3000/auth/refresh'

And that's it! You've successfully created an authentication system using NestJS, Passport, and JWT. This setup provides a starting point that you can build upon to add more advanced features like role-based access control, social logins, and more.

More posts in JavaScript