misc: addressed PR comments

This commit is contained in:
Sheen Capadngan
2024-05-24 23:45:16 +08:00
parent 9b2b6d61be
commit c0daa11aeb
7 changed files with 79 additions and 69 deletions
+3 -6
View File
@@ -23,14 +23,11 @@ export const UsersSchema = z.object({
isGhost: z.boolean().default(false), isGhost: z.boolean().default(false),
username: z.string(), username: z.string(),
isEmailVerified: z.boolean().default(false).nullable().optional(), isEmailVerified: z.boolean().default(false).nullable().optional(),
consecutiveFailedMfaAttempts: z.number(), consecutiveFailedMfaAttempts: z.number().optional(),
isLocked: z.boolean(), isLocked: z.boolean().optional(),
temporaryLockDateEnd: z.date().nullable().optional() temporaryLockDateEnd: z.date().nullable().optional()
}); });
export type TUsers = z.infer<typeof UsersSchema>; export type TUsers = z.infer<typeof UsersSchema>;
export type TUsersInsert = Omit< export type TUsersInsert = Omit<z.input<typeof UsersSchema>, TImmutableDBKeys>;
z.input<typeof UsersSchema>,
TImmutableDBKeys | "isLocked" | "consecutiveFailedMfaAttempts"
>;
export type TUsersUpdate = Partial<Omit<z.input<typeof UsersSchema>, TImmutableDBKeys>>; export type TUsersUpdate = Partial<Omit<z.input<typeof UsersSchema>, TImmutableDBKeys>>;
+1 -1
View File
@@ -32,7 +32,7 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
server.route({ server.route({
method: "GET", method: "GET",
url: "/:userId/unlock-verify", url: "/:userId/unlock",
config: { config: {
rateLimit: authRateLimit rateLimit: authRateLimit
}, },
+1 -1
View File
@@ -50,7 +50,7 @@ export const enforceUserLockStatus = (isLocked: boolean, temporaryLockDateEnd?:
throw new UnauthorizedError({ throw new UnauthorizedError({
name: "User Locked", name: "User Locked",
message: message:
"User is locked due to multiple failed login attempts. An email has been sent to you in order to unlock your account." "User is locked due to multiple failed login attempts. An email has been sent to you in order to unlock your account. You can also reset your password to unlock."
}); });
} }
@@ -4,7 +4,7 @@ import { TUsers, UserDeviceSchema } from "@app/db/schemas";
import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns"; import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto"; import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
import { BadRequestError, UnauthorizedError } from "@app/lib/errors"; import { BadRequestError, DatabaseError, UnauthorizedError } from "@app/lib/errors";
import { getServerCfg } from "@app/services/super-admin/super-admin-service"; import { getServerCfg } from "@app/services/super-admin/super-admin-service";
import { TTokenDALFactory } from "../auth-token/auth-token-dal"; import { TTokenDALFactory } from "../auth-token/auth-token-dal";
@@ -13,7 +13,6 @@ import { TokenType } from "../auth-token/auth-token-types";
import { TOrgDALFactory } from "../org/org-dal"; import { TOrgDALFactory } from "../org/org-dal";
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
import { TUserDALFactory } from "../user/user-dal"; import { TUserDALFactory } from "../user/user-dal";
import { processFailedMfaAttempt } from "../user/user-fns";
import { enforceUserLockStatus, validateProviderAuthToken } from "./auth-fns"; import { enforceUserLockStatus, validateProviderAuthToken } from "./auth-fns";
import { import {
TLoginClientProofDTO, TLoginClientProofDTO,
@@ -214,7 +213,7 @@ export const authLoginServiceFactory = ({
// send multi factor auth token if they it enabled // send multi factor auth token if they it enabled
if (userEnc.isMfaEnabled && userEnc.email) { if (userEnc.isMfaEnabled && userEnc.email) {
const user = await userDAL.findById(userEnc.userId); const user = await userDAL.findById(userEnc.userId);
enforceUserLockStatus(user.isLocked, user.temporaryLockDateEnd); enforceUserLockStatus(Boolean(user.isLocked), user.temporaryLockDateEnd);
const mfaToken = jwt.sign( const mfaToken = jwt.sign(
{ {
@@ -304,13 +303,61 @@ export const authLoginServiceFactory = ({
const resendMfaToken = async (userId: string) => { const resendMfaToken = async (userId: string) => {
const user = await userDAL.findById(userId); const user = await userDAL.findById(userId);
if (!user || !user.email) return; if (!user || !user.email) return;
enforceUserLockStatus(user.isLocked, user.temporaryLockDateEnd); enforceUserLockStatus(Boolean(user.isLocked), user.temporaryLockDateEnd);
await sendUserMfaCode({ await sendUserMfaCode({
userId: user.id, userId: user.id,
email: user.email email: user.email
}); });
}; };
const processFailedMfaAttempt = async (userId: string) => {
try {
const updatedUser = await userDAL.transaction(async (tx) => {
const PROGRESSIVE_DELAY_INTERVAL = 3;
const user = await userDAL.incrementFailedMfaAttempt(userId, tx);
if (!user) {
throw new Error("User not found");
}
const progressiveDelaysInMins = [5, 30, 60];
// lock user when failed attempt exceeds threshold
if (
user.consecutiveFailedMfaAttempts &&
user.consecutiveFailedMfaAttempts >= PROGRESSIVE_DELAY_INTERVAL * (progressiveDelaysInMins.length + 1)
) {
return userDAL.updateById(
userId,
{
isLocked: true,
temporaryLockDateEnd: null
},
tx
);
}
// delay user only when failed MFA attempts is a multiple of configured delay interval
if (user.consecutiveFailedMfaAttempts && user.consecutiveFailedMfaAttempts % PROGRESSIVE_DELAY_INTERVAL === 0) {
const delayIndex = user.consecutiveFailedMfaAttempts / PROGRESSIVE_DELAY_INTERVAL - 1;
return userDAL.updateById(
userId,
{
temporaryLockDateEnd: new Date(new Date().getTime() + progressiveDelaysInMins[delayIndex] * 60 * 1000)
},
tx
);
}
return user;
});
return updatedUser;
} catch (error) {
throw new DatabaseError({ error, name: "Process failed MFA Attempt" });
}
};
/* /*
* Multi factor authentication verification of code * Multi factor authentication verification of code
* Third step of login in which user completes with mfa * Third step of login in which user completes with mfa
@@ -318,7 +365,7 @@ export const authLoginServiceFactory = ({
const verifyMfaToken = async ({ userId, mfaToken, mfaJwtToken, ip, userAgent, orgId }: TVerifyMfaTokenDTO) => { const verifyMfaToken = async ({ userId, mfaToken, mfaJwtToken, ip, userAgent, orgId }: TVerifyMfaTokenDTO) => {
const appCfg = getConfig(); const appCfg = getConfig();
const user = await userDAL.findById(userId); const user = await userDAL.findById(userId);
enforceUserLockStatus(user.isLocked, user.temporaryLockDateEnd); enforceUserLockStatus(Boolean(user.isLocked), user.temporaryLockDateEnd);
try { try {
await tokenService.validateTokenForUser({ await tokenService.validateTokenForUser({
@@ -327,7 +374,7 @@ export const authLoginServiceFactory = ({
code: mfaToken code: mfaToken
}); });
} catch (err) { } catch (err) {
const updatedUser = await processFailedMfaAttempt(userId, userDAL); const updatedUser = await processFailedMfaAttempt(userId);
if (updatedUser.isLocked) { if (updatedUser.isLocked) {
if (updatedUser.email) { if (updatedUser.email) {
const unlockToken = await tokenService.createTokenForUser({ const unlockToken = await tokenService.createTokenForUser({
@@ -341,7 +388,7 @@ export const authLoginServiceFactory = ({
recipients: [updatedUser.email], recipients: [updatedUser.email],
substitutions: { substitutions: {
token: unlockToken, token: unlockToken,
callback_url: `${appCfg.SITE_URL}/api/v1/user/${updatedUser.id}/unlock-verify` callback_url: `${appCfg.SITE_URL}/api/v1/user/${updatedUser.id}/unlock`
} }
}); });
} }
@@ -138,6 +138,11 @@ export const authPaswordServiceFactory = ({
code code
}); });
await userDAL.updateById(user.id, {
isLocked: false,
temporaryLockDateEnd: null
});
const token = jwt.sign( const token = jwt.sign(
{ {
authTokenType: AuthTokenType.SIGNUP_TOKEN, authTokenType: AuthTokenType.SIGNUP_TOKEN,
+15 -1
View File
@@ -143,6 +143,19 @@ export const userDALFactory = (db: TDbClient) => {
} }
}; };
const incrementFailedMfaAttempt = async (userId: string, tx?: Knex) => {
try {
const [user] = await (tx || db)(TableName.Users)
.where("id", userId)
.increment("consecutiveFailedMfaAttempts", 1)
.returning("*");
return user;
} catch (error) {
throw new DatabaseError({ error, name: "Increment Failed MFA Attempt" });
}
};
return { return {
...userOrm, ...userOrm,
findUserByUsername, findUserByUsername,
@@ -155,6 +168,7 @@ export const userDALFactory = (db: TDbClient) => {
upsertUserEncryptionKey, upsertUserEncryptionKey,
createUserEncryption, createUserEncryption,
findOneUserAction, findOneUserAction,
createUserAction createUserAction,
incrementFailedMfaAttempt
}; };
}; };
-53
View File
@@ -1,7 +1,5 @@
import slugify from "@sindresorhus/slugify"; import slugify from "@sindresorhus/slugify";
import { TableName } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
import { TUserDALFactory } from "@app/services/user/user-dal"; import { TUserDALFactory } from "@app/services/user/user-dal";
@@ -21,54 +19,3 @@ 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 + 1)) {
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" });
}
};