diff --git a/backend/src/services/totp/totp-fns.ts b/backend/src/services/totp/totp-fns.ts new file mode 100644 index 0000000000..9e9aae52c5 --- /dev/null +++ b/backend/src/services/totp/totp-fns.ts @@ -0,0 +1,3 @@ +import crypto from "node:crypto"; + +export const generateRecoveryCode = () => String(crypto.randomInt(10 ** 7, 10 ** 8 - 1)); diff --git a/backend/src/services/totp/totp-service.ts b/backend/src/services/totp/totp-service.ts index f304f01b8a..591a66ed6a 100644 --- a/backend/src/services/totp/totp-service.ts +++ b/backend/src/services/totp/totp-service.ts @@ -1,5 +1,3 @@ -import crypto from "node:crypto"; - import { authenticator } from "otplib"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; @@ -7,6 +5,7 @@ import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/ import { TKmsServiceFactory } from "../kms/kms-service"; import { TUserDALFactory } from "../user/user-dal"; import { TTotpConfigDALFactory } from "./totp-config-dal"; +import { generateRecoveryCode } from "./totp-fns"; import { TCreateUserTotpRecoveryCodesDTO, TDeleteUserTotpConfigDTO, @@ -25,6 +24,8 @@ type TTotpServiceFactoryDep = { export type TTotpServiceFactory = ReturnType; +const MAX_RECOVERY_CODE_LIMIT = 10; + export const totpServiceFactory = ({ totpConfigDAL, kmsService, userDAL }: TTotpServiceFactoryDep) => { const getUserTotpConfig = async ({ userId }: TGetUserTotpConfigDTO) => { const totpConfig = await totpConfigDAL.findOne({ @@ -82,7 +83,7 @@ export const totpServiceFactory = ({ totpConfigDAL, kmsService, userDAL }: TTotp // create new TOTP configuration const secret = authenticator.generateSecret(); const encryptedSecret = encryptWithRoot(Buffer.from(secret)); - const recoveryCodes = Array.from({ length: 10 }).map(() => String(crypto.randomInt(10 ** 7, 10 ** 8 - 1))); + const recoveryCodes = Array.from({ length: MAX_RECOVERY_CODE_LIMIT }).map(generateRecoveryCode); const encryptedRecoveryCodes = encryptWithRoot(Buffer.from(recoveryCodes.join(","))); const newTotpConfig = await totpConfigDAL.create({ userId, @@ -241,16 +242,14 @@ export const totpServiceFactory = ({ totpConfigDAL, kmsService, userDAL }: TTotp } const recoveryCodes = decryptWithRoot(totpConfig.encryptedRecoveryCodes).toString().split(","); - if (recoveryCodes.length >= 10) { + if (recoveryCodes.length >= MAX_RECOVERY_CODE_LIMIT) { throw new BadRequestError({ - message: "Cannot have more than 10 recovery codes at a time" + message: `Cannot have more than ${MAX_RECOVERY_CODE_LIMIT} 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 toGenerateCount = MAX_RECOVERY_CODE_LIMIT - recoveryCodes.length; + const newRecoveryCodes = Array.from({ length: toGenerateCount }).map(generateRecoveryCode); const encryptedRecoveryCodes = encryptWithRoot(Buffer.from([...recoveryCodes, ...newRecoveryCodes].join(","))); await totpConfigDAL.updateById(totpConfig.id, { diff --git a/frontend/src/pages/login/select-organization.tsx b/frontend/src/pages/login/select-organization.tsx index c3d939e7b2..e2ebe37ae6 100644 --- a/frontend/src/pages/login/select-organization.tsx +++ b/frontend/src/pages/login/select-organization.tsx @@ -91,10 +91,12 @@ export default function LoginPage() { return; } - const { token, isMfaEnabled, mfaMethod } = await selectOrg.mutateAsync({ - organizationId: organization.id, - userAgent: callbackPort ? UserAgentType.CLI : undefined - }); + const { token, isMfaEnabled, mfaMethod } = await selectOrg + .mutateAsync({ + organizationId: organization.id, + userAgent: callbackPort ? UserAgentType.CLI : undefined + }) + .finally(() => setIsInitialOrgCheckLoading(false)); if (isMfaEnabled) { SecurityClient.setMfaToken(token); diff --git a/frontend/src/views/Login/Mfa.tsx b/frontend/src/views/Login/Mfa.tsx index 6132d05bfe..d494551900 100644 --- a/frontend/src/views/Login/Mfa.tsx +++ b/frontend/src/views/Login/Mfa.tsx @@ -114,7 +114,7 @@ export const Mfa = ({ successCallback, closeMfa, hideLogo, email, method }: Prop return ( <>
- Your organization requires mobile authenticator to be configured. + Your organization requires mobile authentication to be configured.
- No access to both codes? Reset your account + Lost your recovery codes? Reset your account