Skip to content

Instantly share code, notes, and snippets.

@oNddleo
Last active March 4, 2025 01:50
Show Gist options
  • Save oNddleo/05209269ce68611f629bbd4c4dc44b05 to your computer and use it in GitHub Desktop.
Save oNddleo/05209269ce68611f629bbd4c4dc44b05 to your computer and use it in GitHub Desktop.
Prevent spam registration
-- IP-based rate limiting table
CREATE TABLE ip_rate_limits (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
ip_address VARCHAR(45) NOT NULL, -- IPv4 or IPv6 address
endpoint VARCHAR(100) NOT NULL, -- Which endpoint was accessed
request_count INTEGER NOT NULL DEFAULT 1,
first_request_at TIMESTAMP NOT NULL,
last_request_at TIMESTAMP NOT NULL,
blocked_until TIMESTAMP,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT uq_ip_endpoint UNIQUE(ip_address, endpoint)
);
-- Index for quick lookups by IP
CREATE INDEX idx_ip_rate_limits_ip ON ip_rate_limits(ip_address);
-- Table to track OTP request patterns for additional security
CREATE TABLE otp_request_tracking (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
phone_number VARCHAR(15) NOT NULL,
ip_address VARCHAR(45) NOT NULL,
request_success BOOLEAN NOT NULL DEFAULT TRUE,
otp_verified BOOLEAN NOT NULL DEFAULT FALSE,
user_agent TEXT, -- Browser/device info
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- Indexes for analysis
CREATE INDEX idx_otp_tracking_phone ON otp_request_tracking(phone_number);
CREATE INDEX idx_otp_tracking_ip ON otp_request_tracking(ip_address);
CREATE INDEX idx_otp_tracking_created_at ON otp_request_tracking(created_at);
// src/common/middleware/ip-rate-limit.middleware.ts
import { Injectable, NestMiddleware, HttpException, HttpStatus } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { IpRateLimit } from '../entities/ip-rate-limit.entity';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class IpRateLimitMiddleware implements NestMiddleware {
constructor(
@InjectRepository(IpRateLimit)
private ipRateLimitRepository: Repository<IpRateLimit>,
private configService: ConfigService,
) {}
async use(req: Request, res: Response, next: NextFunction) {
const ip = this.getClientIp(req);
const endpoint = req.path;
// Get rate limit settings from config
const maxRequests = this.configService.get<number>('rateLimit.maxRequests', 5);
const windowMs = this.configService.get<number>('rateLimit.windowMs', 60 * 60 * 1000); // 1 hour
const blockDuration = this.configService.get<number>('rateLimit.blockDuration', 24 * 60 * 60 * 1000); // 24 hours
// Find or create rate limit record for this IP and endpoint
let rateLimit = await this.ipRateLimitRepository.findOne({
where: { ipAddress: ip, endpoint },
});
const now = new Date();
if (!rateLimit) {
// First request from this IP to this endpoint
rateLimit = this.ipRateLimitRepository.create({
ipAddress: ip,
endpoint,
requestCount: 1,
firstRequestAt: now,
lastRequestAt: now,
});
await this.ipRateLimitRepository.save(rateLimit);
return next();
}
// Check if IP is currently blocked
if (rateLimit.blockedUntil && rateLimit.blockedUntil > now) {
throw new HttpException(
{
statusCode: HttpStatus.TOO_MANY_REQUESTS,
message: 'Too many requests from this IP, please try again later',
blockedUntil: rateLimit.blockedUntil,
},
HttpStatus.TOO_MANY_REQUESTS,
);
}
// Check if the time window has expired and reset if needed
const windowExpiry = new Date(rateLimit.firstRequestAt.getTime() + windowMs);
if (now > windowExpiry) {
// Reset counter for new time window
rateLimit.requestCount = 1;
rateLimit.firstRequestAt = now;
} else {
// Increment counter for existing time window
rateLimit.requestCount += 1;
}
rateLimit.lastRequestAt = now;
// Check if rate limit exceeded
if (rateLimit.requestCount > maxRequests) {
// Block this IP for the specified duration
rateLimit.blockedUntil = new Date(now.getTime() + blockDuration);
await this.ipRateLimitRepository.save(rateLimit);
throw new HttpException(
{
statusCode: HttpStatus.TOO_MANY_REQUESTS,
message: 'Too many requests from this IP, please try again later',
blockedUntil: rateLimit.blockedUntil,
},
HttpStatus.TOO_MANY_REQUESTS,
);
}
// Save updated rate limit and continue
await this.ipRateLimitRepository.save(rateLimit);
next();
}
private getClientIp(request: Request): string {
// Get IP from various headers that might be set by proxies
const forwarded = request.headers['x-forwarded-for'];
if (forwarded) {
const ips = Array.isArray(forwarded) ? forwarded[0] : forwarded.split(/, /)[0];
return ips;
}
const realIp = request.headers['x-real-ip'];
if (realIp) {
return Array.isArray(realIp) ? realIp[0] : realIp;
}
return request.ip || '0.0.0.0';
}
}
// src/modules/security/infrastructure/persistence/entities/ip-rate-limit.entity.ts
import { EntityHelper } from 'src/utils/entity-helper';
import { Column, Entity, PrimaryGeneratedColumn, Index, Unique } from 'typeorm';
@Entity('ip_rate_limits')
@Unique(['ipAddress', 'endpoint'])
export class IpRateLimit extends EntityHelper {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ name: 'ip_address', length: 45 })
@Index()
ipAddress: string;
@Column({ length: 100 })
endpoint: string;
@Column({ name: 'request_count', default: 1 })
requestCount: number;
@Column({ name: 'first_request_at' })
firstRequestAt: Date;
@Column({ name: 'last_request_at' })
lastRequestAt: Date;
@Column({ name: 'blocked_until', nullable: true })
blockedUntil: Date | null;
@Column({ name: 'created_at', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
createdAt: Date;
@Column({ name: 'updated_at', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP', onUpdate: 'CURRENT_TIMESTAMP' })
updatedAt: Date;
}
// src/modules/security/infrastructure/persistence/entities/otp-request-tracking.entity.ts
import { EntityHelper } from 'src/utils/entity-helper';
import { Column, Entity, PrimaryGeneratedColumn, Index } from 'typeorm';
@Entity('otp_request_tracking')
export class OtpRequestTracking extends EntityHelper {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ name: 'phone_number', length: 15 })
@Index()
phoneNumber: string;
@Column({ name: 'ip_address', length: 45 })
@Index()
ipAddress: string;
@Column({ name: 'request_success', default: true })
requestSuccess: boolean;
@Column({ name: 'otp_verified', default: false })
otpVerified: boolean;
@Column({ name: 'user_agent', type: 'text', nullable: true })
userAgent: string | null;
@Column({ name: 'created_at', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
@Index()
createdAt: Date;
}
// src/modules/security/application/services/security.service.ts
import { Injectable, HttpException, HttpStatus } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { IpRateLimit } from '../../infrastructure/persistence/entities/ip-rate-limit.entity';
import { OtpRequestTracking } from '../../infrastructure/persistence/entities/otp-request-tracking.entity';
import { ConfigService } from '@nestjs/config';
import { Request } from 'express';
interface TrackOtpRequestParams {
phoneNumber: string;
ipAddress: string;
userAgent?: string;
success: boolean;
}
interface ValidateOtpRequestParams {
phoneNumber: string;
ipAddress: string;
endpoint: string;
}
@Injectable()
export class SecurityService {
constructor(
@InjectRepository(IpRateLimit)
private ipRateLimitRepository: Repository<IpRateLimit>,
@InjectRepository(OtpRequestTracking)
private otpRequestTrackingRepository: Repository<OtpRequestTracking>,
private configService: ConfigService,
) {}
/**
* Advanced check for suspicious OTP request patterns
*/
async validateOtpRequest(params: ValidateOtpRequestParams): Promise<boolean> {
const { phoneNumber, ipAddress, endpoint } = params;
// 1. Check suspicious patterns - multiple phone numbers from same IP
const phoneNumbersFromIp = await this.otpRequestTrackingRepository
.createQueryBuilder('tracking')
.select('DISTINCT tracking.phoneNumber')
.where('tracking.ipAddress = :ipAddress', { ipAddress })
.andWhere('tracking.createdAt > :date', {
date: new Date(Date.now() - 24 * 60 * 60 * 1000) // last 24 hours
})
.getCount();
const maxPhonesPerIp = this.configService.get<number>('security.maxPhonesPerIp', 3);
if (phoneNumbersFromIp >= maxPhonesPerIp) {
// Block this IP for suspicious activity
await this.blockIpAddress(ipAddress, endpoint);
throw new HttpException(
'Security check failed: Too many different phone numbers from this IP',
HttpStatus.TOO_MANY_REQUESTS,
);
}
// 2. Check for verification success rate (fraud prevention)
const totalRequests = await this.otpRequestTrackingRepository.count({
where: {
ipAddress,
createdAt: new Date(Date.now() - 24 * 60 * 60 * 1000), // last 24 hours
},
});
const verifiedRequests = await this.otpRequestTrackingRepository.count({
where: {
ipAddress,
otpVerified: true,
createdAt: new Date(Date.now() - 24 * 60 * 60 * 1000), // last 24 hours
},
});
// If more than 10 requests and less than 10% verification rate, it's likely spam
if (totalRequests > 10 && verifiedRequests / totalRequests < 0.1) {
await this.blockIpAddress(ipAddress, endpoint);
throw new HttpException(
'Security check failed: Unusual activity detected',
HttpStatus.TOO_MANY_REQUESTS,
);
}
return true;
}
/**
* Track OTP request for analysis and fraud prevention
*/
async trackOtpRequest(params: TrackOtpRequestParams): Promise<void> {
const { phoneNumber, ipAddress, userAgent, success } = params;
await this.otpRequestTrackingRepository.save({
phoneNumber,
ipAddress,
userAgent,
requestSuccess: success,
otpVerified: false, // will be updated when/if OTP is verified
});
}
/**
* Mark an OTP as verified for a specific phone number
*/
async markOtpVerified(phoneNumber: string): Promise<void> {
// Find the most recent OTP request for this phone number
const latestRequest = await this.otpRequestTrackingRepository.findOne({
where: { phoneNumber },
order: { createdAt: 'DESC' },
});
if (latestRequest) {
latestRequest.otpVerified = true;
await this.otpRequestTrackingRepository.save(latestRequest);
}
}
/**
* Extract IP and client info from request
*/
getClientInfo(req: Request): { ipAddress: string; userAgent: string } {
// Get IP from various headers that might be set by proxies
const forwarded = req.headers['x-forwarded-for'];
let ipAddress: string;
if (forwarded) {
ipAddress = Array.isArray(forwarded) ? forwarded[0] : forwarded.split(/, /)[0];
} else {
const realIp = req.headers['x-real-ip'];
if (realIp) {
ipAddress = Array.isArray(realIp) ? realIp[0] : realIp;
} else {
ipAddress = req.ip || '0.0.0.0';
}
}
const userAgent = req.headers['user-agent'] || '';
return { ipAddress, userAgent: userAgent.toString() };
}
/**
* Block an IP address for a specified endpoint
*/
private async blockIpAddress(ipAddress: string, endpoint: string, durationHours = 24): Promise<void> {
let rateLimit = await this.ipRateLimitRepository.findOne({
where: { ipAddress, endpoint },
});
const now = new Date();
if (!rateLimit) {
rateLimit = this.ipRateLimitRepository.create({
ipAddress,
endpoint,
requestCount: 999, // Arbitrary high number
firstRequestAt: now,
lastRequestAt: now,
});
}
rateLimit.blockedUntil = new Date(now.getTime() + durationHours * 60 * 60 * 1000);
await this.ipRateLimitRepository.save(rateLimit);
}
/**
* Get detailed rate limiting stats for an IP
*/
async getIpStats(ipAddress: string): Promise<any> {
const rateLimits = await this.ipRateLimitRepository.find({
where: { ipAddress },
});
const otpStats = await this.otpRequestTrackingRepository
.createQueryBuilder('tracking')
.select('COUNT(*)', 'totalRequests')
.addSelect('SUM(CASE WHEN tracking.otpVerified = true THEN 1 ELSE 0 END)', 'verifiedRequests')
.where('tracking.ipAddress = :ipAddress', { ipAddress })
.andWhere('tracking.createdAt > :date', {
date: new Date(Date.now() - 24 * 60 * 60 * 1000), // last 24 hours
})
.getRawOne();
return {
rateLimits,
otpStats,
};
}
}
// src/modules/auth/application/services/auth.service.ts
import { HttpException, HttpStatus, Injectable, Req } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
import { UserRepository } from '../../../user/infrastructure/persistence/repositories/user.repository';
import { OtpService } from './otp.service';
import { User } from '../../../user/domain/user.model';
import { UserDto } from '../dto/user.dto';
import { plainToClass } from 'class-transformer';
import { SecurityService } from '../../../security/application/services/security.service';
import { Request } from 'express';
@Injectable()
export class AuthService {
constructor(
private userRepository: UserRepository,
private otpService: OtpService,
private jwtService: JwtService,
private configService: ConfigService,
private securityService: SecurityService,
) {}
async requestOtp(phoneNumber: string, @Req() request: Request): Promise<{ message: string; resendCount?: number; maxResendAttempts?: number }> {
const { ipAddress, userAgent } = this.securityService.getClientInfo(request);
// Enhanced security check
await this.securityService.validateOtpRequest({
phoneNumber,
ipAddress,
endpoint: '/auth/request-otp',
});
// Find or create user
let user = await this.userRepository.findByPhoneNumber(phoneNumber);
if (!user) {
// Create new user if doesn't exist
user = await this.userRepository.create({
phoneNumber,
statusId: 1,
otpAttempts: 0,
resendCount: 0,
phoneVerified: false,
provider: 'phone',
languagePreference: 'en',
availability: false,
points: 0,
createdAt: new Date(),
updatedAt: new Date(),
});
}
// Check if user can request OTP (includes verification attempts, resend limit and rate limiting)
if (!user.canRequestOtp()) {
// Track failed request
await this.securityService.trackOtpRequest({
phoneNumber,
ipAddress,
userAgent,
success: false,
});
if (user.otpAttempts >= 5) {
throw new HttpException(
'Maximum OTP verification attempts reached. Please try again later.',
HttpStatus.TOO_MANY_REQUESTS,
);
}
if (user.resendCount >= 5) {
throw new HttpException(
'Maximum OTP resend attempts reached. Please contact support.',
HttpStatus.TOO_MANY_REQUESTS,
);
}
// If we're here, it's likely due to rate limiting
throw new HttpException(
'Please wait at least 1 minute before requesting another OTP.',
HttpStatus.TOO_MANY_REQUESTS,
);
}
// Generate and set OTP
const otpCode = this.otpService.generateOtp();
user.setOtp(otpCode);
// Save updated user
await this.userRepository.save(user);
// Send OTP
await this.otpService.sendOtp(phoneNumber, otpCode);
// Track successful request
await this.securityService.trackOtpRequest({
phoneNumber,
ipAddress,
userAgent,
success: true,
});
return {
message: 'OTP sent successfully.',
resendCount: user.resendCount,
maxResendAttempts: 5,
};
}
async verifyOtp(phoneNumber: string, otpCode: string, @Req() request: Request): Promise<{ token: string; message: string; user: UserDto }> {
const { ipAddress } = this.securityService.getClientInfo(request);
// Find user
const user = await this.userRepository.findByPhoneNumber(phoneNumber);
if (!user) {
throw new HttpException(
'User not found.',
HttpStatus.NOT_FOUND,
);
}
// Check if OTP has expired
if (user.isOtpExpired()) {
throw new HttpException(
'OTP has expired. Please request a new one.',
HttpStatus.BAD_REQUEST,
);
}
// Increment attempts counter
user.incrementOtpAttempts();
// Validate OTP
if (!user.isOtpValid(otpCode)) {
// Save updated attempts
await this.userRepository.save(user);
throw new HttpException(
user.otpAttempts >= 5
? 'Maximum OTP verification attempts reached. Please request a new OTP.'
: 'Invalid OTP. Please try again.',
HttpStatus.BAD_REQUEST,
);
}
// Mark phone as verified and reset OTP
user.verifyPhone();
await this.userRepository.save(user);
// Mark OTP as verified in tracking
await this.securityService.markOtpVerified(phoneNumber);
// Generate JWT token
const token = await this.generateToken(user);
// Transform to DTO
const userDto = plainToClass(UserDto, user);
return {
token,
message: 'OTP verified successfully.',
user: userDto,
};
}
private async generateToken(user: User): Promise<string> {
const payload = {
id: user.id,
type: 'access',
};
return this.jwtService.signAsync(payload, {
secret: this.configService.get('auth.secret'),
expiresIn: this.configService.get('auth.expires'),
});
}
}
// src/modules/auth/application/services/auth.service.ts
import { HttpException, HttpStatus, Injectable, Req } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
import { UserRepository } from '../../../user/infrastructure/persistence/repositories/user.repository';
import { OtpService } from './otp.service';
import { User } from '../../../user/domain/user.model';
import { UserDto } from '../dto/user.dto';
import { plainToClass } from 'class-transformer';
import { SecurityService } from '../../../security/application/services/security.service';
import { Request } from 'express';
@Injectable()
export class AuthService {
constructor(
private userRepository: UserRepository,
private otpService: OtpService,
private jwtService: JwtService,
private configService: ConfigService,
private securityService: SecurityService,
) {}
async requestOtp(phoneNumber: string, @Req() request: Request): Promise<{ message: string; resendCount?: number; maxResendAttempts?: number }> {
const { ipAddress, userAgent } = this.securityService.getClientInfo(request);
// Enhanced security check
await this.securityService.validateOtpRequest({
phoneNumber,
ipAddress,
endpoint: '/auth/request-otp',
});
// Find or create user
let user = await this.userRepository.findByPhoneNumber(phoneNumber);
if (!user) {
// Create new user if doesn't exist
user = await this.userRepository.create({
phoneNumber,
statusId: 1,
otpAttempts: 0,
resendCount: 0,
phoneVerified: false,
provider: 'phone',
languagePreference: 'en',
availability: false,
points: 0,
createdAt: new Date(),
updatedAt: new Date(),
});
}
// Check if user can request OTP (includes verification attempts, resend limit and rate limiting)
if (!user.canRequestOtp()) {
// Track failed request
await this.securityService.trackOtpRequest({
phoneNumber,
ipAddress,
userAgent,
success: false,
});
if (user.otpAttempts >= 5) {
throw new HttpException(
'Maximum OTP verification attempts reached. Please try again later.',
HttpStatus.TOO_MANY_REQUESTS,
);
}
if (user.resendCount >= 5) {
throw new HttpException(
'Maximum OTP resend attempts reached. Please contact support.',
HttpStatus.TOO_MANY_REQUESTS,
);
}
// If we're here, it's likely due to rate limiting
throw new HttpException(
'Please wait at least 1 minute before requesting another OTP.',
HttpStatus.TOO_MANY_REQUESTS,
);
}
// Generate and set OTP
const otpCode = this.otpService.generateOtp();
user.setOtp(otpCode);
// Save updated user
await this.userRepository.save(user);
// Send OTP
await this.otpService.sendOtp(phoneNumber, otpCode);
// Track successful request
await this.securityService.trackOtpRequest({
phoneNumber,
ipAddress,
userAgent,
success: true,
});
return {
message: 'OTP sent successfully.',
resendCount: user.resendCount,
maxResendAttempts: 5,
};
}
async verifyOtp(phoneNumber: string, otpCode: string, @Req() request: Request): Promise<{ token: string; message: string; user: UserDto }> {
const { ipAddress } = this.securityService.getClientInfo(request);
// Find user
const user = await this.userRepository.findByPhoneNumber(phoneNumber);
if (!user) {
throw new HttpException(
'User not found.',
HttpStatus.NOT_FOUND,
);
}
// Check if OTP has expired
if (user.isOtpExpired()) {
throw new HttpException(
'OTP has expired. Please request a new one.',
HttpStatus.BAD_REQUEST,
);
}
// Increment attempts counter
user.incrementOtpAttempts();
// Validate OTP
if (!user.isOtpValid(otpCode)) {
// Save updated attempts
await this.userRepository.save(user);
throw new HttpException(
user.otpAttempts >= 5
? 'Maximum OTP verification attempts reached. Please request a new OTP.'
: 'Invalid OTP. Please try again.',
HttpStatus.BAD_REQUEST,
);
}
// Mark phone as verified and reset OTP
user.verifyPhone();
await this.userRepository.save(user);
// Mark OTP as verified in tracking
await this.securityService.markOtpVerified(phoneNumber);
// Generate JWT token
const token = await this.generateToken(user);
// Transform to DTO
const userDto = plainToClass(UserDto, user);
return {
token,
message: 'OTP verified successfully.',
user: userDto,
};
}
private async generateToken(user: User): Promise<string> {
const payload = {
id: user.id,
type: 'access',
};
return this.jwtService.signAsync(payload, {
secret: this.configService.get('auth.secret'),
expiresIn: this.configService.get('auth.expires'),
});
}
}
// src/modules/auth/application/services/auth.service.ts
import { HttpException, HttpStatus, Injectable, Req } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
import { UserRepository } from '../../../user/infrastructure/persistence/repositories/user.repository';
import { OtpService } from './otp.service';
import { User } from '../../../user/domain/user.model';
import { UserDto } from '../dto/user.dto';
import { plainToClass } from 'class-transformer';
import { SecurityService } from '../../../security/application/services/security.service';
import { Request } from 'express';
@Injectable()
export class AuthService {
constructor(
private userRepository: UserRepository,
private otpService: OtpService,
private jwtService: JwtService,
private configService: ConfigService,
private securityService: SecurityService,
) {}
async requestOtp(phoneNumber: string, @Req() request: Request): Promise<{ message: string; resendCount?: number; maxResendAttempts?: number }> {
const { ipAddress, userAgent } = this.securityService.getClientInfo(request);
// Enhanced security check
await this.securityService.validateOtpRequest({
phoneNumber,
ipAddress,
endpoint: '/auth/request-otp',
});
// Find or create user
let user = await this.userRepository.findByPhoneNumber(phoneNumber);
if (!user) {
// Create new user if doesn't exist
user = await this.userRepository.create({
phoneNumber,
statusId: 1,
otpAttempts: 0,
resendCount: 0,
phoneVerified: false,
provider: 'phone',
languagePreference: 'en',
availability: false,
points: 0,
createdAt: new Date(),
updatedAt: new Date(),
});
}
// Check if user can request OTP (includes verification attempts, resend limit and rate limiting)
if (!user.canRequestOtp()) {
// Track failed request
await this.securityService.trackOtpRequest({
phoneNumber,
ipAddress,
userAgent,
success: false,
});
if (user.otpAttempts >= 5) {
throw new HttpException(
'Maximum OTP verification attempts reached. Please try again later.',
HttpStatus.TOO_MANY_REQUESTS,
);
}
if (user.resendCount >= 5) {
throw new HttpException(
'Maximum OTP resend attempts reached. Please contact support.',
HttpStatus.TOO_MANY_REQUESTS,
);
}
// If we're here, it's likely due to rate limiting
throw new HttpException(
'Please wait at least 1 minute before requesting another OTP.',
HttpStatus.TOO_MANY_REQUESTS,
);
}
// Generate and set OTP
const otpCode = this.otpService.generateOtp();
user.setOtp(otpCode);
// Save updated user
await this.userRepository.save(user);
// Send OTP
await this.otpService.sendOtp(phoneNumber, otpCode);
// Track successful request
await this.securityService.trackOtpRequest({
phoneNumber,
ipAddress,
userAgent,
success: true,
});
return {
message: 'OTP sent successfully.',
resendCount: user.resendCount,
maxResendAttempts: 5,
};
}
async verifyOtp(phoneNumber: string, otpCode: string, @Req() request: Request): Promise<{ token: string; message: string; user: UserDto }> {
const { ipAddress } = this.securityService.getClientInfo(request);
// Find user
const user = await this.userRepository.findByPhoneNumber(phoneNumber);
if (!user) {
throw new HttpException(
'User not found.',
HttpStatus.NOT_FOUND,
);
}
// Check if OTP has expired
if (user.isOtpExpired()) {
throw new HttpException(
'OTP has expired. Please request a new one.',
HttpStatus.BAD_REQUEST,
);
}
// Increment attempts counter
user.incrementOtpAttempts();
// Validate OTP
if (!user.isOtpValid(otpCode)) {
// Save updated attempts
await this.userRepository.save(user);
throw new HttpException(
user.otpAttempts >= 5
? 'Maximum OTP verification attempts reached. Please request a new OTP.'
: 'Invalid OTP. Please try again.',
HttpStatus.BAD_REQUEST,
);
}
// Mark phone as verified and reset OTP
user.verifyPhone();
await this.userRepository.save(user);
// Mark OTP as verified in tracking
await this.securityService.markOtpVerified(phoneNumber);
// Generate JWT token
const token = await this.generateToken(user);
// Transform to DTO
const userDto = plainToClass(UserDto, user);
return {
token,
message: 'OTP verified successfully.',
user: userDto,
};
}
private async generateToken(user: User): Promise<string> {
const payload = {
id: user.id,
type: 'access',
};
return this.jwtService.signAsync(payload, {
secret: this.configService.get('auth.secret'),
expiresIn: this.configService.get('auth.expires'),
});
}
}
// src/modules/security/security.module.ts
import { Module, MiddlewareConsumer, RequestMethod } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule } from '@nestjs/config';
import { IpRateLimit } from './infrastructure/persistence/entities/ip-rate-limit.entity';
import { OtpRequestTracking } from './infrastructure/persistence/entities/otp-request-tracking.entity';
import { SecurityService } from './application/services/security.service';
import { IpRateLimitMiddleware } from '../../common/middleware/ip-rate-limit.middleware';
@Module({
imports: [
TypeOrmModule.forFeature([
IpRateLimit,
OtpRequestTracking,
]),
ConfigModule,
],
providers: [
SecurityService,
],
exports: [
SecurityService,
],
})
export class SecurityModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(IpRateLimitMiddleware)
.forRoutes(
{ path: 'auth/request-otp', method: RequestMethod.POST },
{ path: 'auth/verify-otp', method: RequestMethod.POST },
{ path: 'auth/register', method: RequestMethod.POST },
);
}
}
// src/config/rate-limit.config.ts
import { registerAs } from '@nestjs/config';
export default registerAs('rateLimit', () => ({
// General rate limit settings
maxRequests: parseInt(process.env.RATE_LIMIT_MAX_REQUESTS || '5', 10),
windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS || '3600000', 10), // 1 hour default
blockDuration: parseInt(process.env.RATE_LIMIT_BLOCK_DURATION || '86400000', 10), // 24 hours default
}));
// src/config/security.config.ts
import { registerAs } from '@nestjs/config';
export default registerAs('security', () => ({
// Advanced security settings
maxPhonesPerIp: parseInt(process.env.SECURITY_MAX_PHONES_PER_IP || '3', 10),
otpTimeWindowMs: parseInt(process.env.SECURITY_OTP_TIME_WINDOW || '60000', 10), // 1 minute
suspiciousThreshold: parseInt(process.env.SECURITY_SUSPICIOUS_THRESHOLD || '10', 10),
}));
// src/modules/auth/auth.controller.ts
import { Body, Controller, HttpCode, HttpStatus, Post, Req } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
import { AuthService } from './application/services/auth.service';
import { RequestOtpDto, VerifyOtpDto, AuthResponseDto } from './application/dto/auth.dto';
import { Request } from 'express';
@ApiTags('Auth')
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@ApiOperation({ summary: 'Request OTP code' })
@ApiResponse({
status: HttpStatus.OK,
description: 'OTP sent successfully',
schema: {
properties: {
message: { type: 'string', example: 'OTP sent successfully.' },
resendCount: { type: 'number', example: 1 },
maxResendAttempts: { type: 'number', example: 5 },
},
},
})
@ApiResponse({
status: HttpStatus.TOO_MANY_REQUESTS,
description: 'Rate limit exceeded',
})
@HttpCode(HttpStatus.OK)
@Post('request-otp')
async requestOtp(@Body() dto: RequestOtpDto, @Req() request: Request) {
return this.authService.requestOtp(dto.phoneNumber, request);
}
@ApiOperation({ summary: 'Verify OTP code' })
@ApiResponse({
status: HttpStatus.OK,
description: 'OTP verified successfully',
type: AuthResponseDto,
})
@HttpCode(HttpStatus.OK)
@Post('verify-otp')
async verifyOtp(@Body() dto: VerifyOtpDto, @Req() request: Request) {
return this.authService.verifyOtp(dto.phoneNumber, dto.otpCode, request);
}
}

For preventing spam registrations and limiting OTP requests by IP address, you need a robust solution that can track and limit requests. Let me outline the best approach for this in a NestJS application with hexagonal architecture.

Design: Rate Limiting by IP for OTP Requests

I recommend a combined approach using:

  1. Middleware for primary IP-based throttling
  2. Service layer for advanced tracking
  3. Dedicated database table for persistent tracking

This gives you both immediate protection and long-term tracking.

Database Table Design​​​​​​​​​​​​​​​​

Middleware Implementation​​​​​​​​​​​​​​​​

Entity Definitions​​​​​​​​​​​​​​​​

Enhanced Security Service​​​​​​​​​​​​​​​​

Integration with Auth Module​​​​​​​​​​​​​​​​

Security Module Configuration​​​​​​​​​​​​​​​​

Controller Updates​​​​​​​​​​​​​​​​

Comprehensive Anti-Spam Strategy for OTP Authentication

I've designed a multi-layered approach to protect your OTP authentication system from spam and abuse. This solution combines middleware-based rate limiting with an advanced security tracking system to detect and prevent various types of abuse.

Key Components

  1. IP-Based Rate Limiting

    • Middleware implementation that tracks requests by IP address
    • Configurable limits for maximum requests within a time window
    • Automatic blocking of IPs that exceed limits
    • Database persistence for tracking across application restarts
  2. Advanced OTP Security Analysis

    • Dedicated tracking table for all OTP requests
    • Pattern detection for suspicious activity
    • Verification success rate monitoring
    • Multiple phone number detection from same IP address
  3. Multi-Level Protection

    • Per-user limits (5 verification attempts)
    • Per-user resend limits (5 OTP requests)
    • Per-IP request limits (configurable)
    • Time-based cooldowns between requests

Database Design

The solution uses two tables:

  1. ip_rate_limits

    • Tracks request counts by IP and endpoint
    • Records time windows for rate limiting
    • Stores blocking status and duration
  2. otp_request_tracking

    • Records all OTP request details including phone, IP, and user agent
    • Tracks​​​​​​​​​​​​​​​​
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment