🎯 The Core Mental Model
Authentication = "Who are you?" (Proving identity)
Authorization = "What can you do?" (Checking permissions)
Think of it like a hotel:
Authentication: Showing your ID at check-in → You get a key card (JWT token)
Authorization: Your key card only opens your room, not the penthouse → Role-based access
📚 The 3-Layer Architecture (Never Forget This!)
┌─────────────────────────────────────┐
│ 1. GUARDS (Bouncers) │ ← "Can you enter?"
│ - Check if token exists/valid │
│ - Extract user from token │
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ 2. STRATEGIES (ID Checkers) │ ← "How do we verify you?"
│ - JWT Strategy │
│ - Local Strategy (login) │
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ 3. DECORATORS (Smart Labels) │ ← "Who can use this?"
│ - @Public() │
│ - @Roles('admin') │
└─────────────────────────────────────┘🛠️ Step-by-Step Implementation
Step 1: Install Dependencies
npm install @nestjs/passport @nestjs/jwt passport passport-jwt bcrypt
npm install -D @types/passport-jwt @types/bcrypt
Step 2: Auth Module Structure
src/
├── auth/
│ ├── auth.module.ts
│ ├── auth.service.ts
│ ├── auth.controller.ts
│ ├── strategies/
│ │ ├── jwt.strategy.ts
│ │ └── local.strategy.ts
│ ├── guards/
│ │ ├── jwt-auth.guard.ts
│ │ └── roles.guard.ts
│ └── decorators/
│ ├── public.decorator.ts
│ └── roles.decorator.tsStep 3: User Entity (Prisma Example)
model User {
id Int @id @default(autoincrement())
email String @unique
password String // HASHED!
role Role @default(USER)
createdAt DateTime @default(now())
}
enum Role {
USER
ADMIN
MODERATOR
}Step 4: Auth Service - The Brain
// auth.service.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { PrismaService } from '../prisma/prisma.service';
import * as bcrypt from 'bcrypt';
@Injectable()
export class AuthService {
constructor(
private prisma: PrismaService,
private jwtService: JwtService,
) {}
// 🔐 REGISTRATION: Hash password + Save user
async register(email: string, password: string) {
const hashedPassword = await bcrypt.hash(password, 10);
const user = await this.prisma.user.create({
data: {
email,
password: hashedPassword,
},
});
// Remove password from response
const { password: _, ...result } = user;
return result;
}
// 🔑 LOGIN: Validate credentials + Generate token
async validateUser(email: string, password: string) {
const user = await this.prisma.user.findUnique({ where: { email } });
if (!user) {
throw new UnauthorizedException('Invalid credentials');
}
const isPasswordValid = await bcrypt.compare(password, user.password);
if (!isPasswordValid) {
throw new UnauthorizedException('Invalid credentials');
}
const { password: _, ...result } = user;
return result;
}
// 🎫 GENERATE TOKEN: Create JWT
async login(user: any) {
const payload = {
sub: user.id, // "sub" is JWT standard for user ID
email: user.email,
role: user.role
};
return {
access_token: this.jwtService.sign(payload),
user,
};
}
}Step 5: Local Strategy - Login Validation
// strategies/local.strategy.ts
import { Strategy } from 'passport-local';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
import { AuthService } from '../auth.service';
@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(private authService: AuthService) {
super({
usernameField: 'email', // Default is 'username', we use 'email'
});
}
// This runs when login endpoint is hit
async validate(email: string, password: string) {
return await this.authService.validateUser(email, password);
}
}💡 Mental Model: Local Strategy = "Check email/password at login time"
// Step 6: JWT Strategy - Token Validation
// strategies/jwt.strategy.ts
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: process.env.JWT_SECRET, // Store in .env!
});
}
// This runs on EVERY protected route
// Payload = decoded JWT data
async validate(payload: any) {
// This object is attached to request.user
return {
id: payload.sub,
email: payload.email,
role: payload.role
};
}
}
💡 Mental Model: JWT Strategy = "Decode token on every request, attach user to req.user"
Step 7: Guards - The Bouncers
//JWT Auth Guard (Default Protected)// guards/jwt-auth.guard.ts
import { Injectable, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
import { IS_PUBLIC_KEY } from '../decorators/public.decorator';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
constructor(private reflector: Reflector) {
super();
}
canActivate(context: ExecutionContext) {
// Check if route is marked as public
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);
if (isPublic) {
return true; // Skip authentication
}
return super.canActivate(context); // Run JWT validation
}
}//Roles Guard (Check Permissions)
// guards/roles.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ROLES_KEY } from '../decorators/roles.decorator';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
// Get required roles from decorator
const requiredRoles = this.reflector.getAllAndOverride<string[]>(ROLES_KEY, [
context.getHandler(),
context.getClass(),
]);
if (!requiredRoles) {
return true; // No roles required
}
const { user } = context.switchToHttp().getRequest();
// Check if user has ANY of the required roles
return requiredRoles.some((role) => user.role === role);
}
}
Step 8: Decorators - Magic Labels
Public Decorator
// decorators/public.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);Roles Decorator
// decorators/roles.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const ROLES_KEY = 'roles';
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);Current User Decorator (Bonus!)
// decorators/current-user.decorator.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const CurrentUser = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return request.user; // Extracted by JWT strategy
},
);Step 9: Auth Module - Wire Everything
// auth.module.ts
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { LocalStrategy } from './strategies/local.strategy';
import { JwtStrategy } from './strategies/jwt.strategy';
import { PrismaService } from '../prisma/prisma.service';
@Module({
imports: [
PassportModule,
JwtModule.register({
secret: process.env.JWT_SECRET,
signOptions: { expiresIn: '7d' }, // Token expires in 7 days
}),
],
controllers: [AuthController],
providers: [AuthService, LocalStrategy, JwtStrategy, PrismaService],
exports: [AuthService], // Export for use in other modules
})
export class AuthModule {}Step 10: Auth Controller - API Endpoints
// auth.controller.ts
import { Controller, Post, Body, UseGuards, Get } from '@nestjs/common';
import { AuthService } from './auth.service';
import { LocalAuthGuard } from './guards/local-auth.guard';
import { Public } from './decorators/public.decorator';
import { CurrentUser } from './decorators/current-user.decorator';
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
// Public route - No authentication needed
@Public()
@Post('register')
async register(@Body() body: { email: string; password: string }) {
return this.authService.register(body.email, body.password);
}
// Uses LocalStrategy to validate email/password
@Public()
@UseGuards(LocalAuthGuard)
@Post('login')
async login(@CurrentUser() user: any) {
return this.authService.login(user);
}
// Protected route - JWT required (global guard)
@Get('profile')
getProfile(@CurrentUser() user: any) {
return user;
}
}Local Auth Guard (Simple wrapper)
// guards/local-auth.guard.ts
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class LocalAuthGuard extends AuthGuard('local') {}Step 11: App Module - Global Protection
// app.module.ts
import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
import { AuthModule } from './auth/auth.module';
import { JwtAuthGuard } from './auth/guards/jwt-auth.guard';
import { RolesGuard } from './auth/guards/roles.guard';
@Module({
imports: [AuthModule],
providers: [
// Make JWT guard global - all routes protected by default
{
provide: APP_GUARD,
useClass: JwtAuthGuard,
},
// Apply roles guard globally (runs after JWT guard)
{
provide: APP_GUARD,
useClass: RolesGuard,
},
],
})
export class AppModule {}🔥 This is the magic: All routes are now protected unless marked @Public()!
🎨 Usage Examples
Example 1: Public Route
@Public()
@Get('products')
getAllProducts() {
return this.productsService.findAll();
}Example 2: Protected Route (Any logged-in user)
@Get('my-orders')
getMyOrders(@CurrentUser() user: any) {
return this.ordersService.findByUserId(user.id);
}Example 3: Admin-Only Route
@Roles('ADMIN')
@Delete('users/:id')
deleteUser(@Param('id') id: string) {
return this.usersService.delete(id);
}Example 4: Multi-Role Access
@Roles('ADMIN', 'MODERATOR')
@Post('posts/:id/approve')
approvePost(@Param('id') id: string) {
return this.postsService.approve(id);
}🔐 Environment Variables (.env)
env
JWT_SECRET=your-super-secret-key-change-this-in-production
JWT_EXPIRATION=7d
DATABASE_URL=postgresql://user:password@localhost:5432/mydb⚠️ CRITICAL: Never commit .env to Git! Use strong, random JWT_SECRET in production.
🚀 Production Best Practices
1. Refresh Tokens (For Long Sessions)
// Add refresh token to User model
model User {
//... other fields
refreshToken String?
}
// Generate both tokens
async login(user: any) {
const accessToken = this.jwtService.sign(payload, { expiresIn: '15m' });
const refreshToken = this.jwtService.sign(payload, { expiresIn: '7d' });
// Store refresh token hash in DB
await this.prisma.user.update({
where: { id: user.id },
data: { refreshToken: await bcrypt.hash(refreshToken, 10) },
});
return { access_token: accessToken, refresh_token: refreshToken };
}2. Password Requirements
import { IsEmail, IsStrongPassword } from 'class-validator';
export class RegisterDto {
@IsEmail()
email: string;
@IsStrongPassword({
minLength: 8,
minLowercase: 1,
minUppercase: 1,
minNumbers: 1,
minSymbols: 1,
})
password: string;
}3. Rate Limiting (Prevent Brute Force)
bash
npm install @nestjs/throttler// app.module.ts
import { ThrottlerModule } from '@nestjs/throttler';
@Module({
imports: [
ThrottlerModule.forRoot([{
ttl: 60000, // 1 minute
limit: 10, // 10 requests per minute
}]),
],
})4. Logout (Invalidate Token)
@Post('logout')
async logout(@CurrentUser() user: any) {
// Clear refresh token from DB
await this.prisma.user.update({
where: { id: user.id },
data: { refreshToken: null },
});
return { message: 'Logged out successfully' };
}🧠 Mental Checklist (Never Forget!)
✅ Authentication Flow:
User sends email/password → LocalStrategy validates → Generate JWT
User sends JWT in header → JwtStrategy decodes → Attach user to req.user
Guards check if user exists + has correct role
✅ Authorization Flow:
JwtAuthGuard: "Is user logged in?"
RolesGuard: "Does user have required role?"
✅ Security Rules:
Hash passwords with bcrypt (never plain text)
Use strong JWT_SECRET (random, 32+ characters)
Set token expiration (15min access, 7d refresh)
Validate all inputs with class-validator
Use HTTPS in production
Add rate limiting to login/register
📝 Quick Reference Card
// Make route public
@Public()
// Require authentication (default with global guard)
// No decorator needed!
// Require specific role
@Roles('ADMIN')
// Get current user
@CurrentUser() user
// Login flow
LocalStrategy → validate() → AuthService.login() → JWT
// Protected route flow
JWT in header → JwtStrategy.validate() → req.user → Guards checkThis architecture is production-ready, scalable, and follows NestJS best practices. The mental models (hotel key cards, bouncers, ID checkers) will stick with you forever! 🎯