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.
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:
-
Define the AuthGuard:
TypeScript1import { 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}
-
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.
- The
This Guard is a crucial part of your security strategy, ensuring only authenticated requests can reach certain endpoints.
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
TypeScript1import { 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.
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.
TypeScript1import { SetMetadata } from '@nestjs/common'; 2 3export const IS_PUBLIC_KEY = 'isPublic'; 4export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
Public
is a custom decorator usingSetMetadata
.- 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.
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
TypeScript1@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}
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.
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:
TypeScript1@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 }
- @Public(): The
@Public()
decorator sets theisPublic
value totrue
in the metadata. This allows these routes to be executed without a token.
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.
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
TypeScript1import 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
- The
createTodo
andgetTodo
functions require anaccess_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.
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!