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