Authentication Guards
Authentication is the first gate every protected request must pass through: it answers who is making this request?. In NestJS the natural place to enforce that gate is a guard — a class that runs before your route handler and returns true to allow the request or throws to reject it. This page shows two complementary approaches: a hand-rolled guard that verifies a JWT yourself, and Passport’s AuthGuard, which wraps battle-tested strategies. Both end the same way — by attaching the resolved user to the request so handlers can read it.
How an authentication guard works
A guard implements CanActivate and receives an ExecutionContext. From the context you pull the underlying HTTP request, extract a credential (typically a bearer token), verify it, and decide the outcome. Unlike middleware, guards know which handler is about to run, so they can read route metadata and they participate in Nest’s exception layer — throwing an UnauthorizedException produces a clean 401 response automatically.
The contract is intentionally tiny:
canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean>;
Return true to proceed, false to deny with a generic 403, or throw an HttpException for a precise status and message.
A custom JWT authentication guard
When you want full control, verify the token yourself with @nestjs/jwt. The guard reads the Authorization: Bearer <token> header, validates the signature, and stashes the decoded payload on request.user.
// auth/jwt-auth.guard.ts
import {
CanActivate,
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
import { Request } from 'express';
@Injectable()
export class JwtAuthGuard implements CanActivate {
constructor(
private readonly jwtService: JwtService,
private readonly config: ConfigService,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest<Request>();
const token = this.extractToken(request);
if (!token) {
throw new UnauthorizedException('Missing bearer token');
}
try {
const payload = await this.jwtService.verifyAsync(token, {
secret: this.config.getOrThrow<string>('JWT_SECRET'),
});
// Make the authenticated user available to handlers and pipes.
request['user'] = payload;
} catch {
throw new UnauthorizedException('Invalid or expired token');
}
return true;
}
private extractToken(request: Request): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
}
Wire up the JWT module so the guard can be constructed via DI:
// auth/auth.module.ts
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { ConfigModule } from '@nestjs/config';
import { JwtAuthGuard } from './jwt-auth.guard';
@Module({
imports: [ConfigModule, JwtModule.register({})],
providers: [JwtAuthGuard],
exports: [JwtAuthGuard, JwtModule],
})
export class AuthModule {}
Apply it to any handler or controller with @UseGuards:
// users/users.controller.ts
import { Controller, Get, Req, UseGuards } from '@nestjs/common';
import { Request } from 'express';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
@Controller('users')
@UseGuards(JwtAuthGuard)
export class UsersController {
@Get('me')
getProfile(@Req() req: Request) {
return req['user'];
}
}
A request without a valid token is rejected before the handler ever runs:
Output:
$ curl -i http://localhost:3000/users/me
HTTP/1.1 401 Unauthorized
Content-Type: application/json
{"statusCode":401,"message":"Missing bearer token","error":"Unauthorized"}
Using Passport’s AuthGuard
For production apps, @nestjs/passport lets you reuse the huge Passport ecosystem (JWT, local, OAuth) while keeping the guard one line. You define a strategy whose validate method returns the user; Passport then attaches that return value to request.user for you.
npm install @nestjs/passport passport @nestjs/jwt passport-jwt
npm install -D @types/passport-jwt
// auth/jwt.strategy.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { ConfigService } from '@nestjs/config';
import { UsersService } from '../users/users.service';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
constructor(
config: ConfigService,
private readonly users: UsersService,
) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: config.getOrThrow<string>('JWT_SECRET'),
});
}
// Whatever this returns becomes request.user.
async validate(payload: { sub: string; email: string }) {
const user = await this.users.findById(payload.sub);
if (!user) {
throw new UnauthorizedException();
}
return { id: user.id, email: user.email, roles: user.roles };
}
}
Subclass AuthGuard so the strategy name is fixed and easy to reuse:
// auth/passport-jwt.guard.ts
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class PassportJwtGuard extends AuthGuard('jwt') {}
Register the strategy and apply the guard exactly as before:
// auth/auth.module.ts
import { Module } from '@nestjs/common';
import { PassportModule } from '@nestjs/passport';
import { JwtStrategy } from './jwt.strategy';
import { PassportJwtGuard } from './passport-jwt.guard';
@Module({
imports: [PassportModule],
providers: [JwtStrategy, PassportJwtGuard],
exports: [PassportJwtGuard],
})
export class AuthModule {}
Reading the authenticated user cleanly
Reaching into request['user'] works but is untyped. A small parameter decorator keeps controllers tidy and strongly typed:
// auth/current-user.decorator.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const CurrentUser = createParamDecorator(
(_data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return request.user;
},
);
@Get('me')
getProfile(@CurrentUser() user: { id: string; email: string }) {
return user;
}
Custom guard vs Passport AuthGuard
| Aspect | Custom CanActivate guard | Passport AuthGuard |
|---|---|---|
| Token extraction | You write it | Handled by the strategy |
| Multiple schemes | Manual branching | AuthGuard(['jwt', 'api-key']) |
| User lookup | Inside the guard | Inside validate() |
| Boilerplate | Lower for one scheme | Lower as schemes grow |
| Best for | Simple, single-strategy apps | OAuth/social/multi-strategy apps |
Tip: Apply an auth guard globally with
APP_GUARD, then mark public endpoints (login, health checks) with a@Public()decorator that your guard checks via theReflector. This is safer than remembering to add@UseGuardsto every protected route.
Best practices
- Always throw
UnauthorizedException(401) for missing/invalid credentials and reserve 403 for authorization failures — they mean different things to clients. - Load secrets through
ConfigService.getOrThrowso a missingJWT_SECRETfails fast at boot instead of silently disabling verification. - Keep authentication (who you are) in guards and authorization (what you may do) in separate, dedicated guards so each stays small and testable.
- Attach a minimal, typed user object to the request — never the raw token or password hash.
- Prefer short-lived access tokens with refresh tokens over long-lived JWTs to limit the blast radius of a leak.
- Cover guards with unit tests by mocking
ExecutionContext; they are pure logic and fast to verify.