import { addMinutes } from 'date-fns'; import { Cursor, MongoRepository, ObjectLiteral } from 'typeorm'; import * as escapeStringRegexp from 'escape-string-regexp'; import { ExecutionContext, Inject } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { LoginAttempt } from './entity/login-attempt.entity'; import { BRUTEFORCE_GUARD_OPTIONS_PROVIDER } from './constants'; import { BruteforceGuardConfiguration } from './config/bruteforce-quard.configuration'; import { BruteforceDetectionException } from './exception/bruteforce-detection.exception'; import { ExceptionCatcherRegistry } from './catcher/exception-catcher.registry'; export class BruteforceGuardService { constructor( @InjectRepository(LoginAttempt) private readonly repository: MongoRepository, @Inject(BRUTEFORCE_GUARD_OPTIONS_PROVIDER) readonly config: BruteforceGuardConfiguration, private readonly registry: ExceptionCatcherRegistry, ) {} async canLogin(context: ExecutionContext): Promise { const login = this.getLoginByContext(context); const ip = context.switchToHttp().getRequest().ip; const loginAttemptsCount = await this.getAttemptsCountByLogin(login); const ipAttemptsCount = await this.getAttemptsCountByIP(ip); if ( login && loginAttemptsCount < this.config.attemptCountByLogin && ipAttemptsCount < this.config.attemptCountByIp ) { return true; } await this.repository.save(new LoginAttempt(login, ip, true, true)); throw new BruteforceDetectionException(); } async saveErrorAttempt(context: ExecutionContext, exception: any): Promise { const login = this.getLoginByContext(context); const ip = context.switchToHttp().getRequest().ip; let loginAttempt = null; for (const catcher of this.registry.getAll()) { loginAttempt = catcher.handle(exception, context); if (!!loginAttempt) { break; } } if (!loginAttempt) { loginAttempt = new LoginAttempt(login, ip, false); } await this.repository.save(loginAttempt); } async findByFilter(filter: ObjectLiteral): Promise> { return this.repository.createEntityCursor(await this.getDefaultQuery(filter)); } async getDefaultQuery(filter: ObjectLiteral): Promise> { const query: ObjectLiteral = {}; if ('login' in filter && filter.login) { query.login = { $regex: new RegExp(escapeStringRegexp(filter.login)), $options: 'i', }; } if ('ip' in filter && filter.ip) { query.ip = { $regex: new RegExp(escapeStringRegexp(filter.ip)), $options: 'i', }; } if ('attemptBlocked' in filter && typeof filter.attemptBlocked === 'boolean') { query.attemptBlocked = filter.attemptBlocked; } if ('loginFailure' in filter && typeof filter.loginFailure === 'boolean') { query.loginFailure = filter.loginFailure; } if ('userDisabled' in filter && typeof filter.userDisabled === 'boolean') { query.userDisabled = filter.userDisabled; } if ('badPassword' in filter && typeof filter.badPassword === 'boolean') { query.badPassword = filter.badPassword; } if ('dateFrom' in filter && filter.dateFrom) { query.$and = query.$and || []; query.$and = [ ...query.$and, { date: { $gte: new Date(filter.dateFrom), }, }, ]; } if ('dateTo' in filter && filter.dateTo) { query.$and = query.$and || []; query.$and = [ ...query.$and, { date: { $lte: new Date(filter.dateTo), }, }, ]; } return query; } async saveSuccessAttempt(context: ExecutionContext): Promise { const login = this.getLoginByContext(context); const ip = context.switchToHttp().getRequest().ip; await this.repository.save(new LoginAttempt(login, ip)); } async save(entity: LoginAttempt) { return await this.repository.save(entity); } private async getAttemptsCountByLogin(login: string): Promise { return await this.repository.count({ login, date: { $gt: addMinutes(new Date(), -this.config.attemptMinutesByLogin), }, loginFailure: true, }); } private async getAttemptsCountByIP(ip: string): Promise { return await this.repository.count({ ip, date: { $gt: addMinutes(new Date(), -this.config.attemptMinutesByIp), }, loginFailure: true, }); } private getLoginByContext(context: ExecutionContext): string { const loginField = this.config.loginField; return context.switchToHttp().getRequest().body[loginField]; } }