feat: added user-locking on mfa failure

This commit is contained in:
Sheen Capadngan
2024-05-22 21:55:56 +08:00
parent 880289217e
commit 41d72d5dc6
11 changed files with 240 additions and 12 deletions
+53
View File
@@ -1,5 +1,7 @@
import slugify from "@sindresorhus/slugify";
import { TableName } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { TUserDALFactory } from "@app/services/user/user-dal";
@@ -19,3 +21,54 @@ export const normalizeUsername = async (username: string, userDAL: Pick<TUserDAL
}
}
};
export const processFailedMfaAttempt = async (userId: string, userDAL: Pick<TUserDALFactory, "transaction">) => {
try {
const updatedUser = await userDAL.transaction(async (tx) => {
const PROGRESSIVE_DELAY_INTERVAL = 3;
const [user] = await tx(TableName.Users)
.where("id", userId)
.increment("consecutiveFailedMfaAttempts", 1)
.returning("*");
if (!user) {
throw new Error("User not found");
}
const progressiveDelaysInMins = [5, 30, 60];
// lock user when failed attempt exceeds threshold
if (user.consecutiveFailedMfaAttempts > PROGRESSIVE_DELAY_INTERVAL * progressiveDelaysInMins.length) {
return (
await tx(TableName.Users)
.where("id", userId)
.update({
isLocked: true,
temporaryLockDateEnd: null
})
.returning("*")
)[0];
}
// delay user only when failed MFA attempts is a multiple of configured delay interval
if (user.consecutiveFailedMfaAttempts % PROGRESSIVE_DELAY_INTERVAL === 0) {
const delayIndex = user.consecutiveFailedMfaAttempts / PROGRESSIVE_DELAY_INTERVAL - 1;
return (
await tx(TableName.Users)
.where("id", userId)
.update({
temporaryLockDateEnd: new Date(new Date().getTime() + progressiveDelaysInMins[delayIndex] * 60 * 1000)
})
.returning("*")
)[0];
}
return user;
});
return updatedUser;
} catch (error) {
throw new DatabaseError({ error, name: "Process failed MFA Attempt" });
}
};
+15 -1
View File
@@ -207,6 +207,19 @@ export const userServiceFactory = ({
return userAction;
};
const unlockUser = async (userId: string, token: string) => {
await tokenService.validateTokenForUser({
userId,
code: token,
type: TokenType.TOKEN_USER_UNLOCK
});
await userDAL.update(
{ id: userId },
{ consecutiveFailedMfaAttempts: 0, isLocked: false, temporaryLockDateEnd: null }
);
};
return {
sendEmailVerificationCode,
verifyEmailVerificationCode,
@@ -216,6 +229,7 @@ export const userServiceFactory = ({
deleteMe,
getMe,
createUserAction,
getUserAction
getUserAction,
unlockUser
};
};