121 lines
3.8 KiB
TypeScript
121 lines
3.8 KiB
TypeScript
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<boolean> {
|
|
|
|
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,
|
|
}
|
|
}
|
|
}
|