feat(auth): add abuse protection for login and OIDC flows with consent-based authorization handling
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
|
||||
import { Reception } from './classes.reception.js';
|
||||
import { AbuseWindow } from './classes.abusewindow.js';
|
||||
|
||||
export interface IAbuseProtectionConfig {
|
||||
maxAttempts: number;
|
||||
windowMillis: number;
|
||||
blockDurationMillis: number;
|
||||
}
|
||||
|
||||
export class AbuseProtectionManager {
|
||||
public receptionRef: Reception;
|
||||
|
||||
public get db() {
|
||||
return this.receptionRef.db.smartdataDb;
|
||||
}
|
||||
|
||||
public CAbuseWindow = plugins.smartdata.setDefaultManagerForDoc(this, AbuseWindow);
|
||||
|
||||
constructor(receptionRefArg: Reception) {
|
||||
this.receptionRef = receptionRefArg;
|
||||
}
|
||||
|
||||
private normalizeIdentifier(identifierArg: string) {
|
||||
return identifierArg.trim().toLowerCase();
|
||||
}
|
||||
|
||||
private hashIdentifier(identifierArg: string) {
|
||||
return plugins.smarthash.sha256FromStringSync(this.normalizeIdentifier(identifierArg));
|
||||
}
|
||||
|
||||
private createWindowId(actionArg: string, identifierArg: string) {
|
||||
return plugins.smarthash.sha256FromStringSync(
|
||||
`${actionArg}:${this.hashIdentifier(identifierArg)}`
|
||||
);
|
||||
}
|
||||
|
||||
private async getWindow(actionArg: string, identifierArg: string) {
|
||||
return this.CAbuseWindow.getInstance({
|
||||
id: this.createWindowId(actionArg, identifierArg),
|
||||
});
|
||||
}
|
||||
|
||||
public async consumeAttempt(
|
||||
actionArg: string,
|
||||
identifierArg: string,
|
||||
configArg: IAbuseProtectionConfig,
|
||||
errorTextArg = 'Too many attempts. Please wait before trying again.'
|
||||
) {
|
||||
const now = Date.now();
|
||||
let abuseWindow = await this.getWindow(actionArg, identifierArg);
|
||||
|
||||
if (!abuseWindow) {
|
||||
abuseWindow = new AbuseWindow();
|
||||
abuseWindow.id = this.createWindowId(actionArg, identifierArg);
|
||||
abuseWindow.data.action = actionArg;
|
||||
abuseWindow.data.identifierHash = this.hashIdentifier(identifierArg);
|
||||
abuseWindow.data.createdAt = now;
|
||||
}
|
||||
|
||||
if (abuseWindow.isBlocked(now)) {
|
||||
throw new plugins.typedrequest.TypedResponseError(errorTextArg);
|
||||
}
|
||||
|
||||
if (abuseWindow.data.blockedUntil && abuseWindow.data.blockedUntil <= now) {
|
||||
abuseWindow.data.attemptCount = 0;
|
||||
abuseWindow.data.windowStartedAt = now;
|
||||
abuseWindow.data.blockedUntil = 0;
|
||||
}
|
||||
|
||||
if (
|
||||
!abuseWindow.data.windowStartedAt ||
|
||||
abuseWindow.data.windowStartedAt + configArg.windowMillis <= now
|
||||
) {
|
||||
abuseWindow.data.attemptCount = 0;
|
||||
abuseWindow.data.windowStartedAt = now;
|
||||
}
|
||||
|
||||
abuseWindow.data.attemptCount += 1;
|
||||
abuseWindow.data.updatedAt = now;
|
||||
abuseWindow.data.validUntil = now + configArg.windowMillis;
|
||||
|
||||
if (abuseWindow.data.attemptCount > configArg.maxAttempts) {
|
||||
abuseWindow.data.blockedUntil = now + configArg.blockDurationMillis;
|
||||
abuseWindow.data.validUntil = abuseWindow.data.blockedUntil;
|
||||
await abuseWindow.save();
|
||||
throw new plugins.typedrequest.TypedResponseError(errorTextArg);
|
||||
}
|
||||
|
||||
await abuseWindow.save();
|
||||
}
|
||||
|
||||
public async clearAttempts(actionArg: string, identifierArg: string) {
|
||||
const abuseWindow = await this.getWindow(actionArg, identifierArg);
|
||||
if (!abuseWindow) {
|
||||
return;
|
||||
}
|
||||
|
||||
await abuseWindow.delete();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user