Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Implementing Authentication Guards for Permission Management in NestJS

Tech 1

Monolithic Application Architecture

Request Interception with Guards

A Guard is used to intercept incoming requests and determine if they should be allowed to proceed. The logic typically allows public endpoints like login to pass through, while requiring authentication for protected routes.

Creating a JWT Authentication Guard

Implement the CanActivate interface to create a custom guard that validates JWT tokens.

File: jwt-auth.guard.ts

import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { Request } from 'express';
import { AuthService } from './auth.service';
import { Reflector } from '@nestjs/core';
import { PUBLIC_ENDPOINT_KEY } from './public-endpoint.decorator';

@Injectable()
export class AuthGuard implements CanActivate {
    constructor(
        private readonly jwtProvider: JwtService,
        private readonly reflector: Reflector,
        private readonly authProvider: AuthService
    ) {}

    async canActivate(ctx: ExecutionContext): Promise<boolean> {
        const isPublic = this.reflector.getAllAndOverride<boolean>(PUBLIC_ENDPOINT_KEY, [
            ctx.getHandler(),
            ctx.getClass()
        ]);
        
        if (isPublic) return true;

        const req = ctx.switchToHttp().getRequest<Request>();
        const authToken = this.extractAuthToken(req);
        
        if (!authToken) throw new UnauthorizedException('No authentication token provided');

        try {
            const userData = await this.authProvider.verifyToken(authToken);
            return !!userData;
        } catch {
            throw new UnauthorizedException('Invalid or expired token');
        }
    }

    private extractAuthToken(req: Request): string | undefined {
        const authHeader = req.headers.authorization;
        if (!authHeader) return undefined;
        
        const [scheme, token] = authHeader.split(' ');
        return scheme === 'Bearer' ? token : undefined;
    }
}

File: public-endpoint.decorator.ts

import { SetMetadata } from '@nestjs/common';

export const PUBLIC_ENDPOINT_KEY = 'isPublicEndpoint';
export const SkipAuth = () => SetMetadata(PUBLIC_ENDPOINT_KEY, true);

Module Configuration

Configure the authentication module with necesary dependencies.

File: auth.module.ts

import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { APP_GUARD } from '@nestjs/core';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { AuthGuard } from './jwt-auth.guard';
import { UserModule } from '../user/user.module';
import { CacheModule } from '../cache/cache.module';

@Module({
    imports: [
        JwtModule.register({
            secret: process.env.JWT_SECRET_KEY,
            signOptions: { expiresIn: process.env.JWT_EXPIRATION }
        }),
        CacheModule,
        UserModule
    ],
    controllers: [AuthController],
    providers: [
        AuthService,
        {
            provide: APP_GUARD,
            useClass: AuthGuard
        }
    ],
    exports: [AuthService]
})
export class AuthModule {}

File: auth.service.ts

import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { CacheService } from '../cache/cache.service';
import { UserService } from '../user/user.service';
import { compare } from 'bcryptjs';

@Injectable()
export class AuthService {
    constructor(
        private readonly jwtProvider: JwtService,
        private readonly cacheProvider: CacheService,
        private readonly userProvider: UserService
    ) {}

    async verifyCredentials(email: string, password: string) {
        const users = await this.userProvider.findByEmail(email);
        if (!users || users.length === 0) return null;
        
        const user = users[0];
        const storedHash = await this.userProvider.getPasswordHash(user.id);
        
        return await compare(password, storedHash) ? user : null;
    }

    async authenticate(email: string, password: string) {
        const user = await this.verifyCredentials(email, password);
        if (!user) throw new Error('Invalid credentials');

        await this.cacheProvider.store(`session:${user.id}`, user);

        return {
            token: this.jwtProvider.sign(
                { userId: user.id },
                { secret: process.env.JWT_SECRET_KEY }
            ),
            user
        };
    }

    async verifyToken(token: string) {
        let userId: number;
        try {
            userId = this.jwtProvider.verify(token).userId;
        } catch (error) {
            throw new Error('Token verification failed');
        }

        const session = await this.cacheProvider.retrieve(`session:${userId}`);
        if (!session) throw new Error('Session not found');

        return session;
    }
}

File: auth.controller.ts

import { Controller, Post, Body, Get } from '@nestjs/common';
import { AuthService } from './auth.service';
import { SkipAuth } from './public-endpoint.decorator';

@Controller('auth')
export class AuthController {
    constructor(private readonly authProvider: AuthService) {}

    @SkipAuth()
    @Post('login')
    async signIn(@Body() credentials: { email: string; password: string }) {
        return this.authProvider.authenticate(credentials.email, credentials.password);
    }

    @SkipAuth()
    @Get('health')
    healthCheck() {
        return { status: 'ok' };
    }
}

Microservices Architecture

Ddeicated Authentication Service

Create a standalone service responsible for authentication that other services can query.

File: local.strategy.ts

import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-local';
import { AuthService } from './auth.service';

@Injectable()
export class LocalAuthStrategy extends PassportStrategy(Strategy) {
    constructor(private readonly authProvider: AuthService) {
        super({ usernameField: 'email' });
    }

    async validate(email: string, password: string) {
        const user = await this.authProvider.verifyCredentials(email, password);
        if (!user) throw new UnauthorizedException('Authentication failed');
        return user;
    }
}

File: auth.service.ts (Microservice)

import { Injectable, Inject } from '@nestjs/common';
import { ClientProxy } from '@nestjs/microservices';
import { JwtService } from '@nestjs/jwt';
import { CacheService } from '../cache/cache.service';
import { lastValueFrom, timeout } from 'rxjs';

@Injectable()
export class AuthService {
    constructor(
        @Inject('USER_SERVICE') private readonly userClient: ClientProxy,
        private readonly jwtProvider: JwtService,
        private readonly cacheProvider: CacheService
    ) {}

    async verifyCredentials(email: string, password: string) {
        const userResponse = await lastValueFrom(
            this.userClient.send(
                { role: 'user', action: 'find' },
                { where: { email }, limit: 1 }
            ).pipe(timeout(5000))
        );

        const [user] = userResponse;
        if (!user) return null;

        const hashResponse = await lastValueFrom(
            this.userClient.send(
                { role: 'user', action: 'getPassword' },
                { id: user.id }
            ).pipe(timeout(5000))
        );

        // Password comparison logic here
        return user;
    }

    async validateToken(token: string): Promise<[boolean, any]> {
        try {
            const payload = this.jwtProvider.verify(token);
            const session = await this.cacheProvider.retrieve(`session:${payload.userId}`);
            return [!!session, session];
        } catch {
            return [false, null];
        }
    }
}

Consuming Services

Other services consume the authentication service via microservice communication.

File: jwt.strategy.ts

import { Injectable, Inject, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-http-bearer';
import { ClientProxy } from '@nestjs/microservices';
import { lastValueFrom, timeout } from 'rxjs';

@Injectable()
export class JwtValidationStrategy extends PassportStrategy(Strategy, 'jwt') {
    constructor(@Inject('AUTH_SERVICE') private readonly authClient: ClientProxy) {
        super();
    }

    async validate(token: string) {
        const response = await lastValueFrom(
            this.authClient.send(
                { role: 'auth', action: 'validateToken' },
                { token }
            ).pipe(timeout(5000))
        );

        const [isValid, user] = response;
        if (!isValid) throw new UnauthorizedException();
        
        return user;
    }
}

File: auth.guard.ts (Microservice Consumer)

import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Injectable()
export class MicroserviceAuthGuard extends AuthGuard('jwt') {}

File: consumer-auth.module.ts

import { Module } from '@nestjs/common';
import { ClientsModule, Transport } from '@nestjs/microservices';
import { JwtValidationStrategy } from './jwt.strategy';

@Module({
    imports: [
        ClientsModule.register([{
            name: 'AUTH_SERVICE',
            transport: Transport.TCP,
            options: {
                host: process.env.AUTH_SERVICE_HOST,
                port: parseInt(process.env.AUTH_SERVICE_PORT)
            }
        }])
    ],
    providers: [JwtValidationStrategy]
})
export class AuthIntegrationModule {}

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.