Lesson 3
Securing Endpoints with JWT Guards
Introduction to Endpoint Security

Welcome to the lesson on securing endpoints with JWT Guards in your NestJS application. In our previous lessons, we laid a solid foundation for user authentication. We explored creating users with encrypted passwords using bcrypt, and then we progressed to integrating JSON Web Tokens (JWT) for authentication. Now, we'll focus on safeguarding API endpoints using Guards in NestJS. This step is crucial for ensuring that sensitive resources are protected and only accessible to authenticated users. By the end of this lesson, you'll be able to implement a JWT-based security layer for your ToDo app's endpoints.

Creating a NestJS Auth Guard

Guards in NestJS act as middleware, determining if a request can proceed upon its received verification. We’ll create an AuthGuard that uses JWTs to ensure that only authenticated requests gain access:

Step-by-step Breakdown:
  1. Define the AuthGuard:

    TypeScript
    1import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common'; 2import { JwtService } from '@nestjs/jwt'; 3import { Request } from 'express'; 4 5@Injectable() 6export class AuthGuard implements CanActivate { 7 constructor(private jwtService: JwtService) {} 8 9 async canActivate(context: ExecutionContext): Promise<boolean> { 10 const request = context.switchToHttp().getRequest(); 11 try { 12 // Extract and verify the JWT from the request header 13 const payload = await this.verifyToken(request); 14 request['user'] = payload; // Attach the user to the request 15 } catch { 16 throw new UnauthorizedException(); 17 } 18 return true; 19 } 20 21 private async verifyToken(request: Request): Promise<any> { 22 const authHeader = request.headers.authorization || ''; 23 const [type, token] = authHeader.split(' '); 24 25 if (type !== 'Bearer' || !token) { 26 throw new UnauthorizedException(); 27 } 28 29 // Verify the token using the JwtService and return the payload 30 const payload = await this.jwtService.verifyAsync(token); 31 return payload; 32 } 33}
  2. Explanation:

    • The AuthGuard checks each incoming request for a JWT in the Authorization header.
    • If present and valid, it attaches the user payload from the JWT to the request object.
    • If not, it throws an UnauthorizedException, blocking access to the endpoint.

This Guard is a crucial part of your security strategy, ensuring only authenticated requests can reach certain endpoints.

Secure all routes with the AuthGuard

In a NestJS application, securing all routes by default ensures that only authenticated users can access the application's resources. By integrating the AuthGuard at the module level, we can enforce this security measure across all endpoints, except those explicitly marked as public.

In this configuration, the AuthGuard is added to the providers array, allowing it to be automatically injected where needed. The JwtService is also included since it's a dependency required by the AuthGuard for verifying tokens.

app/src/auth/auth.module.ts

TypeScript
1import { Module } from '@nestjs/common'; 2import { APP_GUARD } from '@nestjs/core'; 3import { JwtModule } from '@nestjs/jwt'; 4import { AuthService } from './auth.service'; 5import { AuthController } from './auth.controller'; 6import { UserModule } from '../user/user.module'; 7import { jwtConstants } from './constants'; 8import { AuthGuard } from './auth.guard'; 9 10@Module({ 11 imports: [ 12 UserModule, 13 JwtModule.register({ 14 global: true, 15 secret: jwtConstants.secret, 16 signOptions: { expiresIn: '60s' }, 17 }), 18 ], 19 providers: [ 20 // Add the Auth Guard to require a token 21 // for all routes! Unless marked with @Public() 22 { 23 provide: APP_GUARD, 24 useClass: AuthGuard, 25 }, 26 AuthService, 27 ], 28 controllers: [AuthController], 29}) 30export class AuthModule {}

After implementing these changes, all routes within the application will require a valid JWT token. This pattern ensures a strong security stance by default, minimizing potential unauthorized access to application endpoints.

Implementing the Public Decorator

In some cases, you’ll want to allow public access to specific routes, like registration or login. We’ll implement a Public decorator that allows you to mark these routes as exceptions.

Code Example:
TypeScript
1import { SetMetadata } from '@nestjs/common'; 2 3export const IS_PUBLIC_KEY = 'isPublic'; 4export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
Explanation:
  • Public is a custom decorator using SetMetadata.
  • It marks certain endpoints as public by associating metadata with them to be read by the guard (next).
  • NestJS guards can read this metadata to selectively allow public access.
Updating the AuthGuard to allow public routes

Now, we must check for the metadata that was injected with the @Public decorator. If it exists, we allow the request to go through.

app/src/auth/auth.guard.ts

TypeScript
1@Injectable() 2export class AuthGuard implements CanActivate { 3 constructor( 4 private jwtService: JwtService, 5 private reflector: Reflector, 6 ) {} 7 8 async canActivate(context: ExecutionContext): Promise<boolean> { 9 if (this.isPublic(context)) { 10 return true; 11 } 12 13 // Handle the rest of the Guard the same way 14 } 15 16 private isPublic(context: ExecutionContext): boolean { 17 return this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [ 18 context.getHandler(), 19 context.getClass(), 20 ]); 21 } 22}
Explanation:
  • isPublic will check for metadata on Controllers or Controller Handlers.
  • If we detect the public metadata (via IS_PUBLIC_KEY), we let the request through.
  • Otherwise we check for a valid JWT as we did in the previous example.
Example: Allowing the registration and login methods to be public

Most of our API can be protected by the AuthGuard and require a JWT token. However, that is not true for the auth/register and auth/login routes! You don't have a token if you haven't registered or logged-in yet. We can use our @Public() decorator on our routes:

TypeScript
1@Controller('auth') 2export class AuthController { 3 constructor(private readonly authService: AuthService) {} 4 5 @Public() 6 @Post('register') 7 async register(@Body() createUserDto: UsernameWithPassword) { 8 const { username, password } = createUserDto; 9 10 return await this.authService.register(username, password); 11 } 12 13 @Public() 14 @Post('login') 15 async login(@Body() loginUserDto: UsernameWithPassword) { 16 const { username, password } = loginUserDto; 17 18 const authResponse = await this.authService.logIn(username, password); 19 20 if (!authResponse) { 21 throw new HttpException('Invalid credentials', HttpStatus.UNAUTHORIZED); 22 } 23 24 return authResponse; 25 }
Explanation:
  • @Public(): The @Public() decorator sets the isPublic value to true in the metadata. This allows these routes to be executed without a token.
Using the JWT token to access protected routes

Once authentication is set up in your NestJS application, users will receive a JWT token upon successful login. This token must be included in the HTTP headers for requests to access protected routes. Here's how a consumer can send the JWT token using a sample script.

Sample Request Script

In this example, we demonstrate how to utilize the JWT token in an HTTP request header using a tool like axios in a JavaScript environment:

send_request.ts

TypeScript
1import axios from 'axios'; 2 3// Create a new todo item 4async function createTodo(token: string) { 5 try { 6 const response = await axios.post('http://localhost:3000/todos', { 7 title: 'Learn NestJS', 8 description: `Explore the basics of NestJS`, 9 }, { 10 // IMPORTANT! Include the token you received from the login request! 11 headers: { Authorization: `Bearer ${token}` } 12 }); 13 return response.data; 14 } catch (error: any) { 15 console.error('Error:', error.message); 16 } 17} 18 19 20// Get all todo items 21async function getTodos(token: string) { 22 try { 23 const response = await axios.get('http://localhost:3000/todos', { 24 // IMPORTANT! Include the token you received from the login request! 25 headers: { Authorization: `Bearer ${token}` } 26 }); 27 console.log('Todos:', response.data); 28 } catch (error: any) { 29 console.error('Error:', error.message); 30 } 31} 32 33async function run() { 34 console.log('Registering a new user'); 35 await registerUser("testuser"); 36 37 console.log('\nLogging in the User 1'); 38 const { access_token } = await loginUser("testuser1"); 39 40 console.log('\nCreating a new todo'); 41 await createTodo(access_token); 42 43 console.log('\nGetting all todo'); 44 await getTodos(access_token); 45} 46 47run(); 48
Explanation:
  • The createTodo and getTodo functions require an access_token that was recieved by logging in to the API.
  • The access token is passed in as a header to the request.
  • Authorization: Bearer ${token}`` is the standard header to set for token in HTTP.
Lesson Summary and Preparing for Practice

In this lesson, you've learned to secure endpoints using JWT Guards in a NestJS application. We've covered JWT components, authentication with AuthGuard, and marking specific endpoints as public using the Public decorator. These skills are critical to ensuring that only authenticated users can access sensitive data or operations within your application.

Next, you’ll have the opportunity to apply these skills through hands-on practice exercises. Remember, securing API endpoints is a vital real-world skill that enhances the security and integrity of web applications. Great job reaching this far, and keep up the excellent work!

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