Register and Login with Passport

Packages and the Auth Module

Example

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

Example output

added 14 packages in 5s

src/auth/auth.module.ts

import { Module } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { UsersModule } from '../users/users.module';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { LocalStrategy } from './strategies/local.strategy';
import { JwtStrategy } from './strategies/jwt.strategy';

@Module({
  imports: [
    UsersModule,
    PassportModule,
    JwtModule.registerAsync({
      inject: [ConfigService],
      useFactory: (config: ConfigService) => ({
        secret: config.getOrThrow<string>('JWT_SECRET'),
        signOptions: { expiresIn: '15m' },
      }),
    }),
  ],
  controllers: [AuthController],
  providers: [AuthService, LocalStrategy, JwtStrategy],
})
export class AuthModule {}

AuthService

src/auth/auth.service.ts

import { ConflictException, Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import * as bcrypt from 'bcrypt';
import { UsersService } from '../users/users.service';
import { RegisterDto } from './dto/register.dto';

export interface JwtPayload { sub: number; email: string; roles: string[] }

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

  async register(dto: RegisterDto) {
    if (await this.users.findByEmail(dto.email)) throw new ConflictException('Email already registered');
    const passwordHash = await bcrypt.hash(dto.password, 12);
    const user = await this.users.create({ name: dto.name, email: dto.email, passwordHash, roles: ['customer'] });
    return this.issueTokens(user);
  }

  // used by LocalStrategy
  async validateUser(email: string, password: string) {
    const user = await this.users.findByEmail(email);
    if (!user || !(await bcrypt.compare(password, user.passwordHash))) return null;
    return user;
  }

  async issueTokens(user: { id: number; email: string; roles: string[] }) {
    const payload: JwtPayload = { sub: user.id, email: user.email, roles: user.roles };
    return { accessToken: await this.jwt.signAsync(payload), tokenType: 'Bearer', expiresIn: 900 };
  }
}

Local Strategy and the Login Route

The local strategy reads email and password from the body and calls validate(). Whatever it returns becomes req.user.

src/auth/strategies/local.strategy.ts

import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-local';
import { AuthService } from '../auth.service';

@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
  constructor(private readonly auth: AuthService) {
    super({ usernameField: 'email' });          // default is "username"
  }

  async validate(email: string, password: string) {
    const user = await this.auth.validateUser(email, password);
    if (!user) throw new UnauthorizedException('Invalid email or password');
    return user;
  }
}

src/auth/auth.controller.ts

import { Body, Controller, HttpCode, Post, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { AuthService } from './auth.service';
import { RegisterDto } from './dto/register.dto';
import { CurrentUser } from './current-user.decorator';
import { Public } from './public.decorator';

@Controller('auth')
export class AuthController {
  constructor(private readonly auth: AuthService) {}

  @Public()
  @Post('register')
  register(@Body() dto: RegisterDto) {
    return this.auth.register(dto);
  }

  @Public()
  @UseGuards(AuthGuard('local'))
  @HttpCode(200)
  @Post('login')
  login(@CurrentUser() user: { id: number; email: string; roles: string[] }) {
    return this.auth.issueTokens(user);
  }
}

Example

curl -X POST http://localhost:3000/auth/login -H "Content-Type: application/json" \
  -d '{"email":"[email protected]","password":"Shop@2026"}'
curl -i -X POST http://localhost:3000/auth/login -H "Content-Type: application/json" \
  -d '{"email":"[email protected]","password":"wrong"}'

Example output

{"accessToken":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjEyLCJlbWFpbCI6ImFzaGFAZXhhbXBsZS5jb20iLCJyb2xlcyI6WyJjdXN0b21lciJdLCJpYXQiOjE3Njg0NzEyMDAsImV4cCI6MTc2ODQ3MjEwMH0.3q9...","tokenType":"Bearer","expiresIn":900}

HTTP/1.1 401 Unauthorized
{"message":"Invalid email or password","error":"Unauthorized","statusCode":401}