Skip to content
NestJS ns auth 4 min read

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.

OptionPurpose
jwtFromRequestAn extractor function that locates the token. ExtractJwt.fromAuthHeaderAsBearerToken() reads Authorization: Bearer <token>.
secretOrKeySymmetric secret (HS256) used to verify the signature. Use secretOrKeyProvider / publicKey for RS256.
ignoreExpirationLeave false so Passport rejects expired tokens automatically.
issuer / audienceOptional extra claims Passport will assert during verification.

Warning: secretOrKey must match the secret you signed tokens with, and it must never be hardcoded. Read it from ConfigService or process.env so 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 secretOrKey from configuration, never hardcode it, and keep ignoreExpiration: false so 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 named JwtAuthGuard rather than repeating the 'jwt' string across controllers.
  • Use the same secret and algorithm in both JwtModule (signing) and JwtStrategy (verification), or verification will silently fail.
  • Pair short-lived access tokens with refresh tokens, and assert issuer/audience for defence in depth.
Last updated June 14, 2026
Was this helpful?