Implementing Authentication Guards for Permission Management in NestJS
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 {}