JWT Strategy & Guards
Once a client holds an access token, every protected request must prove it. The JWT strategy wraps passport-jwt to pull the bearer token out of the Authorization header, verify its signature and expiry, and decode the payload — all before your handler runs. Your only job is a validate() method that turns the verified payload into the request.user object the rest of the app relies on. This page builds the strategy, a reusable JwtAuthGuard, and shows how to protect endpoints with them.
Installing the dependencies
The strategy needs the Passport core packages, the passport-jwt strategy and its types, plus @nestjs/jwt for signing tokens elsewhere in the flow.
npm install @nestjs/passport passport passport-jwt @nestjs/jwt
npm install -D @types/passport-jwt
How a token is extracted and verified
passport-jwt does three things in order: it extracts the raw token from the request, verifies the signature with your secret and rejects expired tokens, then calls validate() with the decoded payload. You configure extraction and verification through the options passed to super(); you own only the last step.
| Option | Purpose |
|---|---|
jwtFromRequest | An extractor function that locates the token. ExtractJwt.fromAuthHeaderAsBearerToken() reads Authorization: Bearer <token>. |
secretOrKey | Symmetric secret (HS256) used to verify the signature. Use secretOrKeyProvider / publicKey for RS256. |
ignoreExpiration | Leave false so Passport rejects expired tokens automatically. |
issuer / audience | Optional extra claims Passport will assert during verification. |
Warning:
secretOrKeymust match the secret you signed tokens with, and it must never be hardcoded. Read it fromConfigServiceorprocess.envso it differs per environment and stays out of version control.
Implementing the JWT strategy
Subclass PassportStrategy(Strategy) from passport-jwt. By the time validate() is invoked the token’s signature and expiry are already confirmed, so the payload is trustworthy. Map its claims into a small user object — what you return here is exactly what handlers receive as request.user.
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
export interface JwtPayload {
sub: number;
username: string;
iat?: number;
exp?: number;
}
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private readonly config: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: config.getOrThrow<string>('JWT_SECRET'),
});
}
async validate(payload: JwtPayload) {
if (!payload?.sub) {
throw new UnauthorizedException('Malformed token payload');
}
// Returned value is attached to request.user
return { id: payload.sub, username: payload.username };
}
}
The validate() method is also the right place for a fresh database check when you need it — for example, re-loading the user to confirm the account is still active or has not been deactivated since the token was issued. Keep it lightweight; it runs on every protected request.
Building the JwtAuthGuard
The guard is what you actually attach to routes. Subclassing AuthGuard('jwt') gives you a named, extensible class instead of scattering the magic string 'jwt' across controllers.
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}
You can override handleRequest() to customise the error thrown when authentication fails — handy for distinguishing an expired token from a missing one.
import {
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
handleRequest<TUser = any>(err: any, user: any, info: any): TUser {
if (err || !user) {
const reason =
info?.name === 'TokenExpiredError' ? 'Token expired' : 'Unauthorized';
throw err || new UnauthorizedException(reason);
}
return user;
}
}
Wiring up the module
Register JwtStrategy as a provider so Nest resolves its dependencies, import PassportModule, and configure JwtModule with the same secret the strategy verifies against.
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { JwtStrategy } from './jwt.strategy';
import { AuthController } from './auth.controller';
@Module({
imports: [
PassportModule,
ConfigModule,
JwtModule.registerAsync({
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
secret: config.getOrThrow<string>('JWT_SECRET'),
signOptions: { expiresIn: '15m' },
}),
}),
],
controllers: [AuthController],
providers: [JwtStrategy],
})
export class AuthModule {}
Protecting endpoints
Apply JwtAuthGuard to any handler or whole controller. The guard runs before the handler: a valid token populates req.user, an invalid or missing one short-circuits with a 401. Read the authenticated user with the @Request() decorator (or a custom @CurrentUser() param decorator).
import { Controller, Get, Request, UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from './jwt-auth.guard';
@Controller('profile')
export class ProfileController {
@UseGuards(JwtAuthGuard)
@Get('me')
getProfile(@Request() req) {
return req.user; // { id, username } from JwtStrategy.validate()
}
}
Trying it out
A request with a valid bearer token returns the decoded user; a missing or expired token returns a 401.
curl -s http://localhost:3000/profile/me \
-H "Authorization: Bearer $ACCESS_TOKEN"
Output:
{"id":1,"username":"ada"}
$ curl -i http://localhost:3000/profile/me
HTTP/1.1 401 Unauthorized
Content-Type: application/json
{"statusCode":401,"message":"Unauthorized"}
Best Practices
- Read
secretOrKeyfrom configuration, never hardcode it, and keepignoreExpiration: falseso expired tokens are rejected. - Keep
validate()fast — it runs on every protected request; cache or skip the DB hit unless you genuinely need a freshness check. - Return only the claims handlers need from
validate(); do not stuff sensitive data into the token payload, since JWTs are signed but not encrypted. - Subclass
AuthGuard('jwt')into a namedJwtAuthGuardrather than repeating the'jwt'string across controllers. - Use the same secret and algorithm in both
JwtModule(signing) andJwtStrategy(verification), or verification will silently fail. - Pair short-lived access tokens with refresh tokens, and assert
issuer/audiencefor defence in depth.