Documentation Index
Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt
Use this file to discover all available pages before exploring further.
Chapter 6: Authentication & Authorization
Securing your application is critical. This chapter covers authentication (verifying identity), authorization (controlling access), JWT, OAuth, RBAC, password hashing, guards, and best practices for building secure APIs in NestJS. We’ll walk through practical examples and explain the “why” behind each step.
6.1 Authentication vs Authorization
Understanding the difference between authentication and authorization is fundamental to building secure applications.
Authentication
Authentication answers: “Who are you?”
- Verifies user identity
- Confirms credentials (username/password, token, etc.)
- Establishes user session
- Examples: Login, token validation
Authorization
Authorization answers: “What are you allowed to do?”
- Controls access to resources
- Enforces permissions and roles
- Determines what actions are allowed
- Examples: Admin-only routes, resource ownership checks
Analogy:
Think of authentication as showing your employee badge at the office entrance (proving who you are). Authorization is the keycard system on individual floors — just because you work at the company does not mean you can enter the server room. In NestJS, JWT guards handle authentication (validating the badge), while Roles guards handle authorization (checking which rooms your badge unlocks). A common mistake is conflating the two: returning 401 (Unauthorized) when you mean 403 (Forbidden). 401 means “I do not know who you are,” 403 means “I know who you are, but you are not allowed here.”
The Relationship
Authentication (Who) → Authorization (What)
↓ ↓
Login → Access Control
Token Validation → Role Checks
Identity Verify → Permission Checks
Authentication Strategy Comparison
| Strategy | Stateless? | Best For | Token Storage | Revocation | Scalability |
|---|
| JWT (Access Token) | Yes | APIs, SPAs, mobile | Client (memory/cookie) | Hard (need blocklist) | Excellent (no server state) |
| JWT + Refresh Token | Hybrid | Production APIs | Access in memory, refresh in httpOnly cookie | Moderate (revoke refresh) | Excellent |
| Session (Cookie) | No | Server-rendered apps | Server (Redis/DB) | Easy (delete session) | Moderate (session store is bottleneck) |
| OAuth2 (Social) | Depends | ”Login with Google/GitHub” | Delegated to provider | Moderate (revoke at provider) | Excellent |
| API Key | Yes | Service-to-service, public APIs | Client config | Easy (delete key from DB) | Excellent |
| mTLS | Yes | Service mesh, zero-trust | Certificate | Moderate (CRL/OCSP) | Excellent |
Decision Framework — Which Auth Strategy?
Is the client a browser SPA?
--> JWT + Refresh Token (access token in memory, refresh in httpOnly cookie)
--> Consider session-based if you need easy revocation (user clicks "log out everywhere")
Is the client a mobile app?
--> JWT + Refresh Token (store refresh token in secure storage)
Is it server-to-server communication?
--> API keys (simple) or mTLS (high security)
Do you need "Login with Google/GitHub"?
--> OAuth2 via Passport strategies (can combine with JWT for your own tokens)
Do you need instant token revocation (e.g., "ban this user immediately")?
--> Session-based auth OR JWT with a Redis blocklist
RBAC vs ABAC vs PBAC
| Model | How Access Is Decided | Complexity | Best For |
|---|
| RBAC (Role-Based) | User’s role matches required role | Low | Most applications (“admin can delete”) |
| ABAC (Attribute-Based) | User/resource/environment attributes evaluated | High | Complex policies (“EU users can only access EU data”) |
| PBAC (Policy-Based) | Policies evaluated at runtime (like ABAC but externalized) | Very High | Enterprises with dynamic compliance rules |
For most NestJS applications, RBAC is sufficient. Start with roles, add permissions if you need finer granularity, and only move to ABAC when your access rules depend on resource attributes (like “owner can edit their own posts but not others”).
6.2 Password Hashing
Before implementing authentication, you must understand password security. Never store passwords in plain text.
Why Hash Passwords?
- Security: Even if database is compromised, passwords are protected
- Privacy: Developers can’t see user passwords
- Compliance: Required by security standards (GDPR, PCI-DSS)
Using bcrypt
bcrypt is the industry standard for password hashing. It is intentionally slow — that is a feature, not a bug. Each hash takes ~100ms, which makes brute-force attacks impractical (an attacker trying 1 billion passwords would need ~3 years). The “salt rounds” parameter controls this slowness: each increment roughly doubles the time.
npm install bcrypt
npm install --save-dev @types/bcrypt
// hash/hash.service.ts
import { Injectable } from '@nestjs/common';
import * as bcrypt from 'bcrypt';
@Injectable()
export class HashService {
async hash(password: string): Promise<string> {
// Salt rounds = 10 means 2^10 (1024) iterations of the hashing algorithm.
// This takes ~100ms on modern hardware -- slow enough to deter brute force,
// fast enough for a normal login flow. Do not go below 10 in production.
// If you need more security, use 12 (at the cost of ~400ms per hash).
const saltRounds = 10;
// bcrypt.hash() automatically generates a random salt and embeds it in
// the output. You do not need to store the salt separately.
return bcrypt.hash(password, saltRounds);
}
async compare(password: string, hash: string): Promise<boolean> {
// bcrypt.compare() extracts the salt from the stored hash and uses it
// to hash the incoming password, then compares the results.
// It is timing-safe, so it does not leak information via response time.
return bcrypt.compare(password, hash);
}
}
Common Mistake: Using bcryptjs (pure JavaScript) instead of bcrypt (native C++ bindings). The pure JS version is 3-5x slower and can block the Node.js event loop under high load. Use the native bcrypt package unless you cannot compile native modules in your environment.
Password Validation
// dto/register.dto.ts
import { IsString, MinLength, Matches } from 'class-validator';
export class RegisterDto {
@IsString()
@MinLength(8)
@Matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/, {
message: 'Password must contain uppercase, lowercase, number, and special character',
})
password: string;
}
6.3 JWT Authentication
JSON Web Tokens (JWT) are a popular way to implement stateless authentication in APIs. JWTs are signed tokens that clients send with each request to prove their identity.
How JWT Works
- User logs in with credentials
- Server validates credentials
- Server creates and signs a JWT
- Client stores the JWT
- Client sends JWT in
Authorization header
- Server verifies JWT and extracts user info
Diagram: JWT Flow
Client → [Login Request] → Server
↓
Server validates credentials
↓
Server creates JWT (signed)
↓
Server returns JWT → Client
↓
Client stores JWT
↓
Client → [Request with JWT] → Server
↓
Server verifies JWT
↓
Server grants access
Installing JWT Package
npm install @nestjs/jwt @nestjs/passport passport passport-jwt
npm install --save-dev @types/passport-jwt
JWT Module Setup
// auth/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 { JwtStrategy } from './strategies/jwt.strategy';
import { LocalStrategy } from './strategies/local.strategy';
@Module({
imports: [
PassportModule,
// JwtModule.register() is a dynamic module that configures the JWT service.
// In production, NEVER use a hardcoded fallback secret -- if JWT_SECRET is
// missing, the app should crash at startup, not silently use an insecure default.
JwtModule.register({
secret: process.env.JWT_SECRET || 'your-secret-key', // Use registerAsync() with ConfigService in production
signOptions: { expiresIn: '1d' }, // Tokens expire after 1 day
}),
],
controllers: [AuthController],
// LocalStrategy and JwtStrategy are providers because Passport needs them
// registered in the DI container to resolve them when guards activate.
providers: [AuthService, JwtStrategy, LocalStrategy],
exports: [AuthService], // Export so other modules can verify tokens
})
export class AuthModule {}
Production Pattern: Use JwtModule.registerAsync() with ConfigService to load the secret from environment variables safely, and validate that it is set before the app starts:
JwtModule.registerAsync({
imports: [ConfigModule],
useFactory: (config: ConfigService) => ({
secret: config.getOrThrow('JWT_SECRET'), // Crashes at startup if missing
signOptions: { expiresIn: '15m' }, // Short-lived access tokens
}),
inject: [ConfigService],
}),
Auth Service
// auth/auth.service.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { UsersService } from '../users/users.service';
import { HashService } from '../hash/hash.service';
import { RegisterDto } from './dto/register.dto';
import { LoginDto } from './dto/login.dto';
@Injectable()
export class AuthService {
constructor(
private usersService: UsersService,
private jwtService: JwtService,
private hashService: HashService,
) {}
async register(registerDto: RegisterDto) {
const hashedPassword = await this.hashService.hash(registerDto.password);
const user = await this.usersService.create({
...registerDto,
password: hashedPassword,
});
const { password, ...result } = user;
return result;
}
async validateUser(email: string, password: string): Promise<any> {
const user = await this.usersService.findByEmail(email);
if (!user) {
throw new UnauthorizedException('Invalid credentials');
}
const isPasswordValid = await this.hashService.compare(password, user.password);
if (!isPasswordValid) {
throw new UnauthorizedException('Invalid credentials');
}
const { password: _, ...result } = user;
return result;
}
async login(loginDto: LoginDto) {
const user = await this.validateUser(loginDto.email, loginDto.password);
const payload = { email: user.email, sub: user.id, roles: user.roles };
return {
access_token: this.jwtService.sign(payload),
user: {
id: user.id,
email: user.email,
name: user.name,
},
};
}
async validateToken(token: string) {
try {
return this.jwtService.verify(token);
} catch (error) {
throw new UnauthorizedException('Invalid token');
}
}
}
JWT Strategy
// auth/strategies/jwt.strategy.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { UsersService } from '../../users/users.service';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private usersService: UsersService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: process.env.JWT_SECRET || 'your-secret-key',
});
}
async validate(payload: any) {
const user = await this.usersService.findOne(payload.sub);
if (!user) {
throw new UnauthorizedException();
}
return { id: user.id, email: user.email, roles: user.roles };
}
}
Local Strategy (Username/Password)
// 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 authService: AuthService) {
super({ usernameField: 'email' });
}
async validate(email: string, password: string): Promise<any> {
const user = await this.authService.validateUser(email, password);
if (!user) {
throw new UnauthorizedException();
}
return user;
}
}
Auth Controller
// auth/auth.controller.ts
import { Controller, Post, Body, UseGuards, Get, Request } from '@nestjs/common';
import { AuthService } from './auth.service';
import { LocalAuthGuard } from './guards/local-auth.guard';
import { JwtAuthGuard } from './guards/jwt-auth.guard';
import { RegisterDto } from './dto/register.dto';
import { LoginDto } from './dto/login.dto';
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
@Post('register')
async register(@Body() registerDto: RegisterDto) {
return this.authService.register(registerDto);
}
@UseGuards(LocalAuthGuard)
@Post('login')
async login(@Body() loginDto: LoginDto, @Request() req) {
return this.authService.login(req.user);
}
@UseGuards(JwtAuthGuard)
@Get('profile')
getProfile(@Request() req) {
return req.user;
}
}
JWT Auth Guard
// auth/guards/jwt-auth.guard.ts
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}
Local Auth Guard
// auth/guards/local-auth.guard.ts
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class LocalAuthGuard extends AuthGuard('local') {}
Using JWT Guard
// users/users.controller.ts
import { Controller, Get, UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
@Controller('users')
@UseGuards(JwtAuthGuard) // Protect all routes
export class UsersController {
@Get()
findAll() {
return [];
}
}
6.4 Authorization: Roles & Permissions
Role-Based Access Control (RBAC) restricts access based on user roles. This lets you control who can do what in your app.
Roles Decorator
// auth/decorators/roles.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
Roles Guard
// auth/guards/roles.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Roles } from '../decorators/roles.decorator';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.get<string[]>('roles', context.getHandler());
if (!requiredRoles) {
return true; // No roles required
}
const request = context.switchToHttp().getRequest();
const user = request.user;
if (!user || !user.roles) {
return false;
}
return requiredRoles.some(role => user.roles.includes(role));
}
}
Using Roles
// users/users.controller.ts
import { Controller, Delete, UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { RolesGuard } from '../auth/guards/roles.guard';
import { Roles } from '../auth/decorators/roles.decorator';
@Controller('users')
@UseGuards(JwtAuthGuard, RolesGuard)
export class UsersController {
@Delete(':id')
@Roles('admin')
remove(@Param('id') id: string) {
return this.usersService.remove(id);
}
}
Permission-Based Authorization
For more granular control, use permissions:
// auth/decorators/permissions.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const Permissions = (...permissions: string[]) =>
SetMetadata('permissions', permissions);
// auth/guards/permissions.guard.ts
@Injectable()
export class PermissionsGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredPermissions = this.reflector.get<string[]>(
'permissions',
context.getHandler(),
);
if (!requiredPermissions) {
return true;
}
const request = context.switchToHttp().getRequest();
const user = request.user;
return requiredPermissions.every(permission =>
user.permissions?.includes(permission)
);
}
}
6.5 OAuth2 & Social Login
NestJS supports OAuth2 via Passport strategies. This lets users log in with Google, Facebook, GitHub, etc.
Installing Passport OAuth
npm install @nestjs/passport passport-google-oauth20
npm install --save-dev @types/passport-google-oauth20
Google Strategy
// auth/strategies/google.strategy.ts
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy, VerifyCallback } from '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: process.env.GOOGLE_CALLBACK_URL,
scope: ['email', 'profile'],
});
}
async validate(
accessToken: string,
refreshToken: string,
profile: any,
done: VerifyCallback,
): Promise<any> {
const { name, emails, photos } = profile;
const user = {
email: emails[0].value,
firstName: name.givenName,
lastName: name.familyName,
picture: photos[0].value,
accessToken,
};
done(null, user);
}
}
Google Auth Controller
// auth/auth.controller.ts
import { Controller, Get, UseGuards, Req } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { AuthService } from './auth.service';
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
@Get('google')
@UseGuards(AuthGuard('google'))
async googleAuth(@Req() req) {
// Initiates Google OAuth flow
}
@Get('google/callback')
@UseGuards(AuthGuard('google'))
async googleAuthRedirect(@Req() req) {
const user = req.user;
// Create or find user in database
const dbUser = await this.authService.findOrCreateUser(user);
// Generate JWT
const token = this.authService.generateToken(dbUser);
return { access_token: token, user: dbUser };
}
}
6.6 Refresh Tokens
Refresh tokens allow users to get new access tokens without re-authenticating.
Implementing Refresh Tokens
// auth/auth.service.ts
async login(loginDto: LoginDto) {
const user = await this.validateUser(loginDto.email, loginDto.password);
const payload = { email: user.email, sub: user.id };
const accessToken = this.jwtService.sign(payload, { expiresIn: '15m' });
const refreshToken = this.jwtService.sign(payload, { expiresIn: '7d' });
// Store refresh token in database
await this.usersService.updateRefreshToken(user.id, refreshToken);
return {
access_token: accessToken,
refresh_token: refreshToken,
user: {
id: user.id,
email: user.email,
},
};
}
async refreshToken(refreshToken: string) {
try {
const payload = this.jwtService.verify(refreshToken);
const user = await this.usersService.findOne(payload.sub);
if (user.refreshToken !== refreshToken) {
throw new UnauthorizedException('Invalid refresh token');
}
const newPayload = { email: user.email, sub: user.id };
return {
access_token: this.jwtService.sign(newPayload, { expiresIn: '15m' }),
};
} catch (error) {
throw new UnauthorizedException('Invalid refresh token');
}
}
6.7 Multi-Factor Authentication (MFA)
Enhance security by requiring a second factor (e.g., OTP, email code) after password login.
MFA Service
// auth/mfa.service.ts
import { Injectable } from '@nestjs/common';
import { UsersService } from '../users/users.service';
import { EmailService } from '../email/email.service';
@Injectable()
export class MfaService {
constructor(
private usersService: UsersService,
private emailService: EmailService,
) {}
async generateOTP(userId: number): Promise<string> {
const otp = Math.floor(100000 + Math.random() * 900000).toString();
const expiresAt = new Date(Date.now() + 10 * 60 * 1000); // 10 minutes
await this.usersService.saveOTP(userId, otp, expiresAt);
await this.emailService.sendOTP(userId, otp);
return otp;
}
async verifyOTP(userId: number, otp: string): Promise<boolean> {
const user = await this.usersService.findOne(userId);
if (!user.otp || user.otp !== otp) {
return false;
}
if (new Date() > user.otpExpiresAt) {
return false;
}
await this.usersService.clearOTP(userId);
return true;
}
}
6.8 Auth Edge Cases
Edge Case 1: JWT token rotation and race conditions
When a client’s access token expires and it sends a refresh request, what happens if two simultaneous requests both try to refresh at the same time? Both carry the same refresh token, but after the first request succeeds and rotates the token, the second request has a stale refresh token. Without handling this, the second request fails and the user gets logged out. Solutions: (1) Allow a short grace period where the old refresh token is still valid; (2) Use a token family approach where any reuse of an old token invalidates the entire family (detecting token theft).
Edge Case 2: The “log out everywhere” problem with JWT
JWTs are stateless — once issued, they are valid until expiration. If a user changes their password and wants to invalidate all existing sessions, you cannot “revoke” a JWT. Solutions: (1) Keep a Redis blocklist of revoked JWTs (checked on every request via a guard); (2) Store a tokenVersion on the user record and increment it on password change — the JWT strategy’s validate() method checks if the token’s version matches the database; (3) Use short-lived access tokens (5-15 minutes) so revocation is eventually consistent.
Edge Case 3: OAuth provider returns inconsistent email
Some OAuth providers (notably Apple Sign In) can return a “private relay” email that is different every time the user signs in from a different app. You cannot rely solely on email for user matching. Store the provider’s unique user ID (sub claim) alongside the email, and match on the provider ID first.
Edge Case 4: Guard execution with GraphQL
GraphQL resolvers do not have the same ExecutionContext as REST controllers. When using @UseGuards(JwtAuthGuard) on a resolver, the guard receives a GraphQL context, not an HTTP request. The JwtStrategy expects ExtractJwt.fromAuthHeaderAsBearerToken(), which reads from the HTTP request. You need to configure the GraphQL module to pass the HTTP request through: GraphQLModule.forRoot({ context: ({ req }) => ({ req }) }).
Edge Case 5: Rate limiting with distributed instances
The @nestjs/throttler module stores rate limit counts in memory by default. If you run 3 instances behind a load balancer, each instance has its own counter — an attacker can make 30 requests (10 per instance) before any instance blocks them. Use the ThrottlerStorageRedisService to share counts across instances.
6.8 Security Best Practices
Following security best practices protects your application and users.
Password Security
- Always hash passwords (use bcrypt with salt rounds >= 10)
- Enforce strong password policies
- Never log or return passwords
- Use password reset tokens (time-limited)
JWT Security
- Use strong, random secrets
- Set appropriate expiration times
- Use HTTPS in production
- Store secrets in environment variables
- Consider refresh tokens for long sessions
// main.ts
import helmet from 'helmet';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.use(helmet());
await app.listen(3000);
}
Rate Limiting
Rate limiting is your first defense against brute-force login attacks and API abuse. Without it, an attacker can try thousands of passwords per second against your login endpoint.
npm install @nestjs/throttler
// app.module.ts
import { ThrottlerModule } from '@nestjs/throttler';
@Module({
imports: [
// Global rate limit: 10 requests per 60 seconds per IP.
// For login endpoints, apply a stricter limit (e.g., 5 per minute).
ThrottlerModule.forRoot({
ttl: 60, // Time window in seconds
limit: 10, // Maximum requests per window
}),
],
})
export class AppModule {}
Practical Tip: Apply different rate limits to different endpoints. Public endpoints can be generous (100/min), but authentication endpoints should be strict (5/min). Use @Throttle() decorator on individual routes to override the global limit:
@Throttle({ default: { limit: 5, ttl: 60 } })
@Post('login')
async login(@Body() loginDto: LoginDto) { ... }
CORS Configuration
// main.ts
app.enableCors({
origin: process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:3000'],
credentials: true,
});
Always validate and sanitize input:
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
}),
);
Security Checklist
- Always hash passwords
- Use HTTPS in production
- Store secrets in environment variables
- Set secure HTTP headers (helmet)
- Limit login attempts (prevent brute force)
- Validate and sanitize all input
- Keep dependencies up to date
- Use CORS to restrict allowed origins
- Log authentication events for auditing
- Implement rate limiting
- Use refresh tokens for long sessions
- Consider MFA for sensitive applications
6.9 Real-World Example: Complete Auth Module
Here’s a complete authentication module structure:
auth/
├── auth.module.ts
├── auth.service.ts
├── auth.controller.ts
├── strategies/
│ ├── jwt.strategy.ts
│ ├── local.strategy.ts
│ └── google.strategy.ts
├── guards/
│ ├── jwt-auth.guard.ts
│ ├── local-auth.guard.ts
│ └── roles.guard.ts
├── decorators/
│ ├── roles.decorator.ts
│ └── permissions.decorator.ts
└── dto/
├── login.dto.ts
├── register.dto.ts
└── refresh-token.dto.ts
6.10 Summary
You’ve learned how to secure your NestJS APIs:
Key Concepts:
- Authentication: Verifying user identity
- Authorization: Controlling access to resources
- JWT: Stateless token-based authentication
- OAuth2: Social login integration
- RBAC: Role-based access control
- Password Hashing: Secure password storage
- MFA: Multi-factor authentication
Best Practices:
- Always hash passwords
- Use HTTPS in production
- Store secrets in environment variables
- Set secure HTTP headers
- Implement rate limiting
- Validate all input
- Use refresh tokens
- Log authentication events
Next Chapter: Learn about testing strategies, including unit tests, integration tests, and E2E tests.