Tous les articles

Part 1: Building Secure Applications: A Practical Guide to JWT and Passport in NestJS

Introduction Nearly all applications today require authentication, as security is a key business priority. So, authentication is the gatekeeper, ensuring only authorized users can access an applicatio

Etienne Maway

Etienne Maway

6 octobre 2024 · 7 min de lecture

Publié à l’origine sur Medium. Vous le lisez ici, vous pouvez aussi consulter l’original.
Part 1: Building Secure Applications: A Practical Guide to JWT and Passport in NestJS

Introduction

Nearly all applications today require authentication, as security is a key business priority. So, authentication is the gatekeeper, ensuring only authorized users can access an application’s resources and data. Whether you’re building a web app or an API, authentication solid practices are essential for security, protecting against unauthorized access and exploitation.

Without proper authentication, applications are vulnerable to attacks, leading to potential data breaches, legal issues, and even career consequences for developers. Mastering authentication is not just a security measure — it’s a professional necessity.

Overview of Authentication Strategies

NestJS offers various authentication strategies that developers can easily implement to secure applications:

  • JWT (JSON Web Token) Authentication: A stateless method where users are authenticated using tokens included in headers.
  • Local (Username/Password) Authentication: Verifies user credentials (username and password) using Passport.js.
  • OAuth2: Enables social logins via providers like Google or Facebook.
  • API Key-based Authentication: Commonly used for protecting API endpoints or service-to-service communication.

Let’s dive into two of the most common authentication methods — JWT Authentication and Local Strategy with Passport(the most used NestJs auth library). These approaches will help you grasp the authentication flow without much abstraction, as the goal is to provide you with a clear and practical understanding of the data flow.

1. JWT Authentication

JWT (JSON Web Token) is a widely used standard for securing API-based systems. It’s a compact, URL-safe token that represents claims between two parties — typically, a server and a client. JWT provides a way to authenticate users without relying on session storage, making it ideal for stateless applications, especially in modern web and mobile APIs.

The structure of a JWT consists of three parts:

  • Header: Contains the token type (JWT) and the signing algorithm used.
  • Payload: Holds the claims (e.g., user information, expiration time).
  • Signature: Ensures that the token hasn’t been altered, using a secret key.

In a typical flow:

  1. The user logs in with credentials.
  2. The server verifies the credentials and sends back a JWT.
  3. The client stores the token (usually in local storage or cookies) and includes it in the Authorization header of subsequent API requests.
  4. The server validates the token on each request to ensure the user is authenticated.

1. Set Up the Environment

Start by creating a basic NestJS project and installing the required dependencies for JWT authentication.

bash
$ nest new auth-project
$ cd auth-project
$ npm install @nestjs/jwt passport-jwt 

Create the auth and users resources

Generate an Auth resource to house the authentication logic. You’ll also need a Users resource for handling user-related operations.

bash
nest generate resource auth --no-spec
nest generate resource users --no-spec
  • nest generate resource <name>: Generates a resource with module, controller, and service.
  • --no-spec: Skips the creation of test files.

By default, it will also skip generating CRUD-based endpoints unless you specifically enable it.

2. Code Implementation

In src folder, locate users/users.service.ts , paste this code:

typescript
import { Injectable } from '@nestjs/common';

// users type
export type User = {
    id: number;
    username: string;
    password: string;
}

// simulate users in the DB
let users: User[] = [
    {
        id: 1,
        username: 'etienne',
        password: 'admin',
    },
    {
        id: 2,
        username: 'maway',
        password: 'admin',
    },
];

@Injectable()
export class UsersService {
    async getByUsername(username: string): Promise<User> {
        const user = users.find((user) => user.username === username);
        return user;
    }

    // Equivalent to fetch all users from the DB
    async findAllUsers(): Promise<User[]> {
        return users;
    }
}

The UsersService is responsible for managing user data in the application. It simulates fetching users from a database using an in-memory array of user objects. The service provides two methods:

  1. getByUsername(username: string): Fetches a user by their username.
  2. findAllUsers(): Returns a list of all users

Configure JWT in NestJS App

In auth/authModule.ts paste these lines of code:

typescript
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { UsersModule } from 'src/users/users.module';
import { UsersService } from 'src/users/users.service';
import { JwtModule } from '@nestjs/jwt';
import { ConfigModule, ConfigService } from '@nestjs/config';

@Module({
  controllers: [AuthController],
  providers: [AuthService, UsersService],
  imports: [
    UsersModule,
    JwtModule.registerAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: async (configService: ConfigService) => ({
        secret: configService.get<string>('JWT_SECRET'), // Loaded from .env
        signOptions: { expiresIn: '7d' },
      }),
    }),
    ConfigModule.forRoot({ isGlobal: true }), // Loads .env globally
  ],
})
export class AuthModule {}

The AuthModule configures JWTs library we installed in the App. It imports the UsersModule and configures the JwtModule to sign tokens with a secret loaded from the .env file, ensuring security.

Using the ConfigModule, the JWT secret can vary by environment, allowing different values for development, staging, and production. Tokens are signed with a 7-day expiration.

In auth/authService.ts paste this:

typescript
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { User, UsersService } from 'src/users/users.service';
import { LoginDto } from './dto/login.dto';
import { JwtService } from '@nestjs/jwt';

export type AuthResponse  = {
    access_token: string;
    id: number;
    username: string;
}

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


     async login(loginDto: LoginDto): Promise<AuthResponse> {
        const user = await this.validateUser(loginDto.username, loginDto.password);

        if (!user) {
            throw new UnauthorizedException('Invalid credentials');
        }

        return await this.generateToken(user);
    }

     async validateUser(username: string, password: string) {
        const user = await this.usersService.getByUsername(username);

        if (user && user.password === password) {
            return {
                id: user.id,
                username: user.username
            }
        }
        return null
    }

    async generateToken(user: { id: number; username: string }): Promise<AuthResponse> {
        const payload = {
            sub: user.id,
            username: user.username
        }

        const token = await this.jwtService.signAsync(payload);

        return {
            access_token: token,
            id: user.id,
            username: user.username
        }
    }
}

The AuthService handles user authentication, utilizing the UsersService for credential validation and the JwtService for generating JSON Web Tokens (JWTs).

Key components include:

  1. Login Method: Validates user credentials from a LoginDto. If valid, it returns a JWT, user ID, and username; otherwise, it throws an UnauthorizedException.
  2. User Validation: Checks if the user exists and verifies the password. If successful, it returns the user’s ID and username.
  3. Token Generation: Creates a JWT using the user’s ID and username as payload.

For enhanced security, it is essential to hash passwords before storing them in the database to protect user credentials.

Go to app.tsfile located in the root of your project, and register your modules:
typescript
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { AuthModule } from './auth/auth.module'; // auth module imported 
import { UsersModule } from './users/users.module'; // users module imported

@Module({
  imports: [AuthModule, UsersModule], // modules registered
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

With this setup, you’ve managed to authenticate the user successfully, allowing them to generate a token that provides secure access to services within the application.

2. Routes Protection: JWT Authorization

You’ve reached a level where you can restrict access to sensitive services. After the JWT token is issued, the client must provide it to access protected routes. The server verifies the token to grant access.

Remember, the token contains the user’s ID, username, and the server’s secret key, making signature verification straightforward. If the token expires, the request is rejected, requiring a new token to be generated. This is where your security measures come into play, ensuring only authorized users access sensitive resources.

Create a new folder and file in the auth folder, name it guards/auth.guard.ts and inside the file, paste this code:

typescript
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from "@nestjs/common";
import { JwtService } from "@nestjs/jwt";

@Injectable()
export class AuthGuard implements CanActivate {
    constructor(private readonly jwtService: JwtService){}

    async canActivate(context: ExecutionContext){
        
        const request = context.switchToHttp().getRequest()
        const authorization = request.headers.authorization // Bearer <token>

        const token = authorization?.split(' ')[1]

        if(!token){
            throw new UnauthorizedException()
        }

        try {
            const decoded = await this.jwtService.verifyAsync(token)
            request.user = {id: decoded.sub, username: decoded.username}
        } catch (error) {
            throw new UnauthorizedException()
        }
        
        return true
    }
}

The AuthGuard is a security feature in NestJS that checks if users are authorized to access certain routes using JWTs. This guard ensures that only authenticated users can access protected parts of your application.

How It Works:

JWT Verification: It uses the JwtService to verify the JWT provided in the Authorization header of incoming requests.

  • The guard checks if the token is present. If not, no authorization access
  • If the token is present, it tries to verify it. If verification fails, it again throws an UnauthorizedException.
  • If the token is valid, the guard adds the user’s ID and username to the request, making it available for further processing.

In your controllers, for example in users.controller.ts , you can make use of guards like this:

typescript
import { BadRequestException, Controller, Get, Param, Req, Request, UseGuards } from '@nestjs/common';
import { User, UsersService } from './users.service';
import { AuthGuard } from 'src/auth/guards/auth.guard';

@Controller('users')
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Get()
  @UseGuards(AuthGuard)
  async findAllUsers(@Req() req: any): Promise<User[]> {
    return this.usersService.findAllUsers();
  }

  @Get(':username')
  @UseGuards(AuthGuard)
  async getByUsername(@Req() req: any, @Param('username') name: string) {
    const user = req.user;

    if (user.username !== name) {
      throw new BadRequestException("Unmatched username");
    }

  return user
  }
}

The UsersController handles user-related requests in your NestJS application. It includes two endpoints that are protected by the AuthGuard, ensuring that only authenticated users can access them.

  • The findAllUsers method retrieves all users.
  • The getByUsername method allows users to fetch their own profile by username.
  • @UseGuards(AuthGuard): This decorator ensures that the user must be authenticated before accessing this endpoint. You can use this decorator throughout the application on the endpoints you need access only to authenticated users.

This overview equips you with the essentials of authentication and access restriction in web development. You’ve implemented a JWT-based authentication system and secured routes with guards to ensure only authorized users access sensitive data.

In Part 2,

we’ll elevate our authentication strategy using Passport.js, introducing greater abstraction and flexibility for enhanced security.

Congratulations on reaching this stage and thank you for reading! Your dedication to these concepts will empower you to build robust, secure applications. Stay tuned for the next installment!