Lesson 2
Integrating JWT for Authentication in NestJS
Introduction to JWT Authentication in NestJS

Welcome to this lesson on integrating JWT for authentication in your NestJS application. In the previous lesson, we discussed creating users with encrypted passwords using bcrypt, which laid the foundation for secure user authentication. Building upon that, today's lesson will focus on how to implement JWT (JSON Web Tokens) in your NestJS app to facilitate stateless authentication, a powerful technique widely used in modern web applications. JWT offers several advantages, including scalability and ease of management compared to traditional session-based authentication methods.

Understanding JWT Structure and Functionality

Before diving into the implementation, it's essential to understand the structure and functionality of JWTs. A JWT consists of three parts:

  • Header: This contains the type of token (JWT) and the signing algorithm being used, such as HMAC SHA256.
  • Payload: Also known as the claims, this is where you store the user data and other metadata. This part includes registered claims (standard fields like exp for expiration time) and custom claims (custom fields like user roles).
  • Signature: This is created by encoding the header, payload, and a secret key with a hashing algorithm. The signature is used to verify the sender's authenticity and ensure that the message wasn't altered.

JWTs are typically used in scenarios where you need to securely transfer information, such as authenticating users in a web application. The token is usually stored client-side, like in a browser's local storage, and sent with each request to the server via a Bearer token in the request header.

Integrating JWT into NestJS

Now, let's integrate JWT into your NestJS application. We'll go through the code step-by-step.

  1. Configuring JWT Module:

    In the auth.module.ts file, you need to configure the JWT module to handle token creation and validation. Here's how you can do it:

    TypeScript
    1import { Module } from '@nestjs/common'; 2import { JwtModule } from '@nestjs/jwt'; 3import { AuthService } from './auth.service'; 4import { AuthController } from './auth.controller'; 5import { UserModule } from '../user/user.module'; 6import { jwtConstants } from './constants'; 7 8@Module({ 9 imports: [ 10 UserModule, 11 JwtModule.register({ 12 global: true, 13 secret: jwtConstants.secret, 14 signOptions: { expiresIn: '60s' }, 15 }), 16 ], 17 providers: [AuthService], 18 controllers: [AuthController], 19}) 20export class AuthModule {}

    By registering the JwtModule, you allow your application to create and decode JWTs. The secret is used to sign the tokens, and expiresIn specifies how long the token is valid.

  2. Developing AuthService:

    The AuthService handles the business logic for user login. Here's how you can implement these methods:

    TypeScript
    1import { Injectable } from '@nestjs/common'; 2import * as bcrypt from 'bcrypt'; 3import { UserService } from '../user/user.service'; 4import { JwtService } from '@nestjs/jwt'; 5 6const SALT_ROUNDS = 10; 7 8export type AuthResponse = { 9 access_token: string; 10}; 11 12@Injectable() 13export class AuthService { 14 constructor( 15 private usersService: UserService, 16 private jwtService: JwtService, 17 ) {} 18 19 async register(username: string, unhashedPassword: string): Promise<string> { 20 const password = await bcrypt.hash(unhashedPassword, SALT_ROUNDS); 21 const user = await this.usersService.create(username, password); 22 return user.username; 23 } 24 25 async logIn(username: string, unhashedPassword: string): Promise<AuthResponse | null> { 26 const user = await this.usersService.findByUsername(username); 27 28 if (!user || !(await bcrypt.compare(unhashedPassword, user.password))) { 29 return null; 30 } 31 32 const payload = { sub: user._id, username: user.username }; 33 return { 34 access_token: await this.jwtService.signAsync(payload), 35 }; 36 } 37}

    In the logIn method, bcrypt is used to verify the password, and a JWT token is generated using JwtService. The token contains a payload with user information, which is signed and returned to the client.

    bcrypt.compare(unhashedPassword, user.password) checks if the unhashed (plain-text) password provided by the user during login matches the hashed password stored in the database for that user. Instead of directly comparing the plain-text password with the hash (since the plain-text password is never stored), bcrypt.compare() runs an algorithm to determine whether the result of hashing the input password matches the stored hash.

  3. Setting Up AuthController:

    In the auth.controller.ts, you need endpoints to handle user login:

    TypeScript
    1import { Controller, Post, Body, HttpStatus, HttpException } from '@nestjs/common'; 2import { AuthService } from './auth.service'; 3import { UsernameWithPassword } from './dtos/auth.dto'; 4 5@Controller('auth') 6export class AuthController { 7 constructor(private readonly authService: AuthService) {} 8 9 @Post('register') 10 async register(@Body() createUserDto: UsernameWithPassword) { 11 const { username, password } = createUserDto; 12 return await this.authService.register(username, password); 13 } 14 15 @Post('login') 16 async login(@Body() loginUserDto: UsernameWithPassword) { 17 const { username, password } = loginUserDto; 18 19 const token = await this.authService.logIn(username, password); 20 21 if (!token) { 22 throw new HttpException('Invalid credentials', HttpStatus.UNAUTHORIZED); 23 } 24 return token; 25 } 26}

    The login endpoint processes user credentials and issues a JWT upon successful login or returns an error if the credentials are invalid.

Example: Secure Login API with JWT

Let's walk through an example to solidify these concepts. Consider a user attempting to log in to your ToDo app:

  1. Client Sends Login Request:

    JSON
    1POST /auth/login 2Content-Type: application/json 3 4{ 5 "username": "user123", 6 "password": "password123" 7}
  2. Server Verifies Credentials:

    • If verification fails, return an unauthorized error.
    JSON
    1{ 2 "statusCode": 401, 3 "message": "Invalid credentials" 4}
    • On success, generate and return a JWT:
    JSON
    1{ 2 "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." 3}

In this example, the JWT provides a means for the client to authenticate itself with the server for subsequent requests.

Testing and Verifying JWT Implementation

For now, we're only creating the JWT Token. In the next lesson, we'll learn how to use the token. In the meantime, we can still test that the token is being received properly:

send_request.ts

TypeScript
1import axios from 'axios'; 2 3 4// Register a new user 5async function registerUser(username: string) { 6 try { 7 const response = await axios.post('http://localhost:3000/auth/register', { 8 username, 9 password: 'testpass' 10 }); 11 return response.data; 12 } catch (error: any) { 13 console.error('Error:', error.message); 14 } 15} 16 17// Login a user 18async function loginUser(username: string) { 19 try { 20 const response = await axios.post('http://localhost:3000/auth/login', { 21 username, 22 password: 'testpass' 23 }); 24 return response.data; 25 } catch (error: any) { 26 console.error('Error:', error.message); 27 } 28} 29 30async function run() { 31 console.log('Registering a new user'); 32 await registerUser("testuser"); 33 34 console.log('\nLogging in the user'); 35 const { access_token } = await loginUser("testuser1"); 36 console.log("Received JWT: ", access_token); 37} 38 39run();
Sneek Peek!

The JWT token will be used within future API requests in the header of the request. Here's an example of sending the JWT token as an Authorization header. Our code doesn't do anything with the token for requests yet. Once you master this section, you'll get the chance to use your tokens.

TypeScript
1axios.get('http://localhost:3000/todos', { 2 headers: { Authorization: `Bearer ${token}` } 3});
Summary and Next Steps

In this lesson, you learned how to integrate JWT Tokens into an authentication system with NestJS. We broke down the process into manageable steps, covering the JWT structure, configuring NestJS with JWT, building the login functionality, and verifying the implementation. Now, you can proceed to the practice exercises, where you'll reinforce these concepts and ensure you understand how JWT can be used to secure your NestJS applications. Congratulations on reaching the end of this course! Your hard work will pay off as you continue to develop secure applications.

Enjoy this lesson? Now it's time to practice with Cosmo!
Practice is how you turn knowledge into actual skills.