@experium/nest-bruteforce-guard
Version:
Authorization protection from bruteforce
160 lines (132 loc) • 4.81 kB
text/typescript
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(
(LoginAttempt)
private readonly repository: MongoRepository<LoginAttempt>,
(BRUTEFORCE_GUARD_OPTIONS_PROVIDER)
readonly config: BruteforceGuardConfiguration,
private readonly registry: ExceptionCatcherRegistry,
) {}
async canLogin(context: ExecutionContext): Promise<boolean> {
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<void> {
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<Cursor<LoginAttempt>> {
return this.repository.createEntityCursor(await this.getDefaultQuery(filter));
}
async getDefaultQuery(filter: ObjectLiteral): Promise<Partial<LoginAttempt>> {
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<void> {
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<number> {
return await this.repository.count({
login,
date: {
$gt: addMinutes(new Date(), -this.config.attemptMinutesByLogin),
},
loginFailure: true,
});
}
private async getAttemptsCountByIP(ip: string): Promise<number> {
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];
}
}