NestJS Passport.js

Passport.js is the most widely used authentication middleware for Node.js. It supports over 500 authentication strategies — JWT, Google OAuth, GitHub, Facebook, local username/password, and more. NestJS integrates Passport through the @nestjs/passport package, which wraps each strategy into a clean, injectable service that works with NestJS's dependency injection and guard system.

How Passport Works in NestJS

Client Request with credentials
         |
         v
  Passport Guard triggers the strategy
         |
         v
  Strategy extracts credentials from request
  (token from header, username/password from body, etc.)
         |
         v
  Strategy's validate() method runs
  (verify token, check password, look up user)
         |
         v
  Validated user attached to req.user
         |
         v
  Route handler executes

The Two Main Strategies

Local Strategy — Username and Password

The local strategy handles traditional login with email and password. It reads the credentials from the request body and calls your validate method:

import { Strategy } from 'passport-local';
import { PassportStrategy } from '@nestjs/passport';

@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
  constructor(private authService: AuthService) {
    super({ usernameField: 'email' });  // use 'email' instead of 'username'
  }

  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
  }
}

JWT Strategy — Token-Based Auth

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

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      secretOrKey: process.env.JWT_SECRET,
    });
  }

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

Registering Strategies

Each strategy is a provider. Register it in the auth module:

@Module({
  imports: [
    UsersModule,
    PassportModule,
    JwtModule.register({ secret: process.env.JWT_SECRET, signOptions: { expiresIn: '1h' } }),
  ],
  providers: [AuthService, LocalStrategy, JwtStrategy],
  controllers: [AuthController],
})
export class AuthModule {}

Creating Guards for Each Strategy

// local.guard.ts — used on the login route
@Injectable()
export class LocalAuthGuard extends AuthGuard('local') {}

// jwt.guard.ts — used on protected routes
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}

The string argument ('local', 'jwt') identifies which Passport strategy the guard uses. When the guard activates, Passport finds the matching registered strategy and runs it.

Auth Flow Diagram

Login Flow:
POST /auth/login { email, password }
  → LocalAuthGuard triggers LocalStrategy
  → LocalStrategy.validate(email, password) runs
  → User returned → req.user set
  → AuthController.login(req.user) returns JWT token

Protected Route Flow:
GET /profile  Authorization: Bearer <token>
  → JwtAuthGuard triggers JwtStrategy
  → JwtStrategy reads token from header
  → JwtStrategy.validate(payload) runs
  → { id, email } returned → req.user set
  → ProfileController.getProfile(req.user) runs

Google OAuth Strategy

Passport's real power shows with third-party OAuth strategies. Adding Google login requires the passport-google-oauth20 package:

npm install passport-google-oauth20 @types/passport-google-oauth20

@Injectable()
export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
  constructor() {
    super({
      clientID: process.env.GOOGLE_CLIENT_ID,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET,
      callbackURL: 'http://localhost:3000/auth/google/callback',
      scope: ['email', 'profile'],
    });
  }

  async validate(accessToken: string, refreshToken: string, profile: any) {
    const { name, emails } = profile;
    return {
      email: emails[0].value,
      name: `${name.givenName} ${name.familyName}`,
    };
  }
}

// Routes
@Get('google')
@UseGuards(AuthGuard('google'))
googleAuth() {}  // redirects to Google login page

@Get('google/callback')
@UseGuards(AuthGuard('google'))
googleCallback(@Req() req) {
  return this.authService.login(req.user);
}

Accessing the Current User in Controllers

@Get('profile')
@UseGuards(JwtAuthGuard)
getProfile(@Req() req: Request) {
  return req.user;  // populated by Passport after strategy runs
}

For cleaner code, create a custom @CurrentUser() decorator that extracts req.user automatically — eliminating the need to use @Req() and access the user manually from the request object in every controller method.

Why Passport in NestJS

Passport standardizes authentication across strategies. Adding a new login method — GitHub, Facebook, Apple — follows the same pattern: install the strategy package, create a strategy class, register it as a provider, create a guard. The rest of your application — guards, controllers, services — stays unchanged. Passport absorbs the complexity of each authentication protocol behind a consistent interface.

Leave a Comment

Your email address will not be published. Required fields are marked *