import { Injectable, Logger } from '@nestjs/common'; import { Request } from 'express'; import { ConfigService } from '../../../config/config.service'; import { SystemSettings } from '../../../domain/system-settings.types'; import { AuthAccessAttemptDao } from '../../../persistence/auth-access-attempt.dao'; import { CacheService } from '../../../cache/cache.service'; export enum AccessAttemptError { } interface RequestData { userAgent: string; requestPath: string; ip: string; ipAddressKey: string; usernameKey: string; } @Injectable() export class AccessAttemptService { private readonly logger: Logger = new Logger(AccessAttemptService.name); private readonly maxAttemptsAllowed: number; private readonly lockTimeout: number; constructor( private readonly cacheService: CacheService, private readonly accessAttemptService: AuthAccessAttemptDao, configService: ConfigService, ) { this.maxAttemptsAllowed = configService.get(SystemSettings.Auth.AccessAttempts.MaxAttempts); this.lockTimeout = configService.get(SystemSettings.Auth.AccessAttempts.Timeout); } async checkIfLocked(request: Request, username: string): Promise { const { ipAddressKey, usernameKey, ip } = this.parseRequestData(request, username); const ipAttempts = +(await this.cacheService.get(ipAddressKey) ?? 0); const usernameAttempts = +(await this.cacheService.get(usernameKey) ?? 0); if (ipAttempts >= this.maxAttemptsAllowed || usernameAttempts >= this.maxAttemptsAllowed) { const newIpAttempts = (ipAttempts + 1).toString(); await this.cacheService.set(ipAddressKey, newIpAttempts, this.lockTimeout); this.logger.debug(`${ip} now has ${newIpAttempts} failed attempts tracked`); const newUsernameAttempts = (usernameAttempts + 1).toString(); await this.cacheService.set(usernameKey, newUsernameAttempts, this.lockTimeout); this.logger.debug(`${username} now has ${newUsernameAttempts} failed attempts tracked`); return true; } return false; } async recordAttempt(request: Request, username: string, loginSucceeded: boolean): Promise<{ isLocked: boolean }> { const isLocked = await this.checkIfLocked(request, username); if (isLocked) { return { isLocked: true }; } const { userAgent, requestPath, ip, ipAddressKey, usernameKey, } = this.parseRequestData(request, username); await this.accessAttemptService.createAttempt({ userAgent, username, ip, requestPath, valid: loginSucceeded, }); if (loginSucceeded) { await this.cacheService.del(ipAddressKey); await this.cacheService.del(usernameKey); return { isLocked: false } } const ipAttempts = +(await this.cacheService.get(ipAddressKey) ?? 0); const usernameAttempts = +(await this.cacheService.get(usernameKey) ?? 0); const newIpAttempts = (ipAttempts + 1).toString(); await this.cacheService.set(ipAddressKey, newIpAttempts, this.lockTimeout); this.logger.debug(`${ip} now has ${newIpAttempts} failed attempts tracked`); const newUsernameAttempts = (usernameAttempts + 1).toString(); await this.cacheService.set(usernameKey, newUsernameAttempts, this.lockTimeout); this.logger.debug(`${username} now has ${newUsernameAttempts} failed attempts tracked`); return { isLocked: false }; } private parseRequestData(request: Request, username: string): RequestData { const userAgent = request.headers['user-agent'] ?? ''; const requestPath = request.path; const ip = request.ip ?? ''; const ipAddressKey = `access-attempt:ip:${ip}`; const usernameKey = `access-attempt:username:${username}`; return { userAgent, requestPath, ip, ipAddressKey, usernameKey, } } }