mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-08 08:27:53 +00:00
misc: addressed comments 1
This commit is contained in:
@@ -525,7 +525,8 @@ export const registerRoutes = async (
|
|||||||
tokenService,
|
tokenService,
|
||||||
smtpService,
|
smtpService,
|
||||||
authDAL,
|
authDAL,
|
||||||
userDAL
|
userDAL,
|
||||||
|
totpConfigDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
const projectBotService = projectBotServiceFactory({ permissionService, projectBotDAL, projectDAL });
|
const projectBotService = projectBotServiceFactory({ permissionService, projectBotDAL, projectDAL });
|
||||||
|
|||||||
@@ -196,7 +196,7 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
|
|||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
url: "/me/totp",
|
url: "/me/totp",
|
||||||
config: {
|
config: {
|
||||||
rateLimit: readLimit
|
rateLimit: writeLimit
|
||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
@@ -254,4 +254,18 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/me/totp/recovery-codes",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
return server.services.totp.createUserTotpRecoveryCodes({
|
||||||
|
userId: req.permission.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 { NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { mfaRateLimit } from "@app/server/config/rateLimiter";
|
import { mfaRateLimit } from "@app/server/config/rateLimiter";
|
||||||
import { AuthModeMfaJwtTokenPayload, AuthTokenType, MfaMethod } from "@app/services/auth/auth-type";
|
import { AuthModeMfaJwtTokenPayload, AuthTokenType, MfaMethod } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
@@ -73,7 +73,7 @@ export const registerMfaRouter = async (server: FastifyZodProvider) => {
|
|||||||
isVerified: Boolean(totpConfig)
|
isVerified: Boolean(totpConfig)
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof NotFoundError) {
|
if (error instanceof NotFoundError || error instanceof BadRequestError) {
|
||||||
return { isVerified: false };
|
return { isVerified: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -361,9 +361,9 @@ export const authLoginServiceFactory = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const shouldCheckMfa = selectedOrg.enforceMfa || user.isMfaEnabled;
|
const shouldCheckMfa = selectedOrg.enforceMfa || user.isMfaEnabled;
|
||||||
const orgMfaMethod = selectedOrg.enforceMfa ? selectedOrg.selectedMfaMethod : undefined;
|
const orgMfaMethod = selectedOrg.enforceMfa ? selectedOrg.selectedMfaMethod ?? MfaMethod.EMAIL : undefined;
|
||||||
const userMfaMethod = user.isMfaEnabled ? user.selectedMfaMethod : undefined;
|
const userMfaMethod = user.isMfaEnabled ? user.selectedMfaMethod ?? MfaMethod.EMAIL : undefined;
|
||||||
const mfaMethod = orgMfaMethod ?? userMfaMethod ?? MfaMethod.EMAIL;
|
const mfaMethod = orgMfaMethod ?? userMfaMethod;
|
||||||
|
|
||||||
if (shouldCheckMfa && (!decodedToken.isMfaVerified || decodedToken.mfaMethod !== mfaMethod)) {
|
if (shouldCheckMfa && (!decodedToken.isMfaVerified || decodedToken.mfaMethod !== mfaMethod)) {
|
||||||
enforceUserLockStatus(Boolean(user.isLocked), user.temporaryLockDateEnd);
|
enforceUserLockStatus(Boolean(user.isLocked), user.temporaryLockDateEnd);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
|
|||||||
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
|
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
|
||||||
import { TokenType } from "../auth-token/auth-token-types";
|
import { TokenType } from "../auth-token/auth-token-types";
|
||||||
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
||||||
|
import { TTotpConfigDALFactory } from "../totp/totp-config-dal";
|
||||||
import { TUserDALFactory } from "../user/user-dal";
|
import { TUserDALFactory } from "../user/user-dal";
|
||||||
import { TAuthDALFactory } from "./auth-dal";
|
import { TAuthDALFactory } from "./auth-dal";
|
||||||
import { TChangePasswordDTO, TCreateBackupPrivateKeyDTO, TResetPasswordViaBackupKeyDTO } from "./auth-password-type";
|
import { TChangePasswordDTO, TCreateBackupPrivateKeyDTO, TResetPasswordViaBackupKeyDTO } from "./auth-password-type";
|
||||||
@@ -18,6 +19,7 @@ type TAuthPasswordServiceFactoryDep = {
|
|||||||
userDAL: TUserDALFactory;
|
userDAL: TUserDALFactory;
|
||||||
tokenService: TAuthTokenServiceFactory;
|
tokenService: TAuthTokenServiceFactory;
|
||||||
smtpService: TSmtpService;
|
smtpService: TSmtpService;
|
||||||
|
totpConfigDAL: Pick<TTotpConfigDALFactory, "delete">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TAuthPasswordFactory = ReturnType<typeof authPaswordServiceFactory>;
|
export type TAuthPasswordFactory = ReturnType<typeof authPaswordServiceFactory>;
|
||||||
@@ -25,7 +27,8 @@ export const authPaswordServiceFactory = ({
|
|||||||
authDAL,
|
authDAL,
|
||||||
userDAL,
|
userDAL,
|
||||||
tokenService,
|
tokenService,
|
||||||
smtpService
|
smtpService,
|
||||||
|
totpConfigDAL
|
||||||
}: TAuthPasswordServiceFactoryDep) => {
|
}: TAuthPasswordServiceFactoryDep) => {
|
||||||
/*
|
/*
|
||||||
* Pre setup for pass change with srp protocol
|
* Pre setup for pass change with srp protocol
|
||||||
@@ -185,6 +188,12 @@ export const authPaswordServiceFactory = ({
|
|||||||
temporaryLockDateEnd: null,
|
temporaryLockDateEnd: null,
|
||||||
consecutiveFailedMfaAttempts: 0
|
consecutiveFailedMfaAttempts: 0
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* we reset the mobile authenticator configs of the user
|
||||||
|
because we want this to be one of the recovery modes from account lockout */
|
||||||
|
await totpConfigDAL.delete({
|
||||||
|
userId
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { TKmsServiceFactory } from "../kms/kms-service";
|
|||||||
import { TUserDALFactory } from "../user/user-dal";
|
import { TUserDALFactory } from "../user/user-dal";
|
||||||
import { TTotpConfigDALFactory } from "./totp-config-dal";
|
import { TTotpConfigDALFactory } from "./totp-config-dal";
|
||||||
import {
|
import {
|
||||||
|
TCreateUserTotpRecoveryCodesDTO,
|
||||||
TDeleteUserTotpConfigDTO,
|
TDeleteUserTotpConfigDTO,
|
||||||
TGetUserTotpConfigDTO,
|
TGetUserTotpConfigDTO,
|
||||||
TRegisterUserTotpDTO,
|
TRegisterUserTotpDTO,
|
||||||
@@ -27,8 +28,7 @@ export type TTotpServiceFactory = ReturnType<typeof totpServiceFactory>;
|
|||||||
export const totpServiceFactory = ({ totpConfigDAL, kmsService, userDAL }: TTotpServiceFactoryDep) => {
|
export const totpServiceFactory = ({ totpConfigDAL, kmsService, userDAL }: TTotpServiceFactoryDep) => {
|
||||||
const getUserTotpConfig = async ({ userId }: TGetUserTotpConfigDTO) => {
|
const getUserTotpConfig = async ({ userId }: TGetUserTotpConfigDTO) => {
|
||||||
const totpConfig = await totpConfigDAL.findOne({
|
const totpConfig = await totpConfigDAL.findOne({
|
||||||
userId,
|
userId
|
||||||
isVerified: true
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!totpConfig) {
|
if (!totpConfig) {
|
||||||
@@ -37,6 +37,12 @@ export const totpServiceFactory = ({ totpConfigDAL, kmsService, userDAL }: TTotp
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!totpConfig.isVerified) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "TOTP configuration has not been verified"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const decryptWithRoot = kmsService.decryptWithRootKey();
|
const decryptWithRoot = kmsService.decryptWithRootKey();
|
||||||
const recoveryCodes = decryptWithRoot(totpConfig.encryptedRecoveryCodes).toString().split(",");
|
const recoveryCodes = decryptWithRoot(totpConfig.encryptedRecoveryCodes).toString().split(",");
|
||||||
|
|
||||||
@@ -102,8 +108,7 @@ export const totpServiceFactory = ({ totpConfigDAL, kmsService, userDAL }: TTotp
|
|||||||
|
|
||||||
const verifyUserTotpConfig = async ({ userId, totp }: TVerifyUserTotpConfigDTO) => {
|
const verifyUserTotpConfig = async ({ userId, totp }: TVerifyUserTotpConfigDTO) => {
|
||||||
const totpConfig = await totpConfigDAL.findOne({
|
const totpConfig = await totpConfigDAL.findOne({
|
||||||
userId,
|
userId
|
||||||
isVerified: false
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!totpConfig) {
|
if (!totpConfig) {
|
||||||
@@ -112,6 +117,12 @@ export const totpServiceFactory = ({ totpConfigDAL, kmsService, userDAL }: TTotp
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (totpConfig.isVerified) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "TOTP configuration has already been verified"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const decryptWithRoot = kmsService.decryptWithRootKey();
|
const decryptWithRoot = kmsService.decryptWithRootKey();
|
||||||
const secret = decryptWithRoot(totpConfig.encryptedSecret).toString();
|
const secret = decryptWithRoot(totpConfig.encryptedSecret).toString();
|
||||||
const isValid = authenticator.verify({
|
const isValid = authenticator.verify({
|
||||||
@@ -132,8 +143,7 @@ export const totpServiceFactory = ({ totpConfigDAL, kmsService, userDAL }: TTotp
|
|||||||
|
|
||||||
const verifyUserTotp = async ({ userId, totp }: TVerifyUserTotpDTO) => {
|
const verifyUserTotp = async ({ userId, totp }: TVerifyUserTotpDTO) => {
|
||||||
const totpConfig = await totpConfigDAL.findOne({
|
const totpConfig = await totpConfigDAL.findOne({
|
||||||
userId,
|
userId
|
||||||
isVerified: true
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!totpConfig) {
|
if (!totpConfig) {
|
||||||
@@ -142,6 +152,12 @@ export const totpServiceFactory = ({ totpConfigDAL, kmsService, userDAL }: TTotp
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!totpConfig.isVerified) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "TOTP configuration has not been verified"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const decryptWithRoot = kmsService.decryptWithRootKey();
|
const decryptWithRoot = kmsService.decryptWithRootKey();
|
||||||
const secret = decryptWithRoot(totpConfig.encryptedSecret).toString();
|
const secret = decryptWithRoot(totpConfig.encryptedSecret).toString();
|
||||||
const isValid = authenticator.verify({
|
const isValid = authenticator.verify({
|
||||||
@@ -158,8 +174,7 @@ export const totpServiceFactory = ({ totpConfigDAL, kmsService, userDAL }: TTotp
|
|||||||
|
|
||||||
const verifyWithUserRecoveryCode = async ({ userId, recoveryCode }: TVerifyWithUserRecoveryCodeDTO) => {
|
const verifyWithUserRecoveryCode = async ({ userId, recoveryCode }: TVerifyWithUserRecoveryCodeDTO) => {
|
||||||
const totpConfig = await totpConfigDAL.findOne({
|
const totpConfig = await totpConfigDAL.findOne({
|
||||||
userId,
|
userId
|
||||||
isVerified: true
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!totpConfig) {
|
if (!totpConfig) {
|
||||||
@@ -168,6 +183,12 @@ export const totpServiceFactory = ({ totpConfigDAL, kmsService, userDAL }: TTotp
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!totpConfig.isVerified) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "TOTP configuration has not been verified"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const decryptWithRoot = kmsService.decryptWithRootKey();
|
const decryptWithRoot = kmsService.decryptWithRootKey();
|
||||||
const encryptWithRoot = kmsService.encryptWithRootKey();
|
const encryptWithRoot = kmsService.encryptWithRootKey();
|
||||||
|
|
||||||
@@ -188,8 +209,7 @@ export const totpServiceFactory = ({ totpConfigDAL, kmsService, userDAL }: TTotp
|
|||||||
|
|
||||||
const deleteUserTotpConfig = async ({ userId }: TDeleteUserTotpConfigDTO) => {
|
const deleteUserTotpConfig = async ({ userId }: TDeleteUserTotpConfigDTO) => {
|
||||||
const totpConfig = await totpConfigDAL.findOne({
|
const totpConfig = await totpConfigDAL.findOne({
|
||||||
userId,
|
userId
|
||||||
isVerified: true
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!totpConfig) {
|
if (!totpConfig) {
|
||||||
@@ -201,12 +221,51 @@ export const totpServiceFactory = ({ totpConfigDAL, kmsService, userDAL }: TTotp
|
|||||||
await totpConfigDAL.deleteById(totpConfig.id);
|
await totpConfigDAL.deleteById(totpConfig.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const createUserTotpRecoveryCodes = async ({ userId }: TCreateUserTotpRecoveryCodesDTO) => {
|
||||||
|
const decryptWithRoot = kmsService.decryptWithRootKey();
|
||||||
|
const encryptWithRoot = kmsService.encryptWithRootKey();
|
||||||
|
|
||||||
|
return totpConfigDAL.transaction(async (tx) => {
|
||||||
|
const totpConfig = await totpConfigDAL.findOne(
|
||||||
|
{
|
||||||
|
userId,
|
||||||
|
isVerified: true
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!totpConfig) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: "Valid TOTP configuration not found"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const recoveryCodes = decryptWithRoot(totpConfig.encryptedRecoveryCodes).toString().split(",");
|
||||||
|
if (recoveryCodes.length >= 10) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Cannot have more than 10 recovery codes at a time"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const toGenerateCount = 10 - recoveryCodes.length;
|
||||||
|
const newRecoveryCodes = Array.from({ length: toGenerateCount }).map(() =>
|
||||||
|
String(crypto.randomInt(10 ** 7, 10 ** 8 - 1))
|
||||||
|
);
|
||||||
|
const encryptedRecoveryCodes = encryptWithRoot(Buffer.from([...recoveryCodes, ...newRecoveryCodes].join(",")));
|
||||||
|
|
||||||
|
await totpConfigDAL.updateById(totpConfig.id, {
|
||||||
|
encryptedRecoveryCodes
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
registerUserTotp,
|
registerUserTotp,
|
||||||
verifyUserTotpConfig,
|
verifyUserTotpConfig,
|
||||||
getUserTotpConfig,
|
getUserTotpConfig,
|
||||||
verifyUserTotp,
|
verifyUserTotp,
|
||||||
verifyWithUserRecoveryCode,
|
verifyWithUserRecoveryCode,
|
||||||
deleteUserTotpConfig
|
deleteUserTotpConfig,
|
||||||
|
createUserTotpRecoveryCodes
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -24,3 +24,7 @@ export type TVerifyWithUserRecoveryCodeDTO = {
|
|||||||
export type TDeleteUserTotpConfigDTO = {
|
export type TDeleteUserTotpConfigDTO = {
|
||||||
userId: string;
|
userId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TCreateUserTotpRecoveryCodesDTO = {
|
||||||
|
userId: string;
|
||||||
|
};
|
||||||
|
|||||||
@@ -13,11 +13,13 @@ type Props = {
|
|||||||
|
|
||||||
const TotpRegistration = ({ onComplete }: Props) => {
|
const TotpRegistration = ({ onComplete }: Props) => {
|
||||||
const { data: registration, isLoading } = useGetUserTotpRegistration();
|
const { data: registration, isLoading } = useGetUserTotpRegistration();
|
||||||
const { mutateAsync: verifyUserTotp } = useVerifyUserTotpRegistration();
|
const { mutateAsync: verifyUserTotp, isLoading: isVerifyLoading } =
|
||||||
|
useVerifyUserTotpRegistration();
|
||||||
const [qrCodeUrl, setQrCodeUrl] = useState("");
|
const [qrCodeUrl, setQrCodeUrl] = useState("");
|
||||||
const [totp, setTotp] = useState("");
|
const [totp, setTotp] = useState("");
|
||||||
|
|
||||||
const handleTotpVerify = async () => {
|
const handleTotpVerify = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
await verifyUserTotp({
|
await verifyUserTotp({
|
||||||
totp
|
totp
|
||||||
});
|
});
|
||||||
@@ -54,6 +56,7 @@ const TotpRegistration = ({ onComplete }: Props) => {
|
|||||||
<div className="mb-10 flex items-center justify-center">
|
<div className="mb-10 flex items-center justify-center">
|
||||||
<img src={qrCodeUrl} alt="registration-qr" />
|
<img src={qrCodeUrl} alt="registration-qr" />
|
||||||
</div>
|
</div>
|
||||||
|
<form onSubmit={handleTotpVerify}>
|
||||||
<div className="mb-4 text-center">Enter the resulting verification code</div>
|
<div className="mb-4 text-center">Enter the resulting verification code</div>
|
||||||
<div className="mb-4 flex flex-row gap-2">
|
<div className="mb-4 flex flex-row gap-2">
|
||||||
<Input
|
<Input
|
||||||
@@ -61,8 +64,11 @@ const TotpRegistration = ({ onComplete }: Props) => {
|
|||||||
value={totp}
|
value={totp}
|
||||||
placeholder="Verification code"
|
placeholder="Verification code"
|
||||||
/>
|
/>
|
||||||
<Button onClick={handleTotpVerify}>Enable MFA</Button>
|
<Button isLoading={isVerifyLoading} type="submit">
|
||||||
|
Enable MFA
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -140,3 +140,17 @@ export const useDeleteUserTotpConfiguration = () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useCreateNewTotpRecoveryCodes = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async () => {
|
||||||
|
await apiRequest.post("/api/v1/user/me/totp/recovery-codes");
|
||||||
|
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries(userKeys.totpConfiguration);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -480,7 +480,7 @@ export const useGetUserTotpConfiguration = () => {
|
|||||||
|
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof AxiosError && error.response?.data?.statusCode === 404) {
|
if (error instanceof AxiosError && [404, 400].includes(error.response?.data?.statusCode)) {
|
||||||
return {
|
return {
|
||||||
isVerified: false,
|
isVerified: false,
|
||||||
recoveryCodes: []
|
recoveryCodes: []
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import ReactCodeInput from "react-code-input";
|
import ReactCodeInput from "react-code-input";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
@@ -62,7 +62,9 @@ export const Mfa = ({ successCallback, closeMfa, hideLogo, email, method }: Prop
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const verifyMfa = async () => {
|
const verifyMfa = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
const { token } = await verifyMfaToken({
|
const { token } = await verifyMfaToken({
|
||||||
@@ -110,6 +112,10 @@ export const Mfa = ({ successCallback, closeMfa, hideLogo, email, method }: Prop
|
|||||||
|
|
||||||
if (shouldShowTotpRegistration) {
|
if (shouldShowTotpRegistration) {
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
<div className="mb-6 text-center text-lg font-bold text-white">
|
||||||
|
Your organization requires mobile authenticator to be configured.
|
||||||
|
</div>
|
||||||
<div className="mx-auto w-max pb-4 pt-4 md:mb-16 md:px-8">
|
<div className="mx-auto w-max pb-4 pt-4 md:mb-16 md:px-8">
|
||||||
<TotpRegistration
|
<TotpRegistration
|
||||||
onComplete={async () => {
|
onComplete={async () => {
|
||||||
@@ -118,6 +124,7 @@ export const Mfa = ({ successCallback, closeMfa, hideLogo, email, method }: Prop
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,6 +154,7 @@ export const Mfa = ({ successCallback, closeMfa, hideLogo, email, method }: Prop
|
|||||||
</p>
|
</p>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
<form onSubmit={verifyMfa}>
|
||||||
<div className="mx-auto hidden w-max min-w-[20rem] md:block">
|
<div className="mx-auto hidden w-max min-w-[20rem] md:block">
|
||||||
{method === MfaMethod.EMAIL && (
|
{method === MfaMethod.EMAIL && (
|
||||||
<ReactCodeInput
|
<ReactCodeInput
|
||||||
@@ -171,8 +179,8 @@ export const Mfa = ({ successCallback, closeMfa, hideLogo, email, method }: Prop
|
|||||||
<div className="mx-auto mt-2 flex w-1/4 min-w-[20rem] max-w-xs flex-col items-center justify-center text-center text-sm md:max-w-md md:text-left lg:w-[19%]">
|
<div className="mx-auto mt-2 flex w-1/4 min-w-[20rem] max-w-xs flex-col items-center justify-center text-center text-sm md:max-w-md md:text-left lg:w-[19%]">
|
||||||
<div className="text-l w-full py-1 text-lg">
|
<div className="text-l w-full py-1 text-lg">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => verifyMfa()}
|
|
||||||
size="sm"
|
size="sm"
|
||||||
|
type="submit"
|
||||||
isFullWidth
|
isFullWidth
|
||||||
className="h-14"
|
className="h-14"
|
||||||
colorSchema="primary"
|
colorSchema="primary"
|
||||||
@@ -183,6 +191,16 @@ export const Mfa = ({ successCallback, closeMfa, hideLogo, email, method }: Prop
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</form>
|
||||||
|
{method === MfaMethod.TOTP && (
|
||||||
|
<div className="mt-2 flex flex-row justify-center text-sm text-bunker-400 ">
|
||||||
|
<Link href="/verify-email">
|
||||||
|
<span className="cursor-pointer duration-200 hover:text-bunker-200 hover:underline hover:decoration-primary-700 hover:underline-offset-4">
|
||||||
|
No access to both codes? Reset your account
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{method === MfaMethod.EMAIL && (
|
{method === MfaMethod.EMAIL && (
|
||||||
<div className="mx-auto flex max-h-24 w-full max-w-md flex-col items-center justify-center pt-2">
|
<div className="mx-auto flex max-h-24 w-full max-w-md flex-col items-center justify-center pt-2">
|
||||||
<div className="flex flex-row items-baseline gap-1 text-sm">
|
<div className="flex flex-row items-baseline gap-1 text-sm">
|
||||||
|
|||||||
+2
-2
@@ -58,7 +58,7 @@ export const OrgGenericAuthSection = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
createNotification({
|
createNotification({
|
||||||
text: "Successfully updated preferred MFA method",
|
text: "Successfully updated selected MFA method",
|
||||||
type: "success"
|
type: "success"
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -90,7 +90,7 @@ export const OrgGenericAuthSection = () => {
|
|||||||
Enforce members to authenticate with MFA in order to access the organization
|
Enforce members to authenticate with MFA in order to access the organization
|
||||||
</p>
|
</p>
|
||||||
{currentOrg?.enforceMfa && (
|
{currentOrg?.enforceMfa && (
|
||||||
<FormControl label="Required 2FA method" className="mt-3">
|
<FormControl label="Selected 2FA method" className="mt-3">
|
||||||
<Select
|
<Select
|
||||||
className="min-w-[20rem] border border-mineshaft-500"
|
className="min-w-[20rem] border border-mineshaft-500"
|
||||||
onValueChange={handleUpdateSelectedMfa}
|
onValueChange={handleUpdateSelectedMfa}
|
||||||
|
|||||||
@@ -16,7 +16,10 @@ import { useToggle } from "@app/hooks";
|
|||||||
import { useGetUser, userKeys, useUpdateUserMfa } from "@app/hooks/api";
|
import { useGetUser, userKeys, useUpdateUserMfa } from "@app/hooks/api";
|
||||||
import { MfaMethod } from "@app/hooks/api/auth/types";
|
import { MfaMethod } from "@app/hooks/api/auth/types";
|
||||||
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
|
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
|
||||||
import { useDeleteUserTotpConfiguration } from "@app/hooks/api/users/mutation";
|
import {
|
||||||
|
useCreateNewTotpRecoveryCodes,
|
||||||
|
useDeleteUserTotpConfiguration
|
||||||
|
} from "@app/hooks/api/users/mutation";
|
||||||
import { useGetUserTotpConfiguration } from "@app/hooks/api/users/queries";
|
import { useGetUserTotpConfiguration } from "@app/hooks/api/users/queries";
|
||||||
import { AuthMethod } from "@app/hooks/api/users/types";
|
import { AuthMethod } from "@app/hooks/api/users/types";
|
||||||
import { usePopUp } from "@app/hooks/usePopUp";
|
import { usePopUp } from "@app/hooks/usePopUp";
|
||||||
@@ -33,6 +36,7 @@ export const MFASection = () => {
|
|||||||
const { data: totpConfiguration, isLoading: isTotpConfigurationLoading } =
|
const { data: totpConfiguration, isLoading: isTotpConfigurationLoading } =
|
||||||
useGetUserTotpConfiguration();
|
useGetUserTotpConfiguration();
|
||||||
const { mutateAsync: deleteTotpConfiguration } = useDeleteUserTotpConfiguration();
|
const { mutateAsync: deleteTotpConfiguration } = useDeleteUserTotpConfiguration();
|
||||||
|
const { mutateAsync: createTotpRecoveryCodes } = useCreateNewTotpRecoveryCodes();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { data: serverDetails } = useFetchServerStatus();
|
const { data: serverDetails } = useFetchServerStatus();
|
||||||
|
|
||||||
@@ -58,6 +62,26 @@ export const MFASection = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleGenerateMoreRecoveryCodes = async () => {
|
||||||
|
try {
|
||||||
|
await createTotpRecoveryCodes();
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: "Successfully generated new recovery codes",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
const error = err as any;
|
||||||
|
const text = error?.response?.data?.message ?? "Failed to generate new recovery codes";
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text,
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const updateSelectedMfa = async (mfaMethod: MfaMethod) => {
|
const updateSelectedMfa = async (mfaMethod: MfaMethod) => {
|
||||||
try {
|
try {
|
||||||
if (!user) return;
|
if (!user) return;
|
||||||
@@ -67,12 +91,12 @@ export const MFASection = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
createNotification({
|
createNotification({
|
||||||
text: "Successfully updated preferred 2FA method",
|
text: "Successfully updated selected 2FA method",
|
||||||
type: "success"
|
type: "success"
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
createNotification({
|
createNotification({
|
||||||
text: "Something went wrong while updating preferred 2FA method.",
|
text: "Something went wrong while updating selected 2FA method.",
|
||||||
type: "error"
|
type: "error"
|
||||||
});
|
});
|
||||||
console.error(err);
|
console.error(err);
|
||||||
@@ -114,7 +138,7 @@ export const MFASection = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="mb-6 max-w-6xl rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
<div className="mb-6 max-w-6xl rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
<p className="mb-8 text-xl font-semibold text-mineshaft-100">Two-factor Authentication</p>
|
<p className="mb-4 text-xl font-semibold text-mineshaft-100">Two-factor Authentication</p>
|
||||||
{user && (
|
{user && (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
className="data-[state=checked]:bg-primary"
|
className="data-[state=checked]:bg-primary"
|
||||||
@@ -132,7 +156,7 @@ export const MFASection = () => {
|
|||||||
</Checkbox>
|
</Checkbox>
|
||||||
)}
|
)}
|
||||||
{user?.isMfaEnabled && (
|
{user?.isMfaEnabled && (
|
||||||
<FormControl label="Preferred 2FA method" className="mt-3">
|
<FormControl label="Selected 2FA method" className="mt-3">
|
||||||
<Select
|
<Select
|
||||||
className="min-w-[20rem] border border-mineshaft-500"
|
className="min-w-[20rem] border border-mineshaft-500"
|
||||||
onValueChange={updateSelectedMfa}
|
onValueChange={updateSelectedMfa}
|
||||||
@@ -147,7 +171,7 @@ export const MFASection = () => {
|
|||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
<div className="mt-10 text-lg font-semibold text-mineshaft-100">Mobile Authenticator</div>
|
<div className="mt-8 text-lg font-semibold text-mineshaft-100">Mobile Authenticator</div>
|
||||||
{isTotpConfigurationLoading ? (
|
{isTotpConfigurationLoading ? (
|
||||||
<ContentLoader />
|
<ContentLoader />
|
||||||
) : (
|
) : (
|
||||||
@@ -158,6 +182,9 @@ export const MFASection = () => {
|
|||||||
<Button colorSchema="secondary" onClick={setShouldShowRecoveryCodes.toggle}>
|
<Button colorSchema="secondary" onClick={setShouldShowRecoveryCodes.toggle}>
|
||||||
{shouldShowRecoveryCodes ? "Hide recovery codes" : "Show recovery codes"}
|
{shouldShowRecoveryCodes ? "Hide recovery codes" : "Show recovery codes"}
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button colorSchema="secondary" onClick={handleGenerateMoreRecoveryCodes}>
|
||||||
|
Generate more codes
|
||||||
|
</Button>
|
||||||
<Button colorSchema="danger" onClick={() => handlePopUpOpen("deleteTotpConfig")}>
|
<Button colorSchema="danger" onClick={() => handlePopUpOpen("deleteTotpConfig")}>
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
@@ -171,6 +198,11 @@ export const MFASection = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="text-sm text-gray-400">
|
||||||
|
For added security, you can configure a mobile authenticator and set it as your
|
||||||
|
selected 2FA method.
|
||||||
|
</div>
|
||||||
<div className="mt-6 flex min-w-full justify-center">
|
<div className="mt-6 flex min-w-full justify-center">
|
||||||
<TotpRegistration
|
<TotpRegistration
|
||||||
onComplete={async () => {
|
onComplete={async () => {
|
||||||
@@ -178,6 +210,7 @@ export const MFASection = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -188,7 +221,8 @@ export const MFASection = () => {
|
|||||||
/>
|
/>
|
||||||
<DeleteActionModal
|
<DeleteActionModal
|
||||||
isOpen={popUp.deleteTotpConfig.isOpen}
|
isOpen={popUp.deleteTotpConfig.isOpen}
|
||||||
title="Are you sure want to delete the mobile authenticator? You’ll have to go through the setup process to enable it again."
|
title="Are you sure want to delete the configured authenticator?"
|
||||||
|
subTitle="This action is irreversible. You’ll have to go through the setup process to enable it again."
|
||||||
onChange={(isOpen) => handlePopUpToggle("deleteTotpConfig", isOpen)}
|
onChange={(isOpen) => handlePopUpToggle("deleteTotpConfig", isOpen)}
|
||||||
deleteKey="confirm"
|
deleteKey="confirm"
|
||||||
onDeleteApproved={handleTotpDeletion}
|
onDeleteApproved={handleTotpDeletion}
|
||||||
|
|||||||
Reference in New Issue
Block a user