feat(auth): add abuse protection for login and OIDC flows with consent-based authorization handling

This commit is contained in:
2026-04-20 09:46:13 +00:00
parent 21f5abb49b
commit 29a21fd3b3
36 changed files with 1129 additions and 84 deletions
@@ -5,6 +5,34 @@ import { Reception } from './classes.reception.js';
import { logger } from './logging.js';
export class LoginSessionManager {
private readonly abuseProtectionConfigs = {
passwordLogin: {
maxAttempts: 5,
windowMillis: plugins.smarttime.getMilliSecondsFromUnits({ minutes: 15 }),
blockDurationMillis: plugins.smarttime.getMilliSecondsFromUnits({ minutes: 30 }),
},
emailLoginRequest: {
maxAttempts: 5,
windowMillis: plugins.smarttime.getMilliSecondsFromUnits({ minutes: 15 }),
blockDurationMillis: plugins.smarttime.getMilliSecondsFromUnits({ minutes: 15 }),
},
emailLoginToken: {
maxAttempts: 5,
windowMillis: plugins.smarttime.getMilliSecondsFromUnits({ minutes: 15 }),
blockDurationMillis: plugins.smarttime.getMilliSecondsFromUnits({ minutes: 30 }),
},
passwordResetRequest: {
maxAttempts: 5,
windowMillis: plugins.smarttime.getMilliSecondsFromUnits({ minutes: 15 }),
blockDurationMillis: plugins.smarttime.getMilliSecondsFromUnits({ minutes: 15 }),
},
passwordResetCompletion: {
maxAttempts: 5,
windowMillis: plugins.smarttime.getMilliSecondsFromUnits({ minutes: 15 }),
blockDurationMillis: plugins.smarttime.getMilliSecondsFromUnits({ minutes: 30 }),
},
};
// refs
public receptionRef: Reception;
public get db() {
@@ -23,6 +51,14 @@ export class LoginSessionManager {
new plugins.typedrequest.TypedHandler<plugins.idpInterfaces.request.IReq_LoginWithEmailOrUsernameAndPassword>(
'loginWithEmailOrUsernameAndPassword',
async (requestData) => {
const loginIdentifier = requestData.username;
await this.receptionRef.abuseProtectionManager.consumeAttempt(
'passwordLogin',
loginIdentifier,
this.abuseProtectionConfigs.passwordLogin,
'Too many login attempts. Please wait before trying again.'
);
let user = await this.receptionRef.userManager.CUser.getInstance({
data: {
username: requestData.username,
@@ -54,6 +90,11 @@ export class LoginSessionManager {
throw new plugins.typedrequest.TypedResponseError('Could not create login session');
}
await this.receptionRef.abuseProtectionManager.clearAttempts(
'passwordLogin',
loginIdentifier
);
return {
refreshToken,
twoFaNeeded: false,
@@ -69,6 +110,12 @@ export class LoginSessionManager {
new plugins.typedrequest.TypedHandler<plugins.idpInterfaces.request.IReq_LoginWithEmail>(
'loginWithEmail',
async (requestDataArg) => {
await this.receptionRef.abuseProtectionManager.consumeAttempt(
'emailLoginRequest',
requestDataArg.email,
this.abuseProtectionConfigs.emailLoginRequest,
'Too many magic link requests. Please wait before trying again.'
);
logger.log('info', `loginWithEmail requested for: ${requestDataArg.email}`);
const existingUser = await this.receptionRef.userManager.CUser.getInstance({
data: {
@@ -101,6 +148,12 @@ export class LoginSessionManager {
new plugins.typedrequest.TypedHandler<plugins.idpInterfaces.request.IReq_LoginWithEmailAfterEmailTokenAquired>(
'loginWithEmailAfterEmailTokenAquired',
async (requestArg) => {
await this.receptionRef.abuseProtectionManager.consumeAttempt(
'emailLoginToken',
requestArg.email,
this.abuseProtectionConfigs.emailLoginToken,
'Too many magic link attempts. Please wait before trying again.'
);
const tokenObject = await this.consumeEmailActionToken(
requestArg.email,
requestArg.token,
@@ -120,6 +173,10 @@ export class LoginSessionManager {
if (!refreshToken) {
throw new plugins.typedrequest.TypedResponseError('Could not create login session');
}
await this.receptionRef.abuseProtectionManager.clearAttempts(
'emailLoginToken',
requestArg.email
);
return {
refreshToken,
};
@@ -188,6 +245,12 @@ export class LoginSessionManager {
new plugins.typedrequest.TypedHandler<plugins.idpInterfaces.request.IReq_ResetPassword>(
'resetPassword',
async (requestDataArg) => {
await this.receptionRef.abuseProtectionManager.consumeAttempt(
'passwordResetRequest',
requestDataArg.email,
this.abuseProtectionConfigs.passwordResetRequest,
'Too many password reset requests. Please wait before trying again.'
);
const emailOfPasswordToReset = requestDataArg.email;
const existingUser = await this.receptionRef.userManager.CUser.getInstance({
data: {
@@ -216,6 +279,12 @@ export class LoginSessionManager {
new plugins.typedrequest.TypedHandler<plugins.idpInterfaces.request.IReq_SetNewPassword>(
'setNewPassword',
async (requestData) => {
await this.receptionRef.abuseProtectionManager.consumeAttempt(
'passwordResetCompletion',
requestData.email,
this.abuseProtectionConfigs.passwordResetCompletion,
'Too many password change attempts. Please wait before trying again.'
);
const user = await this.receptionRef.userManager.CUser.getInstance({
data: {
email: requestData.email,
@@ -253,6 +322,10 @@ export class LoginSessionManager {
requestData.newPassword
);
await user.save();
await this.receptionRef.abuseProtectionManager.clearAttempts(
'passwordResetCompletion',
requestData.email
);
return {
status: 'ok',
};