mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-05 08:26:29 +00:00
misc: addressed PR comments
This commit is contained in:
@@ -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>>;
|
||||||
|
|||||||
@@ -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
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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" });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|||||||
Reference in New Issue
Block a user