mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 21:27:31 +00:00
Merge pull request #1861 from Infisical/feat/secure-mfa-endpoints-with-improved-rate-limiting
feat: secure mfa endpoints with improved rate limiting and account locking
This commit is contained in:
@@ -0,0 +1,43 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
const hasConsecutiveFailedMfaAttempts = await knex.schema.hasColumn(TableName.Users, "consecutiveFailedMfaAttempts");
|
||||||
|
const hasIsLocked = await knex.schema.hasColumn(TableName.Users, "isLocked");
|
||||||
|
const hasTemporaryLockDateEnd = await knex.schema.hasColumn(TableName.Users, "temporaryLockDateEnd");
|
||||||
|
|
||||||
|
await knex.schema.alterTable(TableName.Users, (t) => {
|
||||||
|
if (!hasConsecutiveFailedMfaAttempts) {
|
||||||
|
t.integer("consecutiveFailedMfaAttempts").defaultTo(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasIsLocked) {
|
||||||
|
t.boolean("isLocked").defaultTo(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasTemporaryLockDateEnd) {
|
||||||
|
t.dateTime("temporaryLockDateEnd").nullable();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
const hasConsecutiveFailedMfaAttempts = await knex.schema.hasColumn(TableName.Users, "consecutiveFailedMfaAttempts");
|
||||||
|
const hasIsLocked = await knex.schema.hasColumn(TableName.Users, "isLocked");
|
||||||
|
const hasTemporaryLockDateEnd = await knex.schema.hasColumn(TableName.Users, "temporaryLockDateEnd");
|
||||||
|
|
||||||
|
await knex.schema.alterTable(TableName.Users, (t) => {
|
||||||
|
if (hasConsecutiveFailedMfaAttempts) {
|
||||||
|
t.dropColumn("consecutiveFailedMfaAttempts");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasIsLocked) {
|
||||||
|
t.dropColumn("isLocked");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasTemporaryLockDateEnd) {
|
||||||
|
t.dropColumn("temporaryLockDateEnd");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -22,7 +22,10 @@ export const UsersSchema = z.object({
|
|||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
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().optional(),
|
||||||
|
isLocked: z.boolean().optional(),
|
||||||
|
temporaryLockDateEnd: z.date().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TUsers = z.infer<typeof UsersSchema>;
|
export type TUsers = z.infer<typeof UsersSchema>;
|
||||||
|
|||||||
@@ -52,6 +52,14 @@ export const inviteUserRateLimit: RateLimitOptions = {
|
|||||||
keyGenerator: (req) => req.realIp
|
keyGenerator: (req) => req.realIp
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const mfaRateLimit: RateLimitOptions = {
|
||||||
|
timeWindow: 60 * 1000,
|
||||||
|
max: 20,
|
||||||
|
keyGenerator: (req) => {
|
||||||
|
return req.headers.authorization?.split(" ")[1] || req.realIp;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const creationLimit: RateLimitOptions = {
|
export const creationLimit: RateLimitOptions = {
|
||||||
// identity, project, org
|
// identity, project, org
|
||||||
timeWindow: 60 * 1000,
|
timeWindow: 60 * 1000,
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas";
|
import { UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas";
|
||||||
import { readLimit } from "@app/server/config/rateLimiter";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import { logger } from "@app/lib/logger";
|
||||||
|
import { authRateLimit, readLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
export const registerUserRouter = async (server: FastifyZodProvider) => {
|
export const registerUserRouter = async (server: FastifyZodProvider) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: "/",
|
url: "/",
|
||||||
@@ -25,4 +29,29 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
|
|||||||
return { user };
|
return { user };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:userId/unlock",
|
||||||
|
config: {
|
||||||
|
rateLimit: authRateLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
querystring: z.object({
|
||||||
|
token: z.string().trim()
|
||||||
|
}),
|
||||||
|
params: z.object({
|
||||||
|
userId: z.string()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
handler: async (req, res) => {
|
||||||
|
try {
|
||||||
|
await server.services.user.unlockUser(req.params.userId, req.query.token);
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(`User unlock failed for ${req.params.userId}`);
|
||||||
|
logger.error(err);
|
||||||
|
}
|
||||||
|
return res.redirect(`${appCfg.SITE_URL}/login`);
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import jwt from "jsonwebtoken";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { writeLimit } from "@app/server/config/rateLimiter";
|
import { mfaRateLimit } from "@app/server/config/rateLimiter";
|
||||||
import { AuthModeMfaJwtTokenPayload, AuthTokenType } from "@app/services/auth/auth-type";
|
import { AuthModeMfaJwtTokenPayload, AuthTokenType } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
export const registerMfaRouter = async (server: FastifyZodProvider) => {
|
export const registerMfaRouter = async (server: FastifyZodProvider) => {
|
||||||
@@ -34,7 +34,7 @@ export const registerMfaRouter = async (server: FastifyZodProvider) => {
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
url: "/mfa/send",
|
url: "/mfa/send",
|
||||||
config: {
|
config: {
|
||||||
rateLimit: writeLimit
|
rateLimit: mfaRateLimit
|
||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
response: {
|
response: {
|
||||||
@@ -53,7 +53,7 @@ export const registerMfaRouter = async (server: FastifyZodProvider) => {
|
|||||||
url: "/mfa/verify",
|
url: "/mfa/verify",
|
||||||
method: "POST",
|
method: "POST",
|
||||||
config: {
|
config: {
|
||||||
rateLimit: writeLimit
|
rateLimit: mfaRateLimit
|
||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
body: z.object({
|
body: z.object({
|
||||||
|
|||||||
@@ -13,8 +13,9 @@ import { TCreateTokenForUserDTO, TIssueAuthTokenDTO, TokenType, TValidateTokenFo
|
|||||||
|
|
||||||
type TAuthTokenServiceFactoryDep = {
|
type TAuthTokenServiceFactoryDep = {
|
||||||
tokenDAL: TTokenDALFactory;
|
tokenDAL: TTokenDALFactory;
|
||||||
userDAL: Pick<TUserDALFactory, "findById">;
|
userDAL: Pick<TUserDALFactory, "findById" | "transaction">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TAuthTokenServiceFactory = ReturnType<typeof tokenServiceFactory>;
|
export type TAuthTokenServiceFactory = ReturnType<typeof tokenServiceFactory>;
|
||||||
|
|
||||||
export const getTokenConfig = (tokenType: TokenType) => {
|
export const getTokenConfig = (tokenType: TokenType) => {
|
||||||
@@ -53,6 +54,11 @@ export const getTokenConfig = (tokenType: TokenType) => {
|
|||||||
const expiresAt = new Date(new Date().getTime() + 86400000);
|
const expiresAt = new Date(new Date().getTime() + 86400000);
|
||||||
return { token, expiresAt };
|
return { token, expiresAt };
|
||||||
}
|
}
|
||||||
|
case TokenType.TOKEN_USER_UNLOCK: {
|
||||||
|
const token = crypto.randomBytes(16).toString("hex");
|
||||||
|
const expiresAt = new Date(new Date().getTime() + 259200000);
|
||||||
|
return { token, expiresAt };
|
||||||
|
}
|
||||||
default: {
|
default: {
|
||||||
const token = crypto.randomBytes(16).toString("hex");
|
const token = crypto.randomBytes(16).toString("hex");
|
||||||
const expiresAt = new Date();
|
const expiresAt = new Date();
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ export enum TokenType {
|
|||||||
TOKEN_EMAIL_VERIFICATION = "emailVerification", // unverified -> verified
|
TOKEN_EMAIL_VERIFICATION = "emailVerification", // unverified -> verified
|
||||||
TOKEN_EMAIL_MFA = "emailMfa",
|
TOKEN_EMAIL_MFA = "emailMfa",
|
||||||
TOKEN_EMAIL_ORG_INVITATION = "organizationInvitation",
|
TOKEN_EMAIL_ORG_INVITATION = "organizationInvitation",
|
||||||
TOKEN_EMAIL_PASSWORD_RESET = "passwordReset"
|
TOKEN_EMAIL_PASSWORD_RESET = "passwordReset",
|
||||||
|
TOKEN_USER_UNLOCK = "userUnlock"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TCreateTokenForUserDTO = {
|
export type TCreateTokenForUserDTO = {
|
||||||
|
|||||||
@@ -44,3 +44,27 @@ export const validateSignUpAuthorization = (token: string, userId: string, valid
|
|||||||
if (decodedToken.authTokenType !== AuthTokenType.SIGNUP_TOKEN) throw new UnauthorizedError();
|
if (decodedToken.authTokenType !== AuthTokenType.SIGNUP_TOKEN) throw new UnauthorizedError();
|
||||||
if (decodedToken.userId !== userId) throw new UnauthorizedError();
|
if (decodedToken.userId !== userId) throw new UnauthorizedError();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const enforceUserLockStatus = (isLocked: boolean, temporaryLockDateEnd?: Date | null) => {
|
||||||
|
if (isLocked) {
|
||||||
|
throw new UnauthorizedError({
|
||||||
|
name: "User Locked",
|
||||||
|
message:
|
||||||
|
"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."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (temporaryLockDateEnd) {
|
||||||
|
const timeDiff = new Date().getTime() - temporaryLockDateEnd.getTime();
|
||||||
|
if (timeDiff < 0) {
|
||||||
|
const secondsDiff = (-1 * timeDiff) / 1000;
|
||||||
|
const timeDisplay =
|
||||||
|
secondsDiff > 60 ? `${Math.ceil(secondsDiff / 60)} minutes` : `${Math.ceil(secondsDiff)} seconds`;
|
||||||
|
|
||||||
|
throw new UnauthorizedError({
|
||||||
|
name: "User Locked",
|
||||||
|
message: `User is temporary locked due to multiple failed login attempts. Try again after ${timeDisplay}. You can also reset your password now to proceed.`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -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,7 @@ 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 { validateProviderAuthToken } from "./auth-fns";
|
import { enforceUserLockStatus, validateProviderAuthToken } from "./auth-fns";
|
||||||
import {
|
import {
|
||||||
TLoginClientProofDTO,
|
TLoginClientProofDTO,
|
||||||
TLoginGenServerPublicKeyDTO,
|
TLoginGenServerPublicKeyDTO,
|
||||||
@@ -212,6 +212,9 @@ 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);
|
||||||
|
enforceUserLockStatus(Boolean(user.isLocked), user.temporaryLockDateEnd);
|
||||||
|
|
||||||
const mfaToken = jwt.sign(
|
const mfaToken = jwt.sign(
|
||||||
{
|
{
|
||||||
authMethod,
|
authMethod,
|
||||||
@@ -300,28 +303,111 @@ 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(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.updateById(userId, { $incr: { consecutiveFailedMfaAttempts: 1 } }, 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
|
||||||
* */
|
* */
|
||||||
const verifyMfaToken = async ({ userId, mfaToken, mfaJwtToken, ip, userAgent, orgId }: TVerifyMfaTokenDTO) => {
|
const verifyMfaToken = async ({ userId, mfaToken, mfaJwtToken, ip, userAgent, orgId }: TVerifyMfaTokenDTO) => {
|
||||||
await tokenService.validateTokenForUser({
|
const appCfg = getConfig();
|
||||||
type: TokenType.TOKEN_EMAIL_MFA,
|
const user = await userDAL.findById(userId);
|
||||||
userId,
|
enforceUserLockStatus(Boolean(user.isLocked), user.temporaryLockDateEnd);
|
||||||
code: mfaToken
|
|
||||||
});
|
try {
|
||||||
|
await tokenService.validateTokenForUser({
|
||||||
|
type: TokenType.TOKEN_EMAIL_MFA,
|
||||||
|
userId,
|
||||||
|
code: mfaToken
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
const updatedUser = await processFailedMfaAttempt(userId);
|
||||||
|
if (updatedUser.isLocked) {
|
||||||
|
if (updatedUser.email) {
|
||||||
|
const unlockToken = await tokenService.createTokenForUser({
|
||||||
|
type: TokenType.TOKEN_USER_UNLOCK,
|
||||||
|
userId: updatedUser.id
|
||||||
|
});
|
||||||
|
|
||||||
|
await smtpService.sendMail({
|
||||||
|
template: SmtpTemplates.UnlockAccount,
|
||||||
|
subjectLine: "Unlock your Infisical account",
|
||||||
|
recipients: [updatedUser.email],
|
||||||
|
substitutions: {
|
||||||
|
token: unlockToken,
|
||||||
|
callback_url: `${appCfg.SITE_URL}/api/v1/user/${updatedUser.id}/unlock`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
const decodedToken = jwt.verify(mfaJwtToken, getConfig().AUTH_SECRET) as AuthModeMfaJwtTokenPayload;
|
const decodedToken = jwt.verify(mfaJwtToken, getConfig().AUTH_SECRET) as AuthModeMfaJwtTokenPayload;
|
||||||
|
|
||||||
const userEnc = await userDAL.findUserEncKeyByUserId(userId);
|
const userEnc = await userDAL.findUserEncKeyByUserId(userId);
|
||||||
if (!userEnc) throw new Error("Failed to authenticate user");
|
if (!userEnc) throw new Error("Failed to authenticate user");
|
||||||
|
|
||||||
|
// reset lock states
|
||||||
|
await userDAL.updateById(userId, {
|
||||||
|
consecutiveFailedMfaAttempts: 0,
|
||||||
|
temporaryLockDateEnd: null
|
||||||
|
});
|
||||||
|
|
||||||
const token = await generateUserTokens({
|
const token = await generateUserTokens({
|
||||||
user: {
|
user: {
|
||||||
...userEnc,
|
...userEnc,
|
||||||
|
|||||||
@@ -174,6 +174,12 @@ export const authPaswordServiceFactory = ({
|
|||||||
salt,
|
salt,
|
||||||
verifier
|
verifier
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await userDAL.updateById(userId, {
|
||||||
|
isLocked: false,
|
||||||
|
temporaryLockDateEnd: null,
|
||||||
|
consecutiveFailedMfaAttempts: 0
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export enum SmtpTemplates {
|
|||||||
EmailVerification = "emailVerification.handlebars",
|
EmailVerification = "emailVerification.handlebars",
|
||||||
SecretReminder = "secretReminder.handlebars",
|
SecretReminder = "secretReminder.handlebars",
|
||||||
EmailMfa = "emailMfa.handlebars",
|
EmailMfa = "emailMfa.handlebars",
|
||||||
|
UnlockAccount = "unlockAccount.handlebars",
|
||||||
AccessApprovalRequest = "accessApprovalRequest.handlebars",
|
AccessApprovalRequest = "accessApprovalRequest.handlebars",
|
||||||
HistoricalSecretList = "historicalSecretLeakIncident.handlebars",
|
HistoricalSecretList = "historicalSecretLeakIncident.handlebars",
|
||||||
NewDeviceJoin = "newDevice.handlebars",
|
NewDeviceJoin = "newDevice.handlebars",
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
||||||
|
<title>Your Infisical account has been locked</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h2>Unlock your Infisical account</h2>
|
||||||
|
<p>Your account has been temporarily locked due to multiple failed login attempts. </h2>
|
||||||
|
<a href="{{callback_url}}?token={{token}}">Unlock your account now</a>
|
||||||
|
<p>If these attempts were not made by you, reset your password immediately.</p>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -207,6 +207,19 @@ export const userServiceFactory = ({
|
|||||||
return userAction;
|
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 {
|
return {
|
||||||
sendEmailVerificationCode,
|
sendEmailVerificationCode,
|
||||||
verifyEmailVerificationCode,
|
verifyEmailVerificationCode,
|
||||||
@@ -216,6 +229,7 @@ export const userServiceFactory = ({
|
|||||||
deleteMe,
|
deleteMe,
|
||||||
getMe,
|
getMe,
|
||||||
createUserAction,
|
createUserAction,
|
||||||
getUserAction
|
getUserAction,
|
||||||
|
unlockUser
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -105,8 +105,18 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err: any) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
if (err.response.data.error === "User Locked") {
|
||||||
|
createNotification({
|
||||||
|
title: err.response.data.error,
|
||||||
|
text: err.response.data.message,
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setLoginError(true);
|
setLoginError(true);
|
||||||
createNotification({
|
createNotification({
|
||||||
text: "Login unsuccessful. Double-check your credentials and try again.",
|
text: "Login unsuccessful. Double-check your credentials and try again.",
|
||||||
|
|||||||
@@ -46,20 +46,7 @@ type Props = {
|
|||||||
callbackPort?: string | null;
|
callbackPort?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface VerifyMfaTokenError {
|
|
||||||
response: {
|
|
||||||
data: {
|
|
||||||
context: {
|
|
||||||
code: string;
|
|
||||||
triesLeft: number;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
status: number;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const MFAStep = ({ email, password, providerAuthToken }: Props) => {
|
export const MFAStep = ({ email, password, providerAuthToken }: Props) => {
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [isLoadingResend, setIsLoadingResend] = useState(false);
|
const [isLoadingResend, setIsLoadingResend] = useState(false);
|
||||||
@@ -178,20 +165,31 @@ export const MFAStep = ({ email, password, providerAuthToken }: Props) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err: any) {
|
||||||
const error = err as VerifyMfaTokenError;
|
if (err.response.data.error === "User Locked") {
|
||||||
|
createNotification({
|
||||||
|
title: err.response.data.error,
|
||||||
|
text: err.response.data.message,
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
createNotification({
|
createNotification({
|
||||||
text: "Failed to log in",
|
text: "Failed to log in",
|
||||||
type: "error"
|
type: "error"
|
||||||
});
|
});
|
||||||
|
|
||||||
if (error?.response?.status === 500) {
|
if (triesLeft) {
|
||||||
window.location.reload();
|
setTriesLeft((left) => {
|
||||||
} else if (error?.response?.data?.context?.triesLeft) {
|
if (triesLeft === 1) {
|
||||||
setTriesLeft(error?.response?.data?.context?.triesLeft);
|
router.push("/");
|
||||||
if (error.response.data.context.triesLeft === 0) {
|
}
|
||||||
window.location.reload();
|
return (left as number) - 1;
|
||||||
}
|
});
|
||||||
|
} else {
|
||||||
|
setTriesLeft(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
|
|||||||
Reference in New Issue
Block a user