Skip to content
NestJS ns auth 5 min read

Passport Integration

Passport is the most popular authentication library in the Node.js ecosystem, and NestJS ships a first-class wrapper around it through the @nestjs/passport package. Rather than wiring Passport’s middleware-based API by hand, you express each authentication mechanism as a NestJS provider (a strategy) and protect routes declaratively with an AuthGuard. This page covers the installation, the mental model behind strategies, the PassportStrategy mixin, and how the result of validate() ends up on the request object.

Installing the packages

You need the framework wrapper, the core Passport runtime, and a concrete strategy package. The example below uses the local (username/password) strategy, but the workflow is identical for passport-jwt, passport-google-oauth20, and the rest.

npm install @nestjs/passport passport passport-local
npm install -D @types/passport-local

Passport itself is unopinionated about where credentials come from. The strategy package you install determines that — passport-local reads a request body, passport-jwt reads a bearer token, and so on. You can register as many strategies as you need.

How strategies fit into NestJS

In vanilla Passport you call passport.use(new Strategy(...)) and register middleware imperatively. NestJS inverts this: a strategy is just an @Injectable() provider. When the class is instantiated by the DI container, the PassportStrategy mixin automatically calls passport.use() for you and registers the strategy under a name. Because it is a regular provider, your strategy can inject other services — a UsersService, a config service, a repository — through the constructor.

The contract every strategy must fulfil is a validate() method. Passport invokes it after it has parsed and verified the raw credentials; your job is to confirm the user exists and return whatever should represent the authenticated principal. Returning a value signals success; throwing an exception signals failure.

Building a strategy with the PassportStrategy mixin

PassportStrategy is a mixin factory — you extend PassportStrategy(Strategy), passing the concrete strategy class imported from the underlying package. The second argument is an optional custom name; if omitted it defaults to the strategy’s own name ('local', 'jwt', etc.).

// auth/local.strategy.ts
import { Strategy } from 'passport-local';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { AuthService } from './auth.service';
import { User } from '../users/user.entity';

@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
  constructor(private readonly authService: AuthService) {
    // passport-local defaults to fields named "username" and "password".
    super({ usernameField: 'email' });
  }

  async validate(email: string, password: string): Promise<Omit<User, 'password'>> {
    const user = await this.authService.validateUser(email, password);
    if (!user) {
      throw new UnauthorizedException('Invalid credentials');
    }
    return user;
  }
}

The arguments to validate() are determined by the underlying strategy. For passport-local they are the username and password fields; for passport-jwt it is the decoded token payload. Whatever you return becomes the authenticated user.

Registering the strategy in a module

Import PassportModule and declare the strategy as a provider. The module wiring is what triggers the DI container to construct the strategy and register it with Passport.

// auth/auth.module.ts
import { Module } from '@nestjs/common';
import { PassportModule } from '@nestjs/passport';
import { AuthService } from './auth.service';
import { LocalStrategy } from './local.strategy';
import { UsersModule } from '../users/users.module';

@Module({
  imports: [UsersModule, PassportModule],
  providers: [AuthService, LocalStrategy],
})
export class AuthModule {}

Protecting routes with AuthGuard

AuthGuard(name) is a factory that returns a guard wired to a named strategy. Apply it with @UseGuards() on a controller or handler. When a request hits the route, the guard runs the strategy, which in turn calls your validate() — only on success does the handler execute.

// auth/auth.controller.ts
import { Controller, Post, Request, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Controller('auth')
export class AuthController {
  @UseGuards(AuthGuard('local'))
  @Post('login')
  login(@Request() req) {
    // req.user is populated by the strategy's validate() return value.
    return { user: req.user };
  }
}

Output:

POST /auth/login  { "email": "[email protected]", "password": "secret" }

200 OK
{
  "user": { "id": 1, "email": "[email protected]", "name": "Ada" }
}

How the validate() result reaches req.user

This is the key piece that ties everything together. After AuthGuard runs the strategy and validate() returns successfully, Passport attaches the return value to the request object under a property — by default request.user. From that point on, any pipe, interceptor, or handler downstream can read it, and you typically expose it through a custom @User() parameter decorator.

ElementResponsibility
PassportModuleRegisters Passport infrastructure with Nest’s DI container
Strategy classExtends PassportStrategy(...), implements validate()
validate()Verifies credentials and returns the principal (or throws)
AuthGuard('name')Activates the named strategy for a route
request.userHolds whatever validate() returned

You can change the property name and other behaviour by passing options to AuthGuard, or override getRequest() / handleRequest() by subclassing the guard for advanced cases such as GraphQL or WebSocket contexts.

// auth/jwt-auth.guard.ts — customising the guard
import { ExecutionContext, Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
  handleRequest(err, user, info) {
    if (err || !user) {
      throw err ?? new UnauthorizedException();
    }
    return user; // becomes request.user
  }
}

Best Practices

  • Keep each strategy focused on a single mechanism (local, jwt, google) and give it a clear name so guards read self-documentingly.
  • Never return the password hash or other secrets from validate() — strip them so they cannot leak into request.user and out through responses.
  • Throw UnauthorizedException (not a generic Error) from validate() so clients receive a proper 401.
  • Register PassportModule in every feature module that uses guards, or configure it globally with PassportModule.register({ defaultStrategy: 'jwt' }).
  • Subclass AuthGuard when you need custom error handling or non-HTTP transports, rather than scattering logic across handlers.
  • Pair Passport with a custom @User() parameter decorator to access request.user in a type-safe, testable way.
Last updated June 14, 2026
Was this helpful?