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-localreads a request body,passport-jwtreads 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.
| Element | Responsibility |
|---|---|
PassportModule | Registers Passport infrastructure with Nest’s DI container |
| Strategy class | Extends PassportStrategy(...), implements validate() |
validate() | Verifies credentials and returns the principal (or throws) |
AuthGuard('name') | Activates the named strategy for a route |
request.user | Holds 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 intorequest.userand out through responses. - Throw
UnauthorizedException(not a genericError) fromvalidate()so clients receive a proper401. - Register
PassportModulein every feature module that uses guards, or configure it globally withPassportModule.register({ defaultStrategy: 'jwt' }). - Subclass
AuthGuardwhen you need custom error handling or non-HTTP transports, rather than scattering logic across handlers. - Pair Passport with a custom
@User()parameter decorator to accessrequest.userin a type-safe, testable way.