Suggest an editImprove this articleRefine the answer for “How to implement authentication with Passport in NestJS?”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**NestJS Passport authentication** - strategies contain validation logic (JWT verification, password checking), guards apply them to routes. Install `@nestjs/passport`, `passport-jwt`, `passport-local`, `@nestjs/jwt`. Define a `JwtStrategy` extending `PassportStrategy(Strategy)` with a `validate()` method, register it in `AuthModule`, and protect routes with `@UseGuards(AuthGuard('jwt'))`. ```typescript @Injectable() export class JwtStrategy extends PassportStrategy(Strategy) { constructor() { super({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), secretOrKey: process.env.JWT_SECRET }); } async validate(payload: { sub: string; email: string }) { return { id: payload.sub, email: payload.email }; // becomes req.user } } ``` **Key point:** the `validate()` return value becomes `req.user`. A missing `PassportModule` import breaks all strategies at runtime.Shown above the full answer for quick recall.Answer (EN)Image**NestJS Passport authentication** - a pattern where strategy classes (JWT, local, OAuth2) contain validation logic, and guards decide which routes require it. ## Theory ### TL;DR - Passport works like airport security: each strategy is a separate checkpoint (ID check, token scanner), guards open or block the gate - Strategies answer *how* to validate (verify JWT signature, compare bcrypt hash); guards answer *when* to run them - Use local strategy only at the login endpoint; use JWT strategy for every other protected route - Four packages: `@nestjs/passport`, `passport-local`, `passport-jwt`, `@nestjs/jwt` - Missing `PassportModule` in imports is the most common setup failure ### Quick setup Install the packages: ```bash npm install @nestjs/passport passport passport-local passport-jwt @nestjs/jwt npm install --save-dev @types/passport-local @types/passport-jwt ``` Then wire up the module: ```typescript // auth.module.ts @Module({ imports: [ PassportModule, JwtModule.register({ secret: process.env.JWT_SECRET, signOptions: { expiresIn: '1h' }, }), UsersModule, ], providers: [AuthService, JwtStrategy, LocalStrategy], controllers: [AuthController], }) export class AuthModule {} ``` `PassportModule` binds strategies to the Express layer. Without it, `AuthGuard('jwt')` fails at runtime with no clear error. ### How strategies and guards connect A strategy is a provider class that extends `PassportStrategy(Strategy)` and implements `validate()`. When a request hits a guarded route, `AuthGuard('jwt')` calls `passport.authenticate('jwt')` internally. Passport finds the registered strategy, verifies the token, and attaches the result to `req.user`. The guard triggers the strategy. The strategy does the work. That separation is what makes swapping strategies later painless. ### Local strategy (login endpoint only) ```typescript // local.strategy.ts import { Strategy } from 'passport-local'; import { PassportStrategy } from '@nestjs/passport'; @Injectable() export class LocalStrategy extends PassportStrategy(Strategy) { constructor(private authService: AuthService) { super({ usernameField: 'email' }); // override default 'username' field } async validate(email: string, password: string): Promise<any> { const user = await this.authService.validateUser(email, password); if (!user) throw new UnauthorizedException('Invalid credentials'); return user; // attached to req.user } } ``` The service-level check uses bcrypt: ```typescript // auth.service.ts (validateUser) async validateUser(email: string, password: string) { const user = await this.usersService.findByEmail(email); if (user && await bcrypt.compare(password, user.password)) { const { password, ...result } = user; return result; // never return the raw password field } return null; } ``` ### JWT strategy (protected routes) After login the client stores the token and sends it as a Bearer header on every subsequent request. The JWT strategy verifies the signature and extracts the payload: ```typescript // jwt.strategy.ts import { ExtractJwt, Strategy } from 'passport-jwt'; import { PassportStrategy } from '@nestjs/passport'; @Injectable() export class JwtStrategy extends PassportStrategy(Strategy) { constructor() { super({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), ignoreExpiration: false, // expired tokens return 401 automatically secretOrKey: process.env.JWT_SECRET, }); } async validate(payload: { sub: string; email: string }) { return { id: payload.sub, email: payload.email }; // becomes req.user } } ``` `ignoreExpiration: false` is the correct default. Setting it to `true` in development and forgetting to revert it is a real security hole. ### Auth service: issuing tokens ```typescript async login(user: any) { const payload = { sub: user.id, email: user.email }; return { access_token: this.jwtService.sign(payload), refresh_token: this.jwtService.sign(payload, { expiresIn: '7d' }), }; } ``` ### Controller: tying it together ```typescript // auth.controller.ts @Controller('auth') export class AuthController { constructor(private authService: AuthService) {} @UseGuards(AuthGuard('local')) @Post('login') async login(@Request() req) { return this.authService.login(req.user); // req.user set by LocalStrategy } @UseGuards(AuthGuard('jwt')) @Get('profile') getProfile(@Request() req) { return req.user; // set by JwtStrategy } } ``` ### Global JWT guard with a @Public() escape hatch Annotating every route with `@UseGuards(AuthGuard('jwt'))` gets repetitive. A better pattern: apply the guard globally and mark open routes with a `@Public()` decorator. ```typescript // jwt-auth.guard.ts @Injectable() export class JwtAuthGuard extends AuthGuard('jwt') { constructor(private reflector: Reflector) { super(); } canActivate(context: ExecutionContext) { const isPublic = this.reflector.getAllAndOverride<boolean>('isPublic', [ context.getHandler(), context.getClass(), ]); if (isPublic) return true; return super.canActivate(context); } } ``` Register it in `AppModule`: ```typescript @Module({ providers: [ { provide: APP_GUARD, useClass: JwtAuthGuard }, ], }) export class AppModule {} ``` Now every route is protected by default. Add `@Public()` to skip the check for login, health, or any open endpoint. ### When to use which strategy - Stateless API (mobile or SPA clients): JWT strategy, no server sessions needed - Login endpoint: local strategy to verify credentials, then issue JWT immediately after - Social login: `passport-google-oauth20` or similar OAuth2 strategy - Internal microservices: custom strategy with API key extraction - Simple session-based web app with no API: skip Passport, use `@nestjs/session` directly ### Common mistakes **Forgetting `PassportModule` in imports.** Strategies register through it. Without the import, guards fail at runtime and the error message points nowhere useful. ```typescript // Wrong @Module({ imports: [JwtModule.register({...})] }) // Fix @Module({ imports: [PassportModule, JwtModule.register({...})] }) ``` **Hardcoding the JWT secret.** Any `git log` exposes it. Every token ever signed becomes forgeable. ```typescript // Wrong secretOrKey: 'my-secret' // Fix secretOrKey: process.env.JWT_SECRET ``` **`validate()` returning null without throwing.** Passport treats a null return as auth failure, but if you do not throw `UnauthorizedException`, the resulting error message is hard to trace. ```typescript // Wrong: silent failure path async validate(payload: any) { return this.usersService.findById(payload.sub); // might return null } // Fix async validate(payload: any) { const user = await this.usersService.findById(payload.sub); if (!user) throw new UnauthorizedException(); return user; } ``` **Setting `ignoreExpiration: true`.** Works fine locally. In production, expired tokens pass as valid indefinitely. **Using local strategy for regular API requests.** Local strategy reads credentials from the request body. It belongs only at the login endpoint. For everything else, use JWT. ### Request flow at a glance ``` POST /auth/login Body: { email, password } -> AuthGuard('local') triggers LocalStrategy.validate() -> AuthService.validateUser() checks bcrypt hash -> Returns { access_token, refresh_token } GET /api/profile Header: Authorization: Bearer <token> -> AuthGuard('jwt') triggers JwtStrategy.validate() -> JWT signature verified, payload decoded -> req.user populated -> controller runs -> 401 if token missing, expired, or tampered ``` ### Real-world usage - E-commerce APIs (similar to MedusaJS patterns): JWT for cart and order endpoints, local strategy only at admin login - Prisma + NestJS starters: custom JWT strategy injected into GraphQL context - NestJS API gateway: API key strategy between internal microservices - SaaS dashboards: `passport-google-oauth20` for social login, JWT for session after OAuth callback For JWT logout: stateless tokens cannot be revoked by design. The production pattern is to add a `jti` (JWT ID) claim, store revoked IDs in Redis with the token's remaining TTL, and check the blacklist inside `JwtStrategy.validate()`. ### Follow-up questions **Q:** Walk me through what happens when a request hits a `@UseGuards(AuthGuard('jwt'))` route. **A:** The guard calls `canActivate()`, which calls `passport.authenticate('jwt')`. Passport finds the registered `JwtStrategy`, extracts the Bearer token, verifies the signature against `secretOrKey`, and if valid calls `validate(payload)`. The return value becomes `req.user`. If anything fails, Passport returns 401 before the controller runs. **Q:** What is the difference between `AuthGuard('jwt')` and a class that extends `AuthGuard('jwt')`? **A:** The string version calls the strategy by name with no customization. Extending `AuthGuard('jwt')` lets you override `canActivate()` or `handleRequest()` to add logging, throttling, role checks, or the `@Public()` reflector pattern without touching controller code. **Q:** How do you implement JWT logout? **A:** JWT tokens are stateless, so the server cannot invalidate them directly. Add a `jti` claim when signing, store revoked `jti` values in Redis with the token's remaining TTL, and reject matching tokens in `validate()`. This is the standard blacklist approach. **Q:** Why use `@nestjs/passport` over raw `@nestjs/jwt`? **A:** `@nestjs/jwt` signs and verifies tokens. Passport adds the request integration layer: it extracts the token, calls `validate()`, and sets `req.user`. It also gives you one consistent interface across multiple strategies (local, JWT, OAuth2) without any changes to controller code. **Q:** Senior: how would you handle concurrent logins across devices and invalidate sessions on password change? **A:** Embed a `version` field from the user record into the JWT payload. On password change, increment the version in the database. In `JwtStrategy.validate()`, load the user and compare versions. If the token's version does not match, throw `UnauthorizedException`. This invalidates all existing tokens without a blacklist. ## Examples ### Basic: login and protected profile endpoint The minimal working setup. User logs in with email and password, receives a JWT, uses it on protected endpoints. ```typescript // auth.controller.ts @Controller('auth') export class AuthController { constructor(private authService: AuthService) {} @UseGuards(AuthGuard('local')) @Post('login') async login(@Request() req) { // req.user comes from LocalStrategy.validate() after bcrypt check return this.authService.login(req.user); // Output: { access_token: 'eyJ...', refresh_token: 'eyJ...' } } @UseGuards(AuthGuard('jwt')) @Get('profile') getProfile(@Request() req) { return req.user; // Output: { id: '123', email: 'user@example.com' } // Without valid Bearer token: 401 Unauthorized } } ``` On login, `LocalStrategy` calls `validateUser()`, which compares the bcrypt hash. On success, `AuthService.login()` signs a JWT with `sub` (user ID) and `email`. On the profile request, `JwtStrategy` decodes that payload and returns it as `req.user`. ### Intermediate: role-based access with a custom guard After JWT verification you often need to check the user's role. `handleRequest()` runs after Passport validation and is the right place for that: ```typescript // admin.guard.ts @Injectable() export class AdminGuard extends AuthGuard('jwt') { handleRequest(err: any, user: any) { if (err || !user) throw err || new UnauthorizedException(); if (!user.roles?.includes('admin')) { throw new ForbiddenException('Admin access required'); } return user; } } // controller @UseGuards(AdminGuard) @Get('admin/dashboard') getDashboard(@Request() req) { return { user: req.user, data: 'admin content' }; // 401 if no token, 403 if token valid but role is not 'admin' } ``` This keeps role logic out of the controller and out of the strategy. Each layer has one job. ### Advanced: refresh token strategy with a named strategy and custom extractor Access tokens expire in minutes. Refresh tokens live longer and rotate on use. A separate named Passport strategy handles this cleanly: ```typescript // refresh.strategy.ts @Injectable() export class RefreshStrategy extends PassportStrategy(Strategy, 'refresh') { // 'refresh' is the strategy name for AuthGuard('refresh') constructor() { super({ jwtFromRequest: ExtractJwt.fromBodyField('refreshToken'), // reads from POST body ignoreExpiration: false, secretOrKey: process.env.REFRESH_SECRET, // different secret than access token }); } async validate(payload: any) { // Production: check Redis blacklist by payload.jti here return { userId: payload.sub }; } } // controller @UseGuards(AuthGuard('refresh')) @Post('auth/refresh') async refresh(@Request() req) { // req.user = { userId: '...' } from RefreshStrategy.validate() return this.authService.rotateTokens(req.user.userId); // Output: { access_token: '...', refresh_token: '...' (new, old invalidated) } } ``` The second argument to `PassportStrategy(Strategy, 'refresh')` is critical. Without a unique name, the second strategy overwrites the first and `AuthGuard('jwt')` stops working.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.