JWT Authentication

Stateless Tokens for APIs

JWT Authentication

Issue a JWT on login, verify it on every request via a JwtStrategy, and guard routes that need an authenticated user.

4 min read Level 2/5 #nestjs#jwt#auth
What you'll learn
  • Configure JwtModule.registerAsync
  • Implement a JwtStrategy
  • Use a JwtAuthGuard on routes

JWT (JSON Web Token) auth is stateless: the server signs a token, the client stores it, and every request carries it in an Authorization header. No session table, no sticky sessions, no Redis required.

Install

npm i @nestjs/jwt @nestjs/passport passport passport-jwt
npm i -D @types/passport-jwt

Configure JwtModule

JwtModule.registerAsync is the same async-factory pattern as the database modules. The secret comes from config, never from code.

@Module({
  imports: [
    PassportModule,
    JwtModule.registerAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: (cfg: ConfigService) => ({
        secret: cfg.get<string>('JWT_SECRET'),
        signOptions: { expiresIn: '15m' },
      }),
    }),
  ],
  providers: [AuthService, JwtStrategy],
})
export class AuthModule {}

Fifteen minutes is a reasonable default for access tokens. Pair it with a longer-lived refresh token if you need persistent sessions.

Sign on Login

@Injectable()
export class AuthService {
  constructor(private readonly jwt: JwtService) {}

  async login(user: User) {
    const payload = { sub: user.id, email: user.email };
    return { accessToken: await this.jwt.signAsync(payload) };
  }
}

The sub (subject) claim is the standard place for the user id. Don’t shove the entire user object into the token — keep it small, since every request carries it.

Verify on Every Request

passport-jwt does the heavy lifting: extract from header, verify signature, parse the payload. You only implement validate.

import { ExtractJwt, Strategy } from 'passport-jwt';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(cfg: ConfigService) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      secretOrKey: cfg.get<string>('JWT_SECRET'),
    });
  }

  validate(payload: { sub: number; email: string }) {
    return { id: payload.sub, email: payload.email };
  }
}

Whatever validate returns becomes req.user. Some teams query the database here to confirm the user still exists — fine for security but adds a query to every request.

Guard Your Routes

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

@Controller('me')
@UseGuards(JwtAuthGuard)
export class MeController {
  @Get()
  whoami(@Request() req) {
    return req.user;
  }
}

Apply the guard globally with APP_GUARD if most routes need auth, and create a @Public() decorator for the few that don’t.

Roles & Permissions (RBAC) →