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.
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.
Now, let's integrate JWT
into your NestJS
application. We'll go through the code step-by-step.
-
Configuring JWT Module:
In the
auth.module.ts
file, you need to configure theJWT
module to handle token creation and validation. Here's how you can do it:TypeScript1import { 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 decodeJWTs
. Thesecret
is used to sign the tokens, andexpiresIn
specifies how long the token is valid. -
Developing AuthService:
The
AuthService
handles the business logic for user login. Here's how you can implement these methods:TypeScript1import { 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 aJWT
token is generated usingJwtService
. 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. -
Setting Up AuthController:
In the
auth.controller.ts
, you need endpoints to handle user login:TypeScript1import { 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 aJWT
upon successful login or returns an error if the credentials are invalid.
Let's walk through an example to solidify these concepts. Consider a user attempting to log in to your ToDo app:
-
Client Sends Login Request:
JSON1POST /auth/login 2Content-Type: application/json 3 4{ 5 "username": "user123", 6 "password": "password123" 7}
-
Server Verifies Credentials:
- If verification fails, return an unauthorized error.
JSON1{ 2 "statusCode": 401, 3 "message": "Invalid credentials" 4}
- On success, generate and return a
JWT
:
JSON1{ 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.
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
TypeScript1import 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();
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.
TypeScript1axios.get('http://localhost:3000/todos', { 2 headers: { Authorization: `Bearer ${token}` } 3});
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.