React & Next.js Topic
NestJs Authentication & Authorization
1. Authentication Overview
Authentication verifies who a user is, while Authorization determines what they can do.
Flow:
-
User registers with email/password
-
Password is hashed and stored
-
User logs in with credentials
-
Server validates and returns JWT
-
Subsequent requests include JWT in Authorization header
-
Server verifies JWT and grants access
2. Project Setup & Dependencies
Install Required Packages:
bash
# Core authentication packages npm install @nestjs/passport passport passport-local passport-jwt npm install @nestjs/jwt bcryptjs npm install @types/passport-local @types/passport-jwt @types/bcryptjs --save-dev # Optional for session-based auth npm install express-session connect-redis npm install @types/express-session @types/connect-redis --save-dev # For TypeORM (if using database) npm install @nestjs/typeorm typeorm pg
Module Structure:
text
src/
├── auth/
│ ├── auth.module.ts
│ ├── auth.controller.ts
│ ├── auth.service.ts
│ ├── interfaces/
│ │ └── jwt-payload.interface.ts
│ ├── strategies/
│ │ ├── jwt.strategy.ts
│ │ └── local.strategy.ts
│ ├── guards/
│ │ ├── jwt-auth.guard.ts
│ │ ├── local-auth.guard.ts
│ │ └── roles.guard.ts
│ └── decorators/
│ ├── current-user.decorator.ts
│ └── roles.decorator.ts
├── users/
│ ├── users.module.ts
│ ├── users.service.ts
│ ├── users.controller.ts
│ └── entities/
│ └── user.entity.ts
└── shared/
└── constants/
└── jwt.constants.ts
3. User Entity & Schema
User Entity (TypeORM):
typescript
// users/entities/user.entity.ts
import {
Entity,
Column,
PrimaryGeneratedColumn,
CreateDateColumn,
UpdateDateColumn,
BeforeInsert,
BeforeUpdate,
Index
} from 'typeorm';
import { Exclude } from 'class-transformer';
import { UserRole } from '../enums/user-role.enum';
@Entity('users')
export class User {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ unique: true })
@Index()
email: string;
@Column({ unique: true, nullable: true })
@Index()
username?: string;
@Column()
@Exclude() // Exclude from serialization
password: string;
@Column({ name: 'first_name' })
firstName: string;
@Column({ name: 'last_name' })
lastName: string;
@Column({
type: 'enum',
enum: UserRole,
default: UserRole.USER,
})
role: UserRole;
@Column({ nullable: true })
avatar?: string;
@Column({ default: false })
isEmailVerified: boolean;
@Column({ nullable: true })
emailVerificationToken?: string;
@Column({ nullable: true })
emailVerificationExpires?: Date;
@Column({ nullable: true })
passwordResetToken?: string;
@Column({ nullable: true })
passwordResetExpires?: Date;
@Column({ default: true })
isActive: boolean;
@Column({ nullable: true })
lastLoginAt?: Date;
@CreateDateColumn({ name: 'created_at' })
createdAt: Date;
@UpdateDateColumn({ name: 'updated_at' })
updatedAt: Date;
// Virtual property
get fullName(): string {
return `${this.firstName} ${this.lastName}`;
}
@BeforeInsert()
@BeforeUpdate()
async normalizeEmail() {
if (this.email) {
this.email = this.email.toLowerCase().trim();
}
}
}
User Role Enum:
typescript
// users/enums/user-role.enum.ts
export enum UserRole {
SUPER_ADMIN = 'super_admin',
ADMIN = 'admin',
MODERATOR = 'moderator',
USER = 'user',
GUEST = 'guest',
}
User DTOs:
typescript
// users/dto/create-user.dto.ts
import {
IsEmail,
IsString,
MinLength,
MaxLength,
Matches,
IsOptional,
IsEnum,
IsBoolean
} from 'class-validator';
import { UserRole } from '../enums/user-role.enum';
export class CreateUserDto {
@IsEmail()
email: string;
@IsString()
@MinLength(2)
@MaxLength(50)
firstName: string;
@IsString()
@MinLength(2)
@MaxLength(50)
lastName: string;
@IsString()
@MinLength(8)
@MaxLength(100)
@Matches(/((?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[\W]).{8,100})/, {
message: 'Password must contain at least one uppercase letter, one lowercase letter, one number and one special character'
})
password: string;
@IsString()
@MinLength(8)
@MaxLength(100)
passwordConfirm: string;
@IsOptional()
@IsString()
@MinLength(3)
@MaxLength(30)
username?: string;
@IsOptional()
@IsEnum(UserRole)
role?: UserRole = UserRole.USER;
@IsOptional()
@IsBoolean()
isActive?: boolean = true;
}
// users/dto/update-user.dto.ts
import { PartialType } from '@nestjs/mapped-types';
import { CreateUserDto } from './create-user.dto';
export class UpdateUserDto extends PartialType(CreateUserDto) {
// Remove password fields from update
password?: never;
passwordConfirm?: never;
}
// users/dto/login-user.dto.ts
import { IsEmail, IsString, MinLength } from 'class-validator';
export class LoginUserDto {
@IsEmail()
email: string;
@IsString()
@MinLength(8)
password: string;
}
// users/dto/change-password.dto.ts
export class ChangePasswordDto {
@IsString()
@MinLength(8)
currentPassword: string;
@IsString()
@MinLength(8)
newPassword: string;
@IsString()
@MinLength(8)
confirmNewPassword: string;
}
4. Password Hashing
Hash Service:
typescript
// shared/services/hash.service.ts
import { Injectable } from '@nestjs/common';
import * as bcrypt from 'bcryptjs';
@Injectable()
export class HashService {
private readonly saltRounds = 10;
/**
* Hash a plain text password
*/
async hashPassword(password: string): Promise<string> {
const salt = await bcrypt.genSalt(this.saltRounds);
return bcrypt.hash(password, salt);
}
/**
* Compare plain text password with hash
*/
async comparePassword(
plainText: string,
hash: string,
): Promise<boolean> {
return bcrypt.compare(plainText, hash);
}
/**
* Check if password needs rehash
*/
async needsRehash(hash: string): Promise<boolean> {
return bcrypt.getRounds(hash) !== this.saltRounds;
}
}
User Service with Password Hashing:
typescript
// users/users.service.ts
import {
Injectable,
ConflictException,
NotFoundException,
BadRequestException,
InternalServerErrorException
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './entities/user.entity';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { HashService } from '../shared/services/hash.service';
import { UserRole } from './enums/user-role.enum';
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private usersRepository: Repository<User>,
private hashService: HashService,
) {}
/**
* Create a new user with hashed password
*/
async create(createUserDto: CreateUserDto): Promise<User> {
// Check if email already exists
const existingUser = await this.findByEmail(createUserDto.email);
if (existingUser) {
throw new ConflictException('Email already registered');
}
// Check if username exists
if (createUserDto.username) {
const existingUsername = await this.findByUsername(createUserDto.username);
if (existingUsername) {
throw new ConflictException('Username already taken');
}
}
// Validate password match
if (createUserDto.password !== createUserDto.passwordConfirm) {
throw new BadRequestException('Passwords do not match');
}
// Hash password
const hashedPassword = await this.hashService.hashPassword(
createUserDto.password,
);
// Create user entity
const user = this.usersRepository.create({
...createUserDto,
password: hashedPassword,
// Remove passwordConfirm from entity
});
delete (user as any).passwordConfirm;
try {
return await this.usersRepository.save(user);
} catch (error) {
if (error.code === '23505') {
// PostgreSQL duplicate key error
throw new ConflictException('User already exists');
}
throw new InternalServerErrorException('Failed to create user');
}
}
/**
* Find user by email
*/
async findByEmail(email: string): Promise<User | null> {
return this.usersRepository.findOne({
where: { email: email.toLowerCase() },
});
}
/**
* Find user by username
*/
async findByUsername(username: string): Promise<User | null> {
return this.usersRepository.findOne({
where: { username },
});
}
/**
* Find user by ID
*/
async findById(id: string): Promise<User> {
const user = await this.usersRepository.findOne({
where: { id },
});
if (!user) {
throw new NotFoundException(`User with ID ${id} not found`);
}
return user;
}
/**
* Validate user credentials
*/
async validateUser(email: string, password: string): Promise<User | null> {
const user = await this.findByEmail(email);
if (!user) {
return null;
}
// Check if user is active
if (!user.isActive) {
throw new BadRequestException('Account is deactivated');
}
// Verify password
const isPasswordValid = await this.hashService.comparePassword(
password,
user.password,
);
if (!isPasswordValid) {
return null;
}
// Update last login
user.lastLoginAt = new Date();
await this.usersRepository.save(user);
return user;
}
/**
* Update user password
*/
async updatePassword(
userId: string,
currentPassword: string,
newPassword: string,
): Promise<void> {
const user = await this.findById(userId);
// Verify current password
const isCurrentPasswordValid = await this.hashService.comparePassword(
currentPassword,
user.password,
);
if (!isCurrentPasswordValid) {
throw new BadRequestException('Current password is incorrect');
}
// Hash new password
const hashedPassword = await this.hashService.hashPassword(newPassword);
user.password = hashedPassword;
await this.usersRepository.save(user);
}
/**
* Update user profile
*/
async update(userId: string, updateUserDto: UpdateUserDto): Promise<User> {
const user = await this.findById(userId);
// Check if email is being changed and if it's available
if (updateUserDto.email && updateUserDto.email !== user.email) {
const existingUser = await this.findByEmail(updateUserDto.email);
if (existingUser) {
throw new ConflictException('Email already registered');
}
}
// Check if username is being changed and if it's available
if (updateUserDto.username && updateUserDto.username !== user.username) {
const existingUsername = await this.findByUsername(updateUserDto.username);
if (existingUsername) {
throw new ConflictException('Username already taken');
}
}
// Update user
Object.assign(user, updateUserDto);
return this.usersRepository.save(user);
}
/**
* Delete user (soft delete)
*/
async delete(userId: string): Promise<void> {
const user = await this.findById(userId);
user.isActive = false;
await this.usersRepository.save(user);
}
/**
* Promote user to admin
*/
async promoteToAdmin(userId: string): Promise<User> {
const user = await this.findById(userId);
user.role = UserRole.ADMIN;
return this.usersRepository.save(user);
}
/**
* Demote admin to user
*/
async demoteToUser(userId: string): Promise<User> {
const user = await this.findById(userId);
user.role = UserRole.USER;
return this.usersRepository.save(user);
}
}
5. JWT Strategy Implementation
JWT Constants:
typescript
// shared/constants/jwt.constants.ts
export const jwtConstants = {
secret: process.env.JWT_SECRET || 'super-secret-key-change-in-production',
accessTokenExpiresIn: process.env.JWT_ACCESS_EXPIRES_IN || '15m',
refreshTokenExpiresIn: process.env.JWT_REFRESH_EXPIRES_IN || '7d',
issuer: process.env.JWT_ISSUER || 'your-app-name',
audience: process.env.JWT_AUDIENCE || 'your-app-audience',
};
JWT Payload Interface:
typescript
// auth/interfaces/jwt-payload.interface.ts
export interface JwtPayload {
sub: string; // User ID
email: string; // User email
role: string; // User role
iat?: number; // Issued at
exp?: number; // Expiration
}
export interface RefreshTokenPayload extends JwtPayload {
tokenId: string; // Refresh token ID
}
JWT Module Configuration:
typescript
// auth/jwt.module.ts
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { jwtConstants } from '../shared/constants/jwt.constants';
@Module({
imports: [
JwtModule.register({
secret: jwtConstants.secret,
signOptions: {
expiresIn: jwtConstants.accessTokenExpiresIn,
issuer: jwtConstants.issuer,
audience: jwtConstants.audience,
},
}),
],
exports: [JwtModule],
})
export class JwtAuthModule {}
JWT Strategy:
typescript
// 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';
import { jwtConstants } from '../../shared/constants/jwt.constants';
import { JwtPayload } from '../interfaces/jwt-payload.interface';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private usersService: UsersService) {
super({
jwtFromRequest: ExtractJwt.fromExtractors([
ExtractJwt.fromAuthHeaderAsBearerToken(),
ExtractJwt.fromUrlQueryParameter('token'), // Optional: from query param
ExtractJwt.fromHeader('x-access-token'), // Optional: custom header
]),
ignoreExpiration: false,
secretOrKey: jwtConstants.secret,
issuer: jwtConstants.issuer,
audience: jwtConstants.audience,
});
}
/**
* Validate JWT payload
*/
async validate(payload: JwtPayload) {
const user = await this.usersService.findById(payload.sub);
if (!user) {
throw new UnauthorizedException('User not found');
}
if (!user.isActive) {
throw new UnauthorizedException('Account is deactivated');
}
// Check if token was issued before password change
// You can implement token invalidation logic here
return {
id: user.id,
email: user.email,
role: user.role,
firstName: user.firstName,
lastName: user.lastName,
isEmailVerified: user.isEmailVerified,
};
}
}
Local Strategy (for username/password login):
typescript
// 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', // Use email instead of username
passwordField: 'password',
});
}
/**
* Validate user credentials
*/
async validate(email: string, password: string) {
const user = await this.authService.validateUser(email, password);
if (!user) {
throw new UnauthorizedException('Invalid credentials');
}
return user;
}
}
6. Authentication Service
typescript
// auth/auth.service.ts
import {
Injectable,
UnauthorizedException,
BadRequestException,
InternalServerErrorException
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { UsersService } from '../users/users.service';
import { HashService } from '../shared/services/hash.service';
import { JwtPayload, RefreshTokenPayload } from './interfaces/jwt-payload.interface';
import { LoginResponseDto } from './dto/login-response.dto';
import { RefreshTokenDto } from './dto/refresh-token.dto';
import { v4 as uuidv4 } from 'uuid';
@Injectable()
export class AuthService {
private refreshTokens = new Map<string, { userId: string; expiresAt: Date }>();
constructor(
private usersService: UsersService,
private jwtService: JwtService,
private hashService: HashService,
) {}
/**
* Validate user credentials
*/
async validateUser(email: string, password: string) {
return this.usersService.validateUser(email, password);
}
/**
* Login user
*/
async login(user: any): Promise<LoginResponseDto> {
const payload: JwtPayload = {
sub: user.id,
email: user.email,
role: user.role,
};
// Generate access token
const accessToken = this.jwtService.sign(payload);
// Generate refresh token
const refreshToken = await this.generateRefreshToken(user.id);
// Update last login
await this.usersService.updateLastLogin(user.id);
return {
accessToken,
refreshToken,
tokenType: 'Bearer',
expiresIn: 15 * 60, // 15 minutes in seconds
user: {
id: user.id,
email: user.email,
role: user.role,
firstName: user.firstName,
lastName: user.lastName,
fullName: user.fullName,
isEmailVerified: user.isEmailVerified,
},
};
}
/**
* Register new user
*/
async register(createUserDto: any) {
// Create user
const user = await this.usersService.create(createUserDto);
// Generate verification token (optional)
const verificationToken = await this.generateEmailVerificationToken(user.id);
// Login user automatically after registration
const loginResponse = await this.login(user);
return {
...loginResponse,
verificationToken, // Send in email, not in response
message: 'Registration successful. Please verify your email.',
};
}
/**
* Refresh access token
*/
async refreshToken(refreshTokenDto: RefreshTokenDto): Promise<LoginResponseDto> {
try {
// Verify refresh token
const payload = this.jwtService.verify(refreshTokenDto.refreshToken, {
secret: jwtConstants.secret,
}) as RefreshTokenPayload;
// Check if refresh token exists in storage
const storedToken = this.refreshTokens.get(payload.tokenId);
if (!storedToken) {
throw new UnauthorizedException('Invalid refresh token');
}
// Check if token has expired
if (storedToken.expiresAt < new Date()) {
this.refreshTokens.delete(payload.tokenId);
throw new UnauthorizedException('Refresh token expired');
}
// Get user
const user = await this.usersService.findById(payload.sub);
// Generate new tokens
const newPayload: JwtPayload = {
sub: user.id,
email: user.email,
role: user.role,
};
const accessToken = this.jwtService.sign(newPayload);
const newRefreshToken = await this.generateRefreshToken(user.id);
// Delete old refresh token
this.refreshTokens.delete(payload.tokenId);
return {
accessToken,
refreshToken: newRefreshToken,
tokenType: 'Bearer',
expiresIn: 15 * 60,
user: {
id: user.id,
email: user.email,
role: user.role,
firstName: user.firstName,
lastName: user.lastName,
fullName: user.fullName,
isEmailVerified: user.isEmailVerified,
},
};
} catch (error) {
throw new UnauthorizedException('Invalid refresh token');
}
}
/**
* Logout user (invalidate refresh token)
*/
async logout(refreshToken: string): Promise<void> {
try {
const payload = this.jwtService.verify(refreshToken, {
secret: jwtConstants.secret,
}) as RefreshTokenPayload;
this.refreshTokens.delete(payload.tokenId);
} catch (error) {
// Token might be expired or invalid, ignore
}
}
/**
* Change user password
*/
async changePassword(userId: string, changePasswordDto: any): Promise<void> {
await this.usersService.updatePassword(
userId,
changePasswordDto.currentPassword,
changePasswordDto.newPassword,
);
// Invalidate all refresh tokens for this user
this.invalidateUserRefreshTokens(userId);
}
/**
* Generate refresh token
*/
private async generateRefreshToken(userId: string): Promise<string> {
const tokenId = uuidv4();
const expiresAt = new Date();
expiresAt.setDate(expiresAt.getDate() + 7); // 7 days from now
const payload: RefreshTokenPayload = {
sub: userId,
email: '', // Will be populated
role: '', // Will be populated
tokenId,
};
// Get user for email and role
const user = await this.usersService.findById(userId);
payload.email = user.email;
payload.role = user.role;
// Store refresh token
this.refreshTokens.set(tokenId, { userId, expiresAt });
return this.jwtService.sign(payload, {
expiresIn: jwtConstants.refreshTokenExpiresIn,
});
}
/**
* Generate email verification token
*/
private async generateEmailVerificationToken(userId: string): Promise<string> {
const token = uuidv4();
const expiresAt = new Date();
expiresAt.setHours(expiresAt.getHours() + 24); // 24 hours
// Store token in database (implementation depends on your setup)
// await this.usersService.saveVerificationToken(userId, token, expiresAt);
return token;
}
/**
* Invalidate all refresh tokens for a user
*/
private invalidateUserRefreshTokens(userId: string): void {
for (const [tokenId, tokenData] of this.refreshTokens.entries()) {
if (tokenData.userId === userId) {
this.refreshTokens.delete(tokenId);
}
}
}
/**
* Verify JWT token
*/
async verifyToken(token: string): Promise<JwtPayload> {
try {
return this.jwtService.verify(token);
} catch (error) {
throw new UnauthorizedException('Invalid token');
}
}
/**
* Get user from JWT token
*/
async getUserFromToken(token: string): Promise<any> {
const payload = await this.verifyToken(token);
return this.usersService.findById(payload.sub);
}
}
7. Auth Controller
typescript
// auth/auth.controller.ts
import {
Controller,
Post,
Body,
UseGuards,
Request,
Get,
HttpCode,
HttpStatus,
UnauthorizedException,
} from '@nestjs/common';
import { AuthService } from './auth.service';
import { UsersService } from '../users/users.service';
import { LocalAuthGuard } from './guards/local-auth.guard';
import { JwtAuthGuard } from './guards/jwt-auth.guard';
import { CreateUserDto } from '../users/dto/create-user.dto';
import { LoginUserDto } from '../users/dto/login-user.dto';
import { RefreshTokenDto } from './dto/refresh-token.dto';
import { ChangePasswordDto } from '../users/dto/change-password.dto';
import { Public } from './decorators/public.decorator';
@Controller('auth')
export class AuthController {
constructor(
private authService: AuthService,
private usersService: UsersService,
) {}
/**
* User registration
*/
@Public()
@Post('register')
@HttpCode(HttpStatus.CREATED)
async register(@Body() createUserDto: CreateUserDto) {
return this.authService.register(createUserDto);
}
/**
* User login
*/
@Public()
@UseGuards(LocalAuthGuard)
@Post('login')
@HttpCode(HttpStatus.OK)
async login(@Request() req, @Body() loginUserDto: LoginUserDto) {
return this.authService.login(req.user);
}
/**
* Refresh access token
*/
@Public()
@Post('refresh')
@HttpCode(HttpStatus.OK)
async refresh(@Body() refreshTokenDto: RefreshTokenDto) {
return this.authService.refreshToken(refreshTokenDto);
}
/**
* Get current user profile
*/
@UseGuards(JwtAuthGuard)
@Get('profile')
@HttpCode(HttpStatus.OK)
async getProfile(@Request() req) {
return req.user;
}
/**
* Change password
*/
@UseGuards(JwtAuthGuard)
@Post('change-password')
@HttpCode(HttpStatus.OK)
async changePassword(
@Request() req,
@Body() changePasswordDto: ChangePasswordDto,
) {
await this.authService.changePassword(req.user.id, changePasswordDto);
return { message: 'Password changed successfully' };
}
/**
* Logout
*/
@UseGuards(JwtAuthGuard)
@Post('logout')
@HttpCode(HttpStatus.OK)
async logout(@Body() refreshTokenDto: RefreshTokenDto) {
await this.authService.logout(refreshTokenDto.refreshToken);
return { message: 'Logged out successfully' };
}
/**
* Validate token
*/
@Public()
@Post('validate')
@HttpCode(HttpStatus.OK)
async validateToken(@Body('token') token: string) {
try {
const payload = await this.authService.verifyToken(token);
return { valid: true, payload };
} catch (error) {
throw new UnauthorizedException('Invalid token');
}
}
}
Auth DTOs:
typescript
// auth/dto/login-response.dto.ts
export class LoginResponseDto {
accessToken: string;
refreshToken: string;
tokenType: string;
expiresIn: number;
user: {
id: string;
email: string;
role: string;
firstName: string;
lastName: string;
fullName: string;
isEmailVerified: boolean;
};
}
// auth/dto/refresh-token.dto.ts
import { IsJWT, IsNotEmpty } from 'class-validator';
export class RefreshTokenDto {
@IsJWT()
@IsNotEmpty()
refreshToken: string;
}
8. Authentication Guards
JWT Auth Guard:
typescript
// auth/guards/jwt-auth.guard.ts
import { Injectable, ExecutionContext } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { Reflector } from '@nestjs/core';
import { Observable } from 'rxjs';
import { IS_PUBLIC_KEY } from '../decorators/public.decorator';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
constructor(private reflector: Reflector) {
super();
}
canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
// Check if route is public
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);
if (isPublic) {
return true;
}
return super.canActivate(context);
}
handleRequest(err, user, info) {
// You can throw an exception based on either "info" or "err" arguments
if (err || !user) {
throw err || new UnauthorizedException('Authentication required');
}
return user;
}
}
Local Auth Guard:
typescript
// auth/guards/local-auth.guard.ts
import { Injectable, ExecutionContext } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { Observable } from 'rxjs';
@Injectable()
export class LocalAuthGuard extends AuthGuard('local') {
canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
return super.canActivate(context);
}
handleRequest(err, user, info) {
if (err || !user) {
throw err || new UnauthorizedException('Invalid credentials');
}
return user;
}
}
Public Decorator:
typescript
// auth/decorators/public.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
Current User Decorator:
typescript
// auth/decorators/current-user.decorator.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const CurrentUser = createParamDecorator(
(data: string | undefined, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
const user = request.user;
return data ? user?.[data] : user;
},
);
9. Authorization & Role-Based Access Control
Roles Decorator:
typescript
// auth/decorators/roles.decorator.ts
import { SetMetadata } from '@nestjs/common';
import { UserRole } from '../../users/enums/user-role.enum';
export const ROLES_KEY = 'roles';
export const Roles = (...roles: UserRole[]) => SetMetadata(ROLES_KEY, roles);
Roles Guard:
typescript
// auth/guards/roles.guard.ts
import {
Injectable,
CanActivate,
ExecutionContext,
ForbiddenException
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { UserRole } from '../../users/enums/user-role.enum';
import { ROLES_KEY } from '../decorators/roles.decorator';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride<UserRole[]>(ROLES_KEY, [
context.getHandler(),
context.getClass(),
]);
// If no roles are required, allow access
if (!requiredRoles || requiredRoles.length === 0) {
return true;
}
const { user } = context.switchToHttp().getRequest();
if (!user) {
throw new ForbiddenException('Authentication required');
}
// Check if user has required role
const hasRole = requiredRoles.some((role) => user.role === role);
if (!hasRole) {
throw new ForbiddenException(
`Insufficient permissions. Required roles: ${requiredRoles.join(', ')}`,
);
}
return true;
}
}
Permission-Based Guard:
typescript
// auth/guards/permissions.guard.ts
import {
Injectable,
CanActivate,
ExecutionContext,
ForbiddenException
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
export interface Permission {
resource: string;
action: string;
}
export const PERMISSIONS_KEY = 'permissions';
@Injectable()
export class PermissionsGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredPermissions = this.reflector.getAllAndOverride<Permission[]>(
PERMISSIONS_KEY,
[
context.getHandler(),
context.getClass(),
],
);
if (!requiredPermissions) {
return true;
}
const { user } = context.switchToHttp().getRequest();
if (!user) {
throw new ForbiddenException('Authentication required');
}
// Check permissions based on user role
// You can implement a more sophisticated permission system
const userPermissions = this.getUserPermissions(user.role);
const hasPermission = requiredPermissions.every((required) =>
userPermissions.some(
(userPerm) =>
userPerm.resource === required.resource &&
userPerm.action === required.action,
),
);
if (!hasPermission) {
throw new ForbiddenException('Insufficient permissions');
}
return true;
}
private getUserPermissions(role: string): Permission[] {
// Define permissions for each role
const permissions: Record<string, Permission[]> = {
super_admin: [
{ resource: 'users', action: 'create' },
{ resource: 'users', action: 'read' },
{ resource: 'users', action: 'update' },
{ resource: 'users', action: 'delete' },
{ resource: 'products', action: 'create' },
{ resource: 'products', action: 'read' },
{ resource: 'products', action: 'update' },
{ resource: 'products', action: 'delete' },
],
admin: [
{ resource: 'users', action: 'read' },
{ resource: 'users', action: 'update' },
{ resource: 'products', action: 'create' },
{ resource: 'products', action: 'read' },
{ resource: 'products', action: 'update' },
],
user: [
{ resource: 'users', action: 'read' },
{ resource: 'users', action: 'update' },
{ resource: 'products', action: 'read' },
],
};
return permissions[role] || [];
}
}
Permission Decorator:
typescript
// auth/decorators/permissions.decorator.ts
import { SetMetadata } from '@nestjs/common';
import { Permission } from '../guards/permissions.guard';
export const Permissions = (...permissions: Permission[]) =>
SetMetadata('permissions', permissions);
Using Guards in Controllers:
typescript
// users/users.controller.ts
import {
Controller,
Get,
Post,
Put,
Delete,
Body,
Param,
UseGuards,
Query,
ParseUUIDPipe,
} from '@nestjs/common';
import { UsersService } from './users.service';
import { UpdateUserDto } from './dto/update-user.dto';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { RolesGuard } from '../auth/guards/roles.guard';
import { Roles } from '../auth/decorators/roles.decorator';
import { CurrentUser } from '../auth/decorators/current-user.decorator';
import { UserRole } from './enums/user-role.enum';
@Controller('users')
@UseGuards(JwtAuthGuard, RolesGuard)
export class UsersController {
constructor(private readonly usersService: UsersService) {}
// Public route (no authentication required for registration)
@Post('register')
async register(@Body() createUserDto: any) {
return this.usersService.create(createUserDto);
}
// Get current user profile (authenticated users only)
@Get('profile')
async getProfile(@CurrentUser('id') userId: string) {
return this.usersService.findById(userId);
}
// Get all users (admin only)
@Get()
@Roles(UserRole.ADMIN, UserRole.SUPER_ADMIN)
async findAll(@Query() query: any) {
return this.usersService.findAll(query);
}
// Get user by ID (admin or own profile)
@Get(':id')
async findOne(
@Param('id', ParseUUIDPipe) id: string,
@CurrentUser() currentUser: any,
) {
// Users can view their own profile, admins can view any
if (currentUser.id !== id && currentUser.role === UserRole.USER) {
throw new ForbiddenException('Cannot view other user profiles');
}
return this.usersService.findById(id);
}
// Update user (admin or own profile)
@Put(':id')
async update(
@Param('id', ParseUUIDPipe) id: string,
@Body() updateUserDto: UpdateUserDto,
@CurrentUser() currentUser: any,
) {
// Users can update their own profile, admins can update any
if (currentUser.id !== id && currentUser.role === UserRole.USER) {
throw new ForbiddenException('Cannot update other users');
}
// Prevent users from changing their own role
if (currentUser.id === id && updateUserDto.role) {
delete updateUserDto.role;
}
return this.usersService.update(id, updateUserDto);
}
// Delete user (admin only)
@Delete(':id')
@Roles(UserRole.ADMIN, UserRole.SUPER_ADMIN)
async remove(@Param('id', ParseUUIDPipe) id: string) {
return this.usersService.delete(id);
}
// Promote user to admin (super admin only)
@Post(':id/promote')
@Roles(UserRole.SUPER_ADMIN)
async promote(@Param('id', ParseUUIDPipe) id: string) {
return this.usersService.promoteToAdmin(id);
}
}
10. Passport Strategies (Optional Extensions)
Google OAuth Strategy:
typescript
// auth/strategies/google.strategy.ts
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy, VerifyCallback } from 'passport-google-oauth20';
import { AuthService } from '../auth.service';
@Injectable()
export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
constructor(private authService: AuthService) {
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,
};
// Find or create user in your database
const existingUser = await this.authService.findOrCreateUser(user);
done(null, existingUser);
}
}
Facebook Strategy:
typescript
// auth/strategies/facebook.strategy.ts
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-facebook';
import { AuthService } from '../auth.service';
@Injectable()
export class FacebookStrategy extends PassportStrategy(Strategy, 'facebook') {
constructor(private authService: AuthService) {
super({
clientID: process.env.FACEBOOK_APP_ID,
clientSecret: process.env.FACEBOOK_APP_SECRET,
callbackURL: process.env.FACEBOOK_CALLBACK_URL,
profileFields: ['id', 'emails', 'name'],
});
}
async validate(
accessToken: string,
refreshToken: string,
profile: any,
done: (error: any, user?: any) => void,
): Promise<any> {
const { name, emails } = profile;
const user = {
email: emails[0].value,
firstName: name.givenName,
lastName: name.familyName,
facebookId: profile.id,
accessToken,
};
const existingUser = await this.authService.findOrCreateUser(user);
done(null, existingUser);
}
}
11. Refresh Tokens Implementation
Refresh Token Entity:
typescript
// auth/entities/refresh-token.entity.ts
import {
Entity,
Column,
PrimaryGeneratedColumn,
CreateDateColumn,
ManyToOne,
JoinColumn
} from 'typeorm';
import { User } from '../../users/entities/user.entity';
@Entity('refresh_tokens')
export class RefreshToken {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
token: string;
@Column({ name: 'user_id' })
userId: string;
@ManyToOne(() => User, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'user_id' })
user: User;
@Column({ name: 'expires_at' })
expiresAt: Date;
@Column({ name: 'is_revoked', default: false })
isRevoked: boolean;
@CreateDateColumn({ name: 'created_at' })
createdAt: Date;
@Column({ name: 'revoked_at', nullable: true })
revokedAt: Date;
@Column({ name: 'ip_address', nullable: true })
ipAddress: string;
@Column({ name: 'user_agent', nullable: true })
userAgent: string;
isExpired(): boolean {
return this.expiresAt < new Date();
}
isValid(): boolean {
return !this.isRevoked && !this.isExpired();
}
}
Refresh Token Service:
typescript
// auth/services/refresh-token.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { RefreshToken } from '../entities/refresh-token.entity';
import { v4 as uuidv4 } from 'uuid';
@Injectable()
export class RefreshTokenService {
constructor(
@InjectRepository(RefreshToken)
private refreshTokenRepository: Repository<RefreshToken>,
) {}
async create(userId: string, ttl: number, ipAddress?: string, userAgent?: string): Promise<RefreshToken> {
const token = uuidv4();
const expiresAt = new Date();
expiresAt.setTime(expiresAt.getTime() + ttl);
const refreshToken = this.refreshTokenRepository.create({
token,
userId,
expiresAt,
ipAddress,
userAgent,
});
return this.refreshTokenRepository.save(refreshToken);
}
async findById(id: string): Promise<RefreshToken | null> {
return this.refreshTokenRepository.findOne({
where: { id },
relations: ['user'],
});
}
async findByToken(token: string): Promise<RefreshToken | null> {
return this.refreshTokenRepository.findOne({
where: { token },
relations: ['user'],
});
}
async revoke(token: string): Promise<void> {
const refreshToken = await this.findByToken(token);
if (refreshToken) {
refreshToken.isRevoked = true;
refreshToken.revokedAt = new Date();
await this.refreshTokenRepository.save(refreshToken);
}
}
async revokeAllForUser(userId: string): Promise<void> {
await this.refreshTokenRepository.update(
{ userId, isRevoked: false },
{ isRevoked: true, revokedAt: new Date() },
);
}
async cleanupExpiredTokens(): Promise<void> {
await this.refreshTokenRepository
.createQueryBuilder()
.delete()
.where('expires_at < :now', { now: new Date() })
.execute();
}
async isValid(token: string): Promise<boolean> {
const refreshToken = await this.findByToken(token);
return !!refreshToken && refreshToken.isValid();
}
}
12. Complete Implementation
Auth Module:
typescript
// auth/auth.module.ts
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { UsersModule } from '../users/users.module';
import { JwtStrategy } from './strategies/jwt.strategy';
import { LocalStrategy } from './strategies/local.strategy';
import { HashService } from '../shared/services/hash.service';
import { RefreshToken } from './entities/refresh-token.entity';
import { RefreshTokenService } from './services/refresh-token.service';
import { jwtConstants } from '../shared/constants/jwt.constants';
@Module({
imports: [
UsersModule,
PassportModule.register({ defaultStrategy: 'jwt' }),
JwtModule.register({
secret: jwtConstants.secret,
signOptions: {
expiresIn: jwtConstants.accessTokenExpiresIn,
issuer: jwtConstants.issuer,
audience: jwtConstants.audience,
},
}),
TypeOrmModule.forFeature([RefreshToken]),
],
controllers: [AuthController],
providers: [
AuthService,
JwtStrategy,
LocalStrategy,
HashService,
RefreshTokenService,
],
exports: [AuthService, JwtModule, PassportModule],
})
export class AuthModule {}
Global Guards Setup:
typescript
// main.ts
import { NestFactory, Reflector } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module';
import { JwtAuthGuard } from './auth/guards/jwt-auth.guard';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Global validation pipe
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
}),
);
// Global guards
const reflector = app.get(Reflector);
app.useGlobalGuards(new JwtAuthGuard(reflector));
await app.listen(3000);
}
bootstrap();
Environment Variables (.env):
env
# JWT Configuration JWT_SECRET=your-super-secret-jwt-key-change-in-production JWT_ACCESS_EXPIRES_IN=15m JWT_REFRESH_EXPIRES_IN=7d JWT_ISSUER=your-app-name JWT_AUDIENCE=your-app-client # Database DATABASE_URL=postgresql://user:password@localhost:5432/dbname # OAuth (Optional) GOOGLE_CLIENT_ID=your-google-client-id GOOGLE_CLIENT_SECRET=your-google-client-secret GOOGLE_CALLBACK_URL=http://localhost:3000/auth/google/callback FACEBOOK_APP_ID=your-facebook-app-id FACEBOOK_APP_SECRET=your-facebook-app-secret FACEBOOK_CALLBACK_URL=http://localhost:3000/auth/facebook/callback
13. Testing & Security
Security Best Practices:
-
Password Security:
-
Use bcrypt with appropriate salt rounds (10-12)
-
Enforce strong password policies
-
Implement rate limiting for login attempts
-
-
JWT Security:
-
Use strong secret keys (32+ characters)
-
Set appropriate expiration times
-
Implement token refresh mechanism
-
Store refresh tokens securely (HTTP-only cookies or database)
-
-
Session Security:
-
Use HTTPS in production
-
Implement CSRF protection
-
Set secure cookie flags (Secure, HttpOnly, SameSite)
-
-
Rate Limiting:
typescript
// rate-limiting setup
import rateLimit from 'express-rate-limit';
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // Limit each IP to 5 requests per windowMs
message: 'Too many login attempts, please try again later',
skipSuccessfulRequests: true,
});
app.use('/auth/login', authLimiter);
app.use('/auth/register', authLimiter);
Testing Authentication:
typescript
// auth/auth.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { JwtService } from '@nestjs/jwt';
import { AuthService } from './auth.service';
import { UsersService } from '../users/users.service';
import { HashService } from '../shared/services/hash.service';
import { UnauthorizedException } from '@nestjs/common';
describe('AuthService', () => {
let service: AuthService;
let usersService: UsersService;
let jwtService: JwtService;
let hashService: HashService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
AuthService,
{
provide: UsersService,
useValue: {
validateUser: jest.fn(),
findById: jest.fn(),
updateLastLogin: jest.fn(),
},
},
{
provide: JwtService,
useValue: {
sign: jest.fn(),
verify: jest.fn(),
},
},
{
provide: HashService,
useValue: {
comparePassword: jest.fn(),
},
},
],
}).compile();
service = module.get<AuthService>(AuthService);
usersService = module.get<UsersService>(UsersService);
jwtService = module.get<JwtService>(JwtService);
hashService = module.get<HashService>(HashService);
});
describe('validateUser', () => {
it('should return user if credentials are valid', async () => {
const mockUser = { id: '1', email: 'test@example.com' };
jest.spyOn(usersService, 'validateUser').mockResolvedValue(mockUser);
const result = await service.validateUser('test@example.com', 'password');
expect(result).toEqual(mockUser);
});
it('should return null if credentials are invalid', async () => {
jest.spyOn(usersService, 'validateUser').mockResolvedValue(null);
const result = await service.validateUser('test@example.com', 'wrong');
expect(result).toBeNull();
});
});
describe('login', () => {
it('should return access and refresh tokens', async () => {
const mockUser = {
id: '1',
email: 'test@example.com',
role: 'user',
firstName: 'John',
lastName: 'Doe',
fullName: 'John Doe',
isEmailVerified: true,
};
jest.spyOn(jwtService, 'sign').mockReturnValue('access-token');
jest.spyOn(usersService, 'updateLastLogin').mockResolvedValue(undefined);
const result = await service.login(mockUser);
expect(result.accessToken).toBe('access-token');
expect(result.refreshToken).toBeDefined();
expect(result.user).toEqual(mockUser);
});
});
});
Integration Tests:
typescript
// auth/auth.controller.e2e-spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from '../../app.module';
describe('AuthController (e2e)', () => {
let app: INestApplication;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
afterAll(async () => {
await app.close();
});
describe('POST /auth/register', () => {
it('should register a new user', () => {
return request(app.getHttpServer())
.post('/auth/register')
.send({
email: 'test@example.com',
password: 'Password123!',
passwordConfirm: 'Password123!',
firstName: 'John',
lastName: 'Doe',
})
.expect(201)
.expect((res) => {
expect(res.body.accessToken).toBeDefined();
expect(res.body.user.email).toBe('test@example.com');
});
});
it('should fail with invalid data', () => {
return request(app.getHttpServer())
.post('/auth/register')
.send({
email: 'invalid-email',
password: '123',
})
.expect(400);
});
});
describe('POST /auth/login', () => {
it('should login with valid credentials', async () => {
// First register a user
await request(app.getHttpServer())
.post('/auth/register')
.send({
email: 'login@example.com',
password: 'Password123!',
passwordConfirm: 'Password123!',
firstName: 'Jane',
lastName: 'Doe',
});
// Then login
return request(app.getHttpServer())
.post('/auth/login')
.send({
email: 'login@example.com',
password: 'Password123!',
})
.expect(200)
.expect((res) => {
expect(res.body.accessToken).toBeDefined();
});
});
it('should fail with invalid credentials', () => {
return request(app.getHttpServer())
.post('/auth/login')
.send({
email: 'login@example.com',
password: 'WrongPassword!',
})
.expect(401);
});
});
});
This extensive authentication and authorization implementation provides a secure foundation for your NestJS application with JWT, password hashing, role-based access control, and proper error handling.
Comments (0)
No comments yet. Be the first to comment!