Repository: Infisical/infisical
Pull request #3198: Daniel/reset password serverside
Author: login: varonix0
Created: 2025-03-07T00:40:49Z | Merged: 2025-03-10T18:31:22Z

# Description 📣

This PR introduces password resets without the need for having an emergency kit (only for v2 user encryption). It remains backwards compatible for users that aren't on the new v2 user encryption architecture.

Additionally I've also removed the backup key's downloads, as they are no longer needed.

Other:
1. I found a timing attack 'vulnerability' in our email password reset endpoint. I've patched this up by forcing the endpoint to take 8 seconds to return.
2. I polished up the UI on the reset password screens.
    a. I also moved us away from react state, and we now use react-hook-form instead.  

## Type ✨

- [ ] Bug fix
- [ ] New feature
- [x] Improvement
- [ ] Breaking change
- [ ] Documentation

---

- [x] I have read the [contributing guide](https://infisical.com/docs/contributing/getting-started/overview), agreed and acknowledged the [code of conduct](https://infisical.com/docs/contributing/getting-started/code-of-conduct). 📝

<!-- If you have any questions regarding contribution, here's the FAQ : https://infisical.com/docs/contributing/getting-started/faq -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Launched a guided, multi-step password reset experience including email confirmation, secure key validation, and new password setup.
  - Enhanced password change options in account settings to support modern security protocols.
  - Introduced new hooks for password reset operations to improve user experience.
  - Added new components for structured password reset steps, including email confirmation and backup key input.
  - Updated user messaging for clarity during the password reset process.

- **Refactor**
  - Streamlined the signup process by eliminating redundant backup download steps for a simpler registration experience.
  - Improved form validations and user messaging across password reset and account management workflows.
  - Updated password reset functionality to utilize a more structured approach with form management libraries.

- **Bug Fixes**
  - Refined password validation to prevent input errors and ensure a smoother authentication experience.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/backend/src/server/routes/v1/password-router.ts b/backend/src/server/routes/v1/password-router.ts
index e96a577d9f7..724468e02a6 100644
--- a/backend/src/server/routes/v1/password-router.ts
+++ b/backend/src/server/routes/v1/password-router.ts
@@ -6,6 +6,7 @@ import { authRateLimit } from "@app/server/config/rateLimiter";
 import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
 import { validateSignUpAuthorization } from "@app/services/auth/auth-fns";
 import { AuthMode } from "@app/services/auth/auth-type";
+import { UserEncryption } from "@app/services/user/user-types";
 
 export const registerPasswordRouter = async (server: FastifyZodProvider) => {
   server.route({
@@ -113,20 +114,16 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => {
       }),
       response: {
         200: z.object({
-          message: z.string(),
           user: UsersSchema,
-          token: z.string()
+          token: z.string(),
+          userEncryptionVersion: z.nativeEnum(UserEncryption)
         })
       }
     },
     handler: async (req) => {
-      const { token, user } = await server.services.password.verifyPasswordResetEmail(req.body.email, req.body.code);
+      const passwordReset = await server.services.password.verifyPasswordResetEmail(req.body.email, req.body.code);
 
-      return {
-        message: "Successfully verified email",
-        user,
-        token
-      };
+      return passwordReset;
     }
   });
 
diff --git a/backend/src/server/routes/v2/index.ts b/backend/src/server/routes/v2/index.ts
index 3d7581a70b9..cece502dafa 100644
--- a/backend/src/server/routes/v2/index.ts
+++ b/backend/src/server/routes/v2/index.ts
@@ -3,6 +3,7 @@ import { registerIdentityOrgRouter } from "./identity-org-router";
 import { registerIdentityProjectRouter } from "./identity-project-router";
 import { registerMfaRouter } from "./mfa-router";
 import { registerOrgRouter } from "./organization-router";
+import { registerPasswordRouter } from "./password-router";
 import { registerProjectMembershipRouter } from "./project-membership-router";
 import { registerProjectRouter } from "./project-router";
 import { registerServiceTokenRouter } from "./service-token-router";
@@ -12,6 +13,7 @@ export const registerV2Routes = async (server: FastifyZodProvider) => {
   await server.register(registerMfaRouter, { prefix: "/auth" });
   await server.register(registerUserRouter, { prefix: "/users" });
   await server.register(registerServiceTokenRouter, { prefix: "/service-token" });
+  await server.register(registerPasswordRouter, { prefix: "/password" });
   await server.register(
     async (orgRouter) => {
       await orgRouter.register(registerOrgRouter);
diff --git a/backend/src/server/routes/v2/password-router.ts b/backend/src/server/routes/v2/password-router.ts
new file mode 100644
index 00000000000..165130decd8
--- /dev/null
+++ b/backend/src/server/routes/v2/password-router.ts
@@ -0,0 +1,53 @@
+import { z } from "zod";
+
+import { authRateLimit } from "@app/server/config/rateLimiter";
+import { validatePasswordResetAuthorization } from "@app/services/auth/auth-fns";
+import { AuthMode } from "@app/services/auth/auth-type";
+import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
+import { ResetPasswordV2Type } from "@app/services/auth/auth-password-type";
+
+export const registerPasswordRouter = async (server: FastifyZodProvider) => {
+  server.route({
+    method: "POST",
+    url: "/password-reset",
+    config: {
+      rateLimit: authRateLimit
+    },
+    schema: {
+      body: z.object({
+        newPassword: z.string().trim()
+      })
+    },
+    handler: async (req) => {
+      const token = validatePasswordResetAuthorization(req.headers.authorization);
+      await server.services.password.resetPasswordV2({
+        type: ResetPasswordV2Type.Recovery,
+        newPassword: req.body.newPassword,
+        userId: token.userId
+      });
+    }
+  });
+
+  server.route({
+    method: "POST",
+    url: "/user/password-reset",
+    schema: {
+      body: z.object({
+        oldPassword: z.string().trim(),
+        newPassword: z.string().trim()
+      })
+    },
+    config: {
+      rateLimit: authRateLimit
+    },
+    onRequest: verifyAuth([AuthMode.JWT], { requireOrg: false }),
+    handler: async (req) => {
+      await server.services.password.resetPasswordV2({
+        type: ResetPasswordV2Type.LoggedInReset,
+        userId: req.permission.id,
+        newPassword: req.body.newPassword,
+        oldPassword: req.body.oldPassword
+      });
+    }
+  });
+};
diff --git a/backend/src/services/auth/auth-fns.ts b/backend/src/services/auth/auth-fns.ts
index 5f7aca8128c..ec6e0a30343 100644
--- a/backend/src/services/auth/auth-fns.ts
+++ b/backend/src/services/auth/auth-fns.ts
@@ -45,6 +45,36 @@ export const validateSignUpAuthorization = (token: string, userId: string, valid
   if (decodedToken.userId !== userId) throw new UnauthorizedError();
 };
 
+export const validatePasswordResetAuthorization = (token?: string) => {
+  if (!token) throw new UnauthorizedError();
+
+  const appCfg = getConfig();
+  const [AUTH_TOKEN_TYPE, AUTH_TOKEN_VALUE] = <[string, string]>token?.split(" ", 2) ?? [null, null];
+  if (AUTH_TOKEN_TYPE === null) {
+    throw new UnauthorizedError({ message: "Missing Authorization Header in the request header." });
+  }
+  if (AUTH_TOKEN_TYPE.toLowerCase() !== "bearer") {
+    throw new UnauthorizedError({
+      message: `The provided authentication type '${AUTH_TOKEN_TYPE}' is not supported.`
+    });
+  }
+  if (AUTH_TOKEN_VALUE === null) {
+    throw new UnauthorizedError({
+      message: "Missing Authorization Body in the request header"
+    });
+  }
+
+  const decodedToken = jwt.verify(AUTH_TOKEN_VALUE, appCfg.AUTH_SECRET) as AuthModeProviderSignUpTokenPayload;
+
+  if (decodedToken.authTokenType !== AuthTokenType.SIGNUP_TOKEN) {
+    throw new UnauthorizedError({
+      message: `The provided authentication token type is not supported.`
+    });
+  }
+
+  return decodedToken;
+};
+
 export const enforceUserLockStatus = (isLocked: boolean, temporaryLockDateEnd?: Date | null) => {
   if (isLocked) {
     throw new ForbiddenRequestError({
diff --git a/backend/src/services/auth/auth-password-service.ts b/backend/src/services/auth/auth-password-service.ts
index 9f004eafc49..f5f42a23640 100644
--- a/backend/src/services/auth/auth-password-service.ts
+++ b/backend/src/services/auth/auth-password-service.ts
@@ -4,6 +4,8 @@ import jwt from "jsonwebtoken";
 import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas";
 import { getConfig } from "@app/lib/config/env";
 import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
+import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
+import { generateUserSrpKeys } from "@app/lib/crypto/srp";
 import { BadRequestError } from "@app/lib/errors";
 import { OrgServiceActor } from "@app/lib/types";
 
@@ -12,14 +14,18 @@ import { TokenType } from "../auth-token/auth-token-types";
 import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
 import { TTotpConfigDALFactory } from "../totp/totp-config-dal";
 import { TUserDALFactory } from "../user/user-dal";
+import { UserEncryption } from "../user/user-types";
 import { TAuthDALFactory } from "./auth-dal";
 import {
+  ResetPasswordV2Type,
   TChangePasswordDTO,
   TCreateBackupPrivateKeyDTO,
+  TResetPasswordV2DTO,
   TResetPasswordViaBackupKeyDTO,
   TSetupPasswordViaBackupKeyDTO
 } from "./auth-password-type";
 import { ActorType, AuthMethod, AuthTokenType } from "./auth-type";
+import { logger } from "@app/lib/logger";
 
 type TAuthPasswordServiceFactoryDep = {
   authDAL: TAuthDALFactory;
@@ -114,26 +120,31 @@ export const authPaswordServiceFactory = ({
    * Email password reset flow via email. Step 1 send email
    */
   const sendPasswordResetEmail = async (email: string) => {
-    const user = await userDAL.findUserByUsername(email);
-    // ignore as user is not found to avoid an outside entity to identify infisical registered accounts
-    if (!user || (user && !user.isAccepted)) return;
-
-    const cfg = getConfig();
-    const token = await tokenService.createTokenForUser({
-      type: TokenType.TOKEN_EMAIL_PASSWORD_RESET,
-      userId: user.id
-    });
-
-    await smtpService.sendMail({
-      template: SmtpTemplates.ResetPassword,
-      recipients: [email],
-      subjectLine: "Infisical password reset",
-      substitutions: {
-        email,
-        token,
-        callback_url: cfg.SITE_URL ? `${cfg.SITE_URL}/password-reset` : ""
+    const sendEmail = async () => {
+      const user = await userDAL.findUserByUsername(email);
+
+      if (user && user.isAccepted) {
+        const cfg = getConfig();
+        const token = await tokenService.createTokenForUser({
+          type: TokenType.TOKEN_EMAIL_PASSWORD_RESET,
+          userId: user.id
+        });
+
+        await smtpService.sendMail({
+          template: SmtpTemplates.ResetPassword,
+          recipients: [email],
+          subjectLine: "Infisical password reset",
+          substitutions: {
+            email,
+            token,
+            callback_url: cfg.SITE_URL ? `${cfg.SITE_URL}/password-reset` : ""
+          }
+        });
       }
-    });
+    };
+
+    // note(daniel): run in background to prevent timing attacks
+    void sendEmail().catch((err) => logger.error(err, "Failed to send password reset email"));
   };
 
   /*
@@ -142,6 +153,11 @@ export const authPaswordServiceFactory = ({
   const verifyPasswordResetEmail = async (email: string, code: string) => {
     const cfg = getConfig();
     const user = await userDAL.findUserByUsername(email);
+
+    const userEnc = await userDAL.findUserEncKeyByUserId(user.id);
+
+    if (!userEnc) throw new BadRequestError({ message: "Failed to find user encryption data" });
+
     // ignore as user is not found to avoid an outside entity to identify infisical registered accounts
     if (!user || (user && !user.isAccepted)) {
       throw new Error("Failed email verification for pass reset");
@@ -162,8 +178,91 @@ export const authPaswordServiceFactory = ({
       { expiresIn: cfg.JWT_SIGNUP_LIFETIME }
     );
 
-    return { token, user };
+    return { token, user, userEncryptionVersion: userEnc.encryptionVersion as UserEncryption };
+  };
+
+  const resetPasswordV2 = async ({ userId, newPassword, type, oldPassword }: TResetPasswordV2DTO) => {
+    const cfg = getConfig();
+
+    const user = await userDAL.findUserEncKeyByUserId(userId);
+    if (!user) {
+      throw new BadRequestError({ message: `User encryption key not found for user with ID '${userId}'` });
+    }
+
+    if (!user.hashedPassword) {
+      throw new BadRequestError({ message: "Unable to reset password, no password is set" });
+    }
+
+    if (!user.authMethods?.includes(AuthMethod.EMAIL)) {
+      throw new BadRequestError({ message: "Unable to reset password, no email authentication method is configured" });
+    }
+
+    // we check the old password if the user is resetting their password while logged in
+    if (type === ResetPasswordV2Type.LoggedInReset) {
+      if (!oldPassword) {
+        throw new BadRequestError({ message: "Current password is required." });
+      }
+
+      const isValid = await bcrypt.compare(oldPassword, user.hashedPassword);
+      if (!isValid) {
+        throw new BadRequestError({ message: "Incorrect current password." });
+      }
+    }
+
+    const newHashedPassword = await bcrypt.hash(newPassword, cfg.BCRYPT_SALT_ROUND);
+
+    // we need to get the original private key first for v2
+    let privateKey: string;
+    if (
+      user.serverEncryptedPrivateKey &&
+      user.serverEncryptedPrivateKeyTag &&
+      user.serverEncryptedPrivateKeyIV &&
+      user.serverEncryptedPrivateKeyEncoding &&
+      user.encryptionVersion === UserEncryption.V2
+    ) {
+      privateKey = infisicalSymmetricDecrypt({
+        iv: user.serverEncryptedPrivateKeyIV,
+        tag: user.serverEncryptedPrivateKeyTag,
+        ciphertext: user.serverEncryptedPrivateKey,
+        keyEncoding: user.serverEncryptedPrivateKeyEncoding as SecretKeyEncoding
+      });
+    } else {
+      throw new BadRequestError({
+        message: "Cannot reset password without current credentials or recovery method",
+        name: "Reset password"
+      });
+    }
+
+    const encKeys = await generateUserSrpKeys(user.username, newPassword, {
+      publicKey: user.publicKey,
+      privateKey
+    });
+
+    const { tag, iv, ciphertext, encoding } = infisicalSymmetricEncypt(privateKey);
+
+    await userDAL.updateUserEncryptionByUserId(userId, {
+      hashedPassword: newHashedPassword,
+
+      // srp params
+      salt: encKeys.salt,
+      verifier: encKeys.verifier,
+
+      protectedKey: encKeys.protectedKey,
+      protectedKeyIV: encKeys.protectedKeyIV,
+      protectedKeyTag: encKeys.protectedKeyTag,
+      encryptedPrivateKey: encKeys.encryptedPrivateKey,
+      iv: encKeys.encryptedPrivateKeyIV,
+      tag: encKeys.encryptedPrivateKeyTag,
+
+      serverEncryptedPrivateKey: ciphertext,
+      serverEncryptedPrivateKeyIV: iv,
+      serverEncryptedPrivateKeyTag: tag,
+      serverEncryptedPrivateKeyEncoding: encoding
+    });
+
+    await tokenService.revokeAllMySessions(userId);
   };
+
   /*
    * Reset password of a user via backup key
    * */
@@ -391,6 +490,7 @@ export const authPaswordServiceFactory = ({
     createBackupPrivateKey,
     getBackupPrivateKeyOfUser,
     sendPasswordSetupEmail,
-    setupPassword
+    setupPassword,
+    resetPasswordV2
   };
 };
diff --git a/backend/src/services/auth/auth-password-type.ts b/backend/src/services/auth/auth-password-type.ts
index 7c67c093474..b3b14c3b41f 100644
--- a/backend/src/services/auth/auth-password-type.ts
+++ b/backend/src/services/auth/auth-password-type.ts
@@ -13,6 +13,18 @@ export type TChangePasswordDTO = {
   password: string;
 };
 
+export enum ResetPasswordV2Type {
+  Recovery = "recovery",
+  LoggedInReset = "logged-in-reset"
+}
+
+export type TResetPasswordV2DTO = {
+  type: ResetPasswordV2Type;
+  userId: string;
+  newPassword: string;
+  oldPassword?: string;
+};
+
 export type TResetPasswordViaBackupKeyDTO = {
   userId: string;
   protectedKey: string;
diff --git a/frontend/src/components/auth/DonwloadBackupPDFStep.tsx b/frontend/src/components/auth/DonwloadBackupPDFStep.tsx
deleted file mode 100644
index b11ca8bd2b8..00000000000
--- a/frontend/src/components/auth/DonwloadBackupPDFStep.tsx
+++ /dev/null
@@ -1,93 +0,0 @@
-import { useTranslation } from "react-i18next";
-import { faWarning } from "@fortawesome/free-solid-svg-icons";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-
-import { useToggle } from "@app/hooks";
-import { generateUserBackupKey } from "@app/lib/crypto";
-
-import { createNotification } from "../notifications";
-import { generateBackupPDFAsync } from "../utilities/generateBackupPDF";
-import { Button } from "../v2";
-
-interface DownloadBackupPDFStepProps {
-  incrementStep: () => void;
-  email: string;
-  password: string;
-  name: string;
-}
-
-/**
- * This is the step of the signup flow where the user downloads the backup pdf
- * @param {object} obj
- * @param {function} obj.incrementStep - function that moves the user on to the next stage of signup
- * @param {string} obj.email - user's email
- * @param {string} obj.password - user's password
- * @param {string} obj.name - user's name
- * @returns
- */
-export default function DonwloadBackupPDFStep({
-  incrementStep,
-  email,
-  password,
-  name
-}: DownloadBackupPDFStepProps): JSX.Element {
-  const { t } = useTranslation();
-
-  const [isLoading, setIsLoading] = useToggle();
-
-  const handleBackupKeyGenerate = async () => {
-    try {
-      setIsLoading.on();
-      const generatedKey = await generateUserBackupKey(email, password);
-      await generateBackupPDFAsync({
-        generatedKey,
-        personalEmail: email,
-        personalName: name
-      });
-      incrementStep();
-    } catch (err) {
-      console.log(err);
-      createNotification({
-        type: "error",
-        text: "Failed to generate backup key"
-      });
-    } finally {
-      setIsLoading.off();
-    }
-  };
-
-  return (
-    <div className="mx-auto mb-36 flex h-full w-full flex-col items-center md:mb-16 md:px-6">
-      <p className="flex flex-col items-center justify-center bg-gradient-to-b from-white to-bunker-200 bg-clip-text text-center text-xl font-medium text-transparent">
-        <FontAwesomeIcon
-          icon={faWarning}
-          className="mb-6 ml-2 mr-3 pt-1 text-6xl text-bunker-200"
-        />
-        {t("signup.step4-message")}
-      </p>
-      <div className="text-md mt-8 flex w-full max-w-md flex-col items-center justify-center rounded-md border border-mineshaft-600 bg-mineshaft-800 pb-2 text-center text-bunker-300 md:min-w-[24rem] lg:w-1/6">
-        <div className="m-2 mx-auto mt-4 flex w-full flex-row items-center rounded-md px-3 text-center text-bunker-300 md:mt-8 md:min-w-[23rem] lg:w-1/6">
-          <span className="mb-2">
-            {t("signup.step4-description1")} {t("signup.step4-description3")}
-          </span>
-        </div>
-        <div className="mx-auto mb-2 mt-2 flex w-full flex-col items-center justify-center px-3 text-center text-sm md:mb-4 md:mt-4 md:min-w-[20rem] md:max-w-md md:text-left lg:w-1/6">
-          <div className="text-l w-full py-1 text-lg">
-            <Button
-              onClick={handleBackupKeyGenerate}
-              size="sm"
-              isFullWidth
-              isLoading={isLoading}
-              isDisabled={isLoading}
-              className="h-12"
-              colorSchema="primary"
-              variant="outline_bg"
-            >
-              Download PDF
-            </Button>
-          </div>
-        </div>
-      </div>
-    </div>
-  );
-}
diff --git a/frontend/src/components/utilities/checks/password/PasswordCheck.ts b/frontend/src/components/utilities/checks/password/PasswordCheck.ts
index e37abd4757d..fb51862205e 100644
--- a/frontend/src/components/utilities/checks/password/PasswordCheck.ts
+++ b/frontend/src/components/utilities/checks/password/PasswordCheck.ts
@@ -34,12 +34,12 @@ const passwordCheck = async ({
   const tests = [
     {
       name: "tooShort",
-      validator: (pwd: string) => pwd.length >= 14,
+      validator: (pwd: string) => pwd?.length >= 14,
       setError: setPasswordErrorTooShort
     },
     {
       name: "tooLong",
-      validator: (pwd: string) => pwd.length < 101,
+      validator: (pwd: string) => pwd?.length < 101,
       setError: setPasswordErrorTooLong
     },
     {
diff --git a/frontend/src/hooks/api/auth/index.tsx b/frontend/src/hooks/api/auth/index.tsx
index 66688cbdcf4..392f7b1f57d 100644
--- a/frontend/src/hooks/api/auth/index.tsx
+++ b/frontend/src/hooks/api/auth/index.tsx
@@ -2,6 +2,8 @@ export {
   useGetAuthToken,
   useOauthTokenExchange,
   useResetPassword,
+  useResetPasswordV2,
+  useResetUserPasswordV2,
   useSelectOrganization,
   useSendMfaToken,
   useSendPasswordResetEmail,
diff --git a/frontend/src/hooks/api/auth/queries.tsx b/frontend/src/hooks/api/auth/queries.tsx
index 9b8afbac718..796fd315292 100644
--- a/frontend/src/hooks/api/auth/queries.tsx
+++ b/frontend/src/hooks/api/auth/queries.tsx
@@ -22,12 +22,15 @@ import {
   LoginLDAPRes,
   MfaMethod,
   ResetPasswordDTO,
+  ResetPasswordV2DTO,
+  ResetUserPasswordV2DTO,
   SendMfaTokenDTO,
   SetupPasswordDTO,
   SRP1DTO,
   SRPR1Res,
   TOauthTokenExchangeDTO,
   UserAgentType,
+  UserEncryptionVersion,
   VerifyMfaTokenDTO,
   VerifyMfaTokenRes,
   VerifySignupInviteDTO
@@ -247,7 +250,10 @@ export const useSendPasswordResetEmail = () => {
 export const useVerifyPasswordResetCode = () => {
   return useMutation({
     mutationFn: async ({ email, code }: { email: string; code: string }) => {
-      const { data } = await apiRequest.post("/api/v1/password/email/password-reset-verify", {
+      const { data } = await apiRequest.post<{
+        token: string;
+        userEncryptionVersion: UserEncryptionVersion;
+      }>("/api/v1/password/email/password-reset-verify", {
         email,
         code
       });
@@ -302,6 +308,26 @@ export const useResetPassword = () => {
   });
 };
 
+export const useResetPasswordV2 = () => {
+  return useMutation({
+    mutationFn: async (details: ResetPasswordV2DTO) => {
+      await apiRequest.post("/api/v2/password/password-reset", details, {
+        headers: {
+          Authorization: `Bearer ${details.verificationToken}`
+        }
+      });
+    }
+  });
+};
+
+export const useResetUserPasswordV2 = () => {
+  return useMutation({
+    mutationFn: async (details: ResetUserPasswordV2DTO) => {
+      await apiRequest.post("/api/v2/password/user/password-reset", details);
+    }
+  });
+};
+
 export const changePassword = async (details: ChangePasswordDTO) => {
   const { data } = await apiRequest.post("/api/v1/password/change-password", details);
   return data;
diff --git a/frontend/src/hooks/api/auth/types.ts b/frontend/src/hooks/api/auth/types.ts
index 036897fed4e..32610c28d65 100644
--- a/frontend/src/hooks/api/auth/types.ts
+++ b/frontend/src/hooks/api/auth/types.ts
@@ -3,6 +3,11 @@ export type GetAuthTokenAPI = {
   organizationId?: string;
 };
 
+export enum UserEncryptionVersion {
+  V1 = 1,
+  V2 = 2
+}
+
 export type SendMfaTokenDTO = {
   email: string;
 };
@@ -136,6 +141,16 @@ export type ResetPasswordDTO = {
   password: string;
 };
 
+export type ResetPasswordV2DTO = {
+  newPassword: string;
+  verificationToken: string;
+};
+
+export type ResetUserPasswordV2DTO = {
+  oldPassword: string;
+  newPassword: string;
+};
+
 export type SetupPasswordDTO = {
   protectedKey: string;
   protectedKeyIV: string;
diff --git a/frontend/src/pages/admin/SignUpPage/SignUpPage.tsx b/frontend/src/pages/admin/SignUpPage/SignUpPage.tsx
index f1c70d02822..97b363558d4 100644
--- a/frontend/src/pages/admin/SignUpPage/SignUpPage.tsx
+++ b/frontend/src/pages/admin/SignUpPage/SignUpPage.tsx
@@ -1,4 +1,3 @@
-import { useState } from "react";
 import { Helmet } from "react-helmet";
 import { Controller, useForm } from "react-hook-form";
 import { useTranslation } from "react-i18next";
@@ -8,16 +7,13 @@ import { AnimatePresence, motion } from "framer-motion";
 import { z } from "zod";
 
 import { createNotification } from "@app/components/notifications";
-import { generateBackupPDFAsync } from "@app/components/utilities/generateBackupPDF";
 // TODO(akhilmhdh): rewrite this into module functions in lib
 import { saveTokenToLocalStorage } from "@app/components/utilities/saveTokenToLocalStorage";
 import SecurityClient from "@app/components/utilities/SecurityClient";
 import { Button, ContentLoader, FormControl, Input } from "@app/components/v2";
 import { useServerConfig } from "@app/context";
 import { useCreateAdminUser, useSelectOrganization } from "@app/hooks/api";
-import { generateUserBackupKey, generateUserPassKey } from "@app/lib/crypto";
-
-import { DownloadBackupKeys } from "./components/DownloadBackupKeys";
+import { generateUserPassKey } from "@app/lib/crypto";
 
 const formSchema = z
   .object({
@@ -34,25 +30,17 @@ const formSchema = z
 
 type TFormSchema = z.infer<typeof formSchema>;
 
-enum SignupSteps {
-  DetailsForm = "details-form",
-  BackupKey = "backup-key"
-}
-
 export const SignUpPage = () => {
   const { t } = useTranslation();
   const navigate = useNavigate();
   const {
     control,
     handleSubmit,
-    getValues,
     formState: { isSubmitting }
   } = useForm<TFormSchema>({
     resolver: zodResolver(formSchema)
   });
 
-  const [step, setStep] = useState(SignupSteps.DetailsForm);
-
   const { config } = useServerConfig();
   const { mutateAsync: createAdminUser } = useCreateAdminUser();
   const { mutateAsync: selectOrganization } = useSelectOrganization();
@@ -84,37 +72,17 @@ export const SignUpPage = () => {
       // Will be refactored in next iteration to make it url based rather than local storage ones
       // Part of migration to nextjs 14
       localStorage.setItem("orgData.id", res.organization.id);
-      setStep(SignupSteps.BackupKey);
-    } catch (err) {
-      console.log(err);
-      createNotification({
-        type: "error",
-        text: "Failed to create admin"
-      });
-    }
-  };
-
-  const handleBackupKeyGenerate = async () => {
-    try {
-      const { email, password, firstName, lastName } = getValues();
-      const generatedKey = await generateUserBackupKey(email, password);
-      await generateBackupPDFAsync({
-        generatedKey,
-        personalEmail: email,
-        personalName: `${firstName} ${lastName}`
-      });
       navigate({ to: "/admin" });
     } catch (err) {
       console.log(err);
       createNotification({
         type: "error",
-        text: "Failed to generate backup"
+        text: "Failed to create admin"
       });
     }
   };
 
-  if (config?.initialized && step === SignupSteps.DetailsForm)
-    return <ContentLoader text="Redirecting to admin page..." />;
+  if (config?.initialized) return <ContentLoader text="Redirecting to admin page..." />;
 
   return (
     <div className="flex max-h-screen min-h-screen flex-col justify-center overflow-y-auto bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700 px-6">
@@ -127,56 +95,28 @@ export const SignUpPage = () => {
       </Helmet>
       <div className="flex items-center justify-center">
         <AnimatePresence mode="wait">
-          {step === SignupSteps.DetailsForm && (
-            <motion.div
-              className="text-mineshaft-200"
-              key="panel-1"
-              transition={{ duration: 0.15 }}
-              initial={{ opacity: 0, translateX: 30 }}
-              animate={{ opacity: 1, translateX: 0 }}
-              exit={{ opacity: 0, translateX: 30 }}
-            >
-              <div className="flex flex-col items-center space-y-2 text-center">
-                <img src="/images/gradientLogo.svg" height={90} width={120} alt="Infisical logo" />
-                <div className="pt-4 text-4xl">Welcome to Infisical</div>
-                <div className="pb-4 text-bunker-300">Create your first Super Admin Account</div>
-              </div>
-              <form onSubmit={handleSubmit(handleFormSubmit)}>
-                <div className="mt-8">
-                  <div className="flex items-center space-x-4">
-                    <Controller
-                      control={control}
-                      name="firstName"
-                      render={({ field, fieldState: { error } }) => (
-                        <FormControl
-                          label="First name"
-                          errorText={error?.message}
-                          isError={Boolean(error)}
-                        >
-                          <Input isFullWidth size="md" {...field} />
-                        </FormControl>
-                      )}
-                    />
-                    <Controller
-                      control={control}
-                      name="lastName"
-                      render={({ field, fieldState: { error } }) => (
-                        <FormControl
-                          label="Last name"
-                          errorText={error?.message}
-                          isError={Boolean(error)}
-                        >
-                          <Input isFullWidth size="md" {...field} />
-                        </FormControl>
-                      )}
-                    />
-                  </div>
+          <motion.div
+            className="text-mineshaft-200"
+            key="panel-1"
+            transition={{ duration: 0.15 }}
+            initial={{ opacity: 0, translateX: 30 }}
+            animate={{ opacity: 1, translateX: 0 }}
+            exit={{ opacity: 0, translateX: 30 }}
+          >
+            <div className="flex flex-col items-center space-y-2 text-center">
+              <img src="/images/gradientLogo.svg" height={90} width={120} alt="Infisical logo" />
+              <div className="pt-4 text-4xl">Welcome to Infisical</div>
+              <div className="pb-4 text-bunker-300">Create your first Super Admin Account</div>
+            </div>
+            <form onSubmit={handleSubmit(handleFormSubmit)}>
+              <div className="mt-8">
+                <div className="flex items-center space-x-4">
                   <Controller
                     control={control}
-                    name="email"
+                    name="firstName"
                     render={({ field, fieldState: { error } }) => (
                       <FormControl
-                        label="Email"
+                        label="First name"
                         errorText={error?.message}
                         isError={Boolean(error)}
                       >
@@ -186,56 +126,66 @@ export const SignUpPage = () => {
                   />
                   <Controller
                     control={control}
-                    name="password"
-                    render={({ field, fieldState: { error } }) => (
-                      <FormControl
-                        label="Password"
-                        errorText={error?.message}
-                        isError={Boolean(error)}
-                      >
-                        <Input isFullWidth size="md" type="password" {...field} />
-                      </FormControl>
-                    )}
-                  />
-                  <Controller
-                    control={control}
-                    name="confirmPassword"
+                    name="lastName"
                     render={({ field, fieldState: { error } }) => (
                       <FormControl
-                        label="Confirm password"
+                        label="Last name"
                         errorText={error?.message}
                         isError={Boolean(error)}
                       >
-                        <Input isFullWidth size="md" type="password" {...field} />
+                        <Input isFullWidth size="md" {...field} />
                       </FormControl>
                     )}
                   />
                 </div>
-                <Button
-                  type="submit"
-                  colorSchema="primary"
-                  variant="outline_bg"
-                  isFullWidth
-                  className="mt-4"
-                  isLoading={isSubmitting}
-                >
-                  Continue
-                </Button>
-              </form>
-            </motion.div>
-          )}
-          {step === SignupSteps.BackupKey && (
-            <motion.div
-              className="text-mineshaft-200"
-              key="panel-2"
-              transition={{ duration: 0.15 }}
-              initial={{ opacity: 0, translateX: 30 }}
-              animate={{ opacity: 1, translateX: 0 }}
-              exit={{ opacity: 0, translateX: 30 }}
-            >
-              <DownloadBackupKeys onGenerate={handleBackupKeyGenerate} />
-            </motion.div>
-          )}
+                <Controller
+                  control={control}
+                  name="email"
+                  render={({ field, fieldState: { error } }) => (
+                    <FormControl label="Email" errorText={error?.message} isError={Boolean(error)}>
+                      <Input isFullWidth size="md" {...field} />
+                    </FormControl>
+                  )}
+                />
+                <Controller
+                  control={control}
+                  name="password"
+                  render={({ field, fieldState: { error } }) => (
+                    <FormControl
+                      label="Password"
+                      errorText={error?.message}
+                      isError={Boolean(error)}
+                    >
+                      <Input isFullWidth size="md" type="password" {...field} />
+                    </FormControl>
+                  )}
+                />
+                <Controller
+                  control={control}
+                  name="confirmPassword"
+                  render={({ field, fieldState: { error } }) => (
+                    <FormControl
+                      label="Confirm password"
+                      errorText={error?.message}
+                      isError={Boolean(error)}
+                    >
+                      <Input isFullWidth size="md" type="password" {...field} />
+                    </FormControl>
+                  )}
+                />
+              </div>
+              <Button
+                type="submit"
+                colorSchema="primary"
+                variant="outline_bg"
+                isFullWidth
+                className="mt-4"
+                isLoading={isSubmitting}
+              >
+                Continue
+              </Button>
+            </form>
+          </motion.div>
         </AnimatePresence>
       </div>
     </div>
diff --git a/frontend/src/pages/admin/SignUpPage/components/DownloadBackupKeys/DownloadBackupKeys.tsx b/frontend/src/pages/admin/SignUpPage/components/DownloadBackupKeys/DownloadBackupKeys.tsx
deleted file mode 100644
index 253031a910b..00000000000
--- a/frontend/src/pages/admin/SignUpPage/components/DownloadBackupKeys/DownloadBackupKeys.tsx
+++ /dev/null
@@ -1,56 +0,0 @@
-import { useTranslation } from "react-i18next";
-import { faWarning } from "@fortawesome/free-solid-svg-icons";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-
-import { Button } from "@app/components/v2";
-import { useToggle } from "@app/hooks";
-
-type Props = {
-  onGenerate: () => Promise<void>;
-};
-
-export const DownloadBackupKeys = ({ onGenerate }: Props): JSX.Element => {
-  const { t } = useTranslation();
-  const [isLoading, setIsLoading] = useToggle();
-
-  return (
-    <div className="mx-auto mb-36 flex h-full w-full flex-col items-center md:mb-16 md:px-6">
-      <p className="flex flex-col items-center justify-center bg-gradient-to-b from-white to-bunker-200 bg-clip-text text-center text-xl font-medium text-transparent">
-        <FontAwesomeIcon
-          icon={faWarning}
-          className="mb-6 ml-2 mr-3 pt-1 text-6xl text-bunker-200"
-        />
-        {t("signup.step4-message")}
-      </p>
-      <div className="text-md mt-8 flex w-full max-w-md flex-col items-center justify-center rounded-md border border-mineshaft-600 bg-mineshaft-800 pb-2 text-center text-bunker-300 md:min-w-[24rem] lg:w-1/6">
-        <div className="m-2 mx-auto mt-4 flex w-full flex-row items-center rounded-md px-3 text-center text-bunker-300 md:mt-8 md:min-w-[23rem] lg:w-1/6">
-          <span className="mb-2">
-            {t("signup.step4-description1")} {t("signup.step4-description3")}
-          </span>
-        </div>
-        <div className="mx-auto mb-2 mt-2 flex w-full flex-col items-center justify-center px-3 text-center text-sm md:mb-4 md:mt-4 md:min-w-[20rem] md:max-w-md md:text-left lg:w-1/6">
-          <div className="text-l w-full py-1 text-lg">
-            <Button
-              onClick={async () => {
-                try {
-                  setIsLoading.on();
-                  await onGenerate();
-                } finally {
-                  setIsLoading.off();
-                }
-              }}
-              size="sm"
-              isFullWidth
-              className="h-12"
-              colorSchema="primary"
-              variant="outline_bg"
-              isLoading={isLoading}
-            >
-              Download PDF
-            </Button>
-          </div>
-        </div>
-      </div>
-    </div>
-  );
-};
diff --git a/frontend/src/pages/admin/SignUpPage/components/DownloadBackupKeys/index.tsx b/frontend/src/pages/admin/SignUpPage/components/DownloadBackupKeys/index.tsx
deleted file mode 100644
index bbbd9aad924..00000000000
--- a/frontend/src/pages/admin/SignUpPage/components/DownloadBackupKeys/index.tsx
+++ /dev/null
@@ -1 +0,0 @@
-export { DownloadBackupKeys } from "./DownloadBackupKeys";
diff --git a/frontend/src/pages/auth/PasswordResetPage/PasswordResetPage.tsx b/frontend/src/pages/auth/PasswordResetPage/PasswordResetPage.tsx
index 3361dd961a6..8b6be6b9a74 100644
--- a/frontend/src/pages/auth/PasswordResetPage/PasswordResetPage.tsx
+++ b/frontend/src/pages/auth/PasswordResetPage/PasswordResetPage.tsx
@@ -1,396 +1,75 @@
-import crypto from "crypto";
-
-import { FormEvent, useState } from "react";
-import { faCheck, faX } from "@fortawesome/free-solid-svg-icons";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { useNavigate, useSearch } from "@tanstack/react-router";
-import jsrp from "jsrp";
-
-import InputField from "@app/components/basic/InputField";
-import passwordCheck from "@app/components/utilities/checks/password/PasswordCheck";
-import Aes256Gcm from "@app/components/utilities/cryptography/aes-256-gcm";
-import { deriveArgonKey } from "@app/components/utilities/cryptography/crypto";
-import { Button } from "@app/components/v2";
-import { ROUTE_PATHS } from "@app/const/routes";
-import { useResetPassword, useVerifyPasswordResetCode } from "@app/hooks/api";
-import { getBackupEncryptedPrivateKey } from "@app/hooks/api/auth/queries";
-
-// eslint-disable-next-line new-cap
-const client = new jsrp.client();
+import { useState } from "react";
+import { useForm } from "react-hook-form";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { useNavigate } from "@tanstack/react-router";
+import { z } from "zod";
+
+import { UserEncryptionVersion } from "@app/hooks/api/auth/types";
+
+import { ConfirmEmailStep } from "./components/ConfirmEmailStep";
+import { EnterPasswordStep } from "./components/EnterPasswordStep";
+import { InputBackupKeyStep } from "./components/InputBackupKeyStep";
+
+enum Steps {
+  ConfirmEmail = 1,
+  InputBackupKey = 2,
+  EnterNewPassword = 3
+}
+
+const formData = z.object({
+  verificationToken: z.string(),
+  privateKey: z.string(),
+  userEncryptionVersion: z.nativeEnum(UserEncryptionVersion)
+});
+type TFormData = z.infer<typeof formData>;
 
 export const PasswordResetPage = () => {
-  const [verificationToken, setVerificationToken] = useState("");
-  const [step, setStep] = useState(1);
-  const [loading, setLoading] = useState(false);
-  const [backupKey, setBackupKey] = useState("");
-  const [privateKey, setPrivateKey] = useState("");
-  const [newPassword, setNewPassword] = useState("");
-  const [backupKeyError, setBackupKeyError] = useState(false);
-  const [passwordErrorTooShort, setPasswordErrorTooShort] = useState(false);
-  const [passwordErrorTooLong, setPasswordErrorTooLong] = useState(false);
-  const [passwordErrorNoLetterChar, setPasswordErrorNoLetterChar] = useState(false);
-  const [passwordErrorNoNumOrSpecialChar, setPasswordErrorNoNumOrSpecialChar] = useState(false);
-  const [passwordErrorRepeatedChar, setPasswordErrorRepeatedChar] = useState(false);
-  const [passwordErrorEscapeChar, setPasswordErrorEscapeChar] = useState(false);
-  const [passwordErrorLowEntropy, setPasswordErrorLowEntropy] = useState(false);
-  const [passwordErrorBreached, setPasswordErrorBreached] = useState(false);
-
-  const navigate = useNavigate();
-  const search = useSearch({ from: ROUTE_PATHS.Auth.PasswordResetPage.id });
-
-  const {
-    mutateAsync: verifyPasswordResetCodeMutateAsync,
-    isPending: isVerifyPasswordResetLoading
-  } = useVerifyPasswordResetCode();
-  const { mutateAsync: resetPasswordMutateAsync } = useResetPassword();
-
-  const parsedUrl = search;
-  const token = parsedUrl.token as string;
-  const email = (parsedUrl.to as string)?.replace(" ", "+").trim();
-
-  // Decrypt the private key with a backup key
-  const getEncryptedKeyHandler = async (e: FormEvent<HTMLFormElement>) => {
-    e.preventDefault();
-    try {
-      const result = await getBackupEncryptedPrivateKey({ verificationToken });
-
-      setPrivateKey(
-        Aes256Gcm.decrypt({
-          ciphertext: result.encryptedPrivateKey,
-          iv: result.iv,
-          tag: result.tag,
-          secret: backupKey
-        })
-      );
-      setStep(3);
-    } catch (err) {
-      console.error(err);
-      setBackupKeyError(true);
-    }
-  };
-
-  // If everything is correct, reset the password
-  const resetPasswordHandler = async (e: FormEvent<HTMLFormElement>) => {
-    e.preventDefault();
-    const errorCheck = await passwordCheck({
-      password: newPassword,
-      setPasswordErrorTooShort,
-      setPasswordErrorTooLong,
-      setPasswordErrorNoLetterChar,
-      setPasswordErrorNoNumOrSpecialChar,
-      setPasswordErrorRepeatedChar,
-      setPasswordErrorEscapeChar,
-      setPasswordErrorLowEntropy,
-      setPasswordErrorBreached
-    });
-
-    if (!errorCheck) {
-      client.init(
-        {
-          username: email,
-          password: newPassword
-        },
-        async () => {
-          client.createVerifier(async (_err: any, result: { salt: string; verifier: string }) => {
-            const derivedKey = await deriveArgonKey({
-              password: newPassword,
-              salt: result.salt,
-              mem: 65536,
-              time: 3,
-              parallelism: 1,
-              hashLen: 32
-            });
-
-            if (!derivedKey) throw new Error("Failed to derive key from password");
-
-            const key = crypto.randomBytes(32);
-
-            // create encrypted private key by encrypting the private
-            // key with the symmetric key [key]
-            const {
-              ciphertext: encryptedPrivateKey,
-              iv: encryptedPrivateKeyIV,
-              tag: encryptedPrivateKeyTag
-            } = Aes256Gcm.encrypt({
-              text: privateKey,
-              secret: key
-            });
-
-            // create the protected key by encrypting the symmetric key
-            // [key] with the derived key
-            const {
-              ciphertext: protectedKey,
-              iv: protectedKeyIV,
-              tag: protectedKeyTag
-            } = Aes256Gcm.encrypt({
-              text: key.toString("hex"),
-              secret: Buffer.from(derivedKey.hash)
-            });
-
-            await resetPasswordMutateAsync({
-              protectedKey,
-              protectedKeyIV,
-              protectedKeyTag,
-              encryptedPrivateKey,
-              encryptedPrivateKeyIV,
-              encryptedPrivateKeyTag,
-              salt: result.salt,
-              verifier: result.verifier,
-              verificationToken,
-              password: newPassword
-            });
-
-            navigate({ to: "/login" });
+  const { watch, setValue } = useForm<TFormData>({
+    resolver: zodResolver(formData)
+  });
 
-            setLoading(false);
-          });
-        }
-      );
-    }
-  };
+  const verificationToken = watch("verificationToken");
+  const encryptionVersion = watch("userEncryptionVersion");
+  const privateKey = watch("privateKey");
 
-  // Click a button to confirm email
-  const stepConfirmEmail = (
-    <div className="mx-1 my-32 flex w-full max-w-xs flex-col items-center rounded-xl bg-bunker px-4 py-6 drop-shadow-xl md:max-w-lg md:px-6">
-      <p className="mb-8 flex justify-center bg-gradient-to-br from-sky-400 to-primary bg-clip-text text-center text-4xl font-semibold text-transparent">
-        Confirm your email
-      </p>
-      <img
-        src="/images/envelope.svg"
-        style={{ height: "262px", width: "410px" }}
-        alt="verify email"
-      />
-      <div className="mx-auto mb-2 mt-4 flex max-h-24 max-w-md flex-col items-center justify-center px-4 text-lg md:p-2">
-        <Button
-          onClick={async () => {
-            try {
-              const response = await verifyPasswordResetCodeMutateAsync({
-                email,
-                code: token
-              });
+  const [step, setStep] = useState<Steps>(Steps.ConfirmEmail);
+  const navigate = useNavigate();
 
-              setVerificationToken(response.token);
-              setStep(2);
-            } catch (err) {
-              console.log("ERROR", err);
-              navigate({ to: "/email-not-verified" });
+  return (
+    <div className="flex h-screen w-full flex-col items-center justify-center bg-bunker-800">
+      {step === Steps.ConfirmEmail && (
+        <ConfirmEmailStep
+          onComplete={(verifyToken, userEncryptionVersion) => {
+            setValue("verificationToken", verifyToken);
+            setValue("userEncryptionVersion", userEncryptionVersion);
+
+            if (userEncryptionVersion === UserEncryptionVersion.V2) {
+              setStep(Steps.EnterNewPassword);
+            } else {
+              setStep(Steps.InputBackupKey);
             }
           }}
-          isLoading={isVerifyPasswordResetLoading}
-          size="lg"
-        >
-          Confirm Email
-        </Button>
-      </div>
-    </div>
-  );
-
-  // Input backup key
-  const stepInputBackupKey = (
-    <form
-      onSubmit={getEncryptedKeyHandler}
-      className="mx-1 my-32 flex w-full max-w-xs flex-col items-center rounded-xl bg-bunker px-4 pb-3 pt-6 drop-shadow-xl md:max-w-lg md:px-6"
-    >
-      <p className="mx-auto mb-4 flex w-max justify-center text-2xl font-semibold text-bunker-100 md:text-3xl">
-        Enter your backup key
-      </p>
-      <div className="mt-4 flex flex-row items-center justify-center md:mx-2 md:pb-4">
-        <p className="flex w-max max-w-md justify-center text-sm text-gray-400">
-          You can find it in your emergency kit. You had to download the emergency kit during
-          signup.
-        </p>
-      </div>
-      <div className="mt-4 flex max-h-24 w-full items-center justify-center rounded-lg md:mt-0 md:max-h-28 md:p-2">
-        <InputField
-          label="Backup Key"
-          onChangeHandler={setBackupKey}
-          type="password"
-          value={backupKey}
-          placeholder=""
-          isRequired
-          error={backupKeyError}
-          errorText="Something is wrong with the backup key"
         />
-      </div>
-      <div className="mx-auto mt-4 flex max-h-20 w-full max-w-md flex-col items-center justify-center text-sm md:p-2">
-        <div className="text-l m-8 mt-6 px-8 py-3 text-lg">
-          <Button type="submit" size="lg">
-            Submit Backup Key
-          </Button>
-        </div>
-      </div>
-    </form>
-  );
-
-  // Enter new password
-  const stepEnterNewPassword = (
-    <form
-      onSubmit={resetPasswordHandler}
-      className="mx-1 my-32 flex w-full max-w-xs flex-col items-center rounded-xl bg-bunker px-4 pb-3 pt-6 drop-shadow-xl md:max-w-lg md:px-6"
-    >
-      <p className="mx-auto flex w-max justify-center text-2xl font-semibold text-bunker-100 md:text-3xl">
-        Enter new password
-      </p>
-      <div className="mt-1 flex flex-row items-center justify-center md:mx-2 md:pb-4">
-        <p className="flex w-max max-w-md justify-center text-sm text-gray-400">
-          Make sure you save it somewhere safe.
-        </p>
-      </div>
-      <div className="mt-4 flex max-h-24 w-full items-center justify-center rounded-lg md:mt-0 md:max-h-28 md:p-2">
-        <InputField
-          label="New Password"
-          onChangeHandler={(password) => {
-            setNewPassword(password);
-            passwordCheck({
-              password,
-              setPasswordErrorTooShort,
-              setPasswordErrorTooLong,
-              setPasswordErrorNoLetterChar,
-              setPasswordErrorNoNumOrSpecialChar,
-              setPasswordErrorRepeatedChar,
-              setPasswordErrorEscapeChar,
-              setPasswordErrorLowEntropy,
-              setPasswordErrorBreached
-            });
+      )}
+      {step === Steps.InputBackupKey && (
+        <InputBackupKeyStep
+          verificationToken={verificationToken}
+          onComplete={(key) => {
+            setValue("privateKey", key);
+            setStep(Steps.EnterNewPassword);
+          }}
+        />
+      )}
+      {step === Steps.EnterNewPassword && (
+        <EnterPasswordStep
+          verificationToken={verificationToken}
+          privateKey={privateKey}
+          encryptionVersion={encryptionVersion}
+          onComplete={() => {
+            navigate({ to: "/login" });
           }}
-          type="password"
-          value={newPassword}
-          isRequired
-          error={
-            passwordErrorTooShort &&
-            passwordErrorTooLong &&
-            passwordErrorNoLetterChar &&
-            passwordErrorNoNumOrSpecialChar &&
-            passwordErrorRepeatedChar &&
-            passwordErrorEscapeChar &&
-            passwordErrorLowEntropy &&
-            passwordErrorBreached
-          }
-          autoComplete="new-password"
-          id="new-password"
         />
-      </div>
-      {passwordErrorTooShort ||
-      passwordErrorTooLong ||
-      passwordErrorNoLetterChar ||
-      passwordErrorNoNumOrSpecialChar ||
-      passwordErrorRepeatedChar ||
-      passwordErrorEscapeChar ||
-      passwordErrorLowEntropy ||
-      passwordErrorBreached ? (
-        <div className="mx-2 mb-2 mt-3 flex w-full max-w-md flex-col items-start rounded-md bg-white/5 px-2 py-2">
-          <div className="mb-1 text-sm text-gray-400">Password should contain:</div>
-          <div className="ml-1 flex flex-row items-center justify-start">
-            {passwordErrorTooShort ? (
-              <FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
-            ) : (
-              <FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
-            )}
-            <div className={`${passwordErrorTooShort ? "text-gray-400" : "text-gray-600"} text-sm`}>
-              at least 14 characters
-            </div>
-          </div>
-          <div className="ml-1 flex flex-row items-center justify-start">
-            {passwordErrorTooLong ? (
-              <FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
-            ) : (
-              <FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
-            )}
-            <div className={`${passwordErrorTooLong ? "text-gray-400" : "text-gray-600"} text-sm`}>
-              at most 100 characters
-            </div>
-          </div>
-          <div className="ml-1 flex flex-row items-center justify-start">
-            {passwordErrorNoLetterChar ? (
-              <FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
-            ) : (
-              <FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
-            )}
-            <div
-              className={`${passwordErrorNoLetterChar ? "text-gray-400" : "text-gray-600"} text-sm`}
-            >
-              at least 1 letter character
-            </div>
-          </div>
-          <div className="ml-1 flex flex-row items-center justify-start">
-            {passwordErrorNoNumOrSpecialChar ? (
-              <FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
-            ) : (
-              <FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
-            )}
-            <div
-              className={`${
-                passwordErrorNoNumOrSpecialChar ? "text-gray-400" : "text-gray-600"
-              } text-sm`}
-            >
-              at least 1 number or special character
-            </div>
-          </div>
-          <div className="ml-1 flex flex-row items-center justify-start">
-            {passwordErrorRepeatedChar ? (
-              <FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
-            ) : (
-              <FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
-            )}
-            <div
-              className={`${passwordErrorRepeatedChar ? "text-gray-400" : "text-gray-600"} text-sm`}
-            >
-              at most 3 repeated, consecutive characters
-            </div>
-          </div>
-          <div className="ml-1 flex flex-row items-center justify-start">
-            {passwordErrorEscapeChar ? (
-              <FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
-            ) : (
-              <FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
-            )}
-            <div
-              className={`${passwordErrorEscapeChar ? "text-gray-400" : "text-gray-600"} text-sm`}
-            >
-              No escape characters allowed.
-            </div>
-          </div>
-          <div className="ml-1 flex flex-row items-center justify-start">
-            {passwordErrorLowEntropy ? (
-              <FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
-            ) : (
-              <FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
-            )}
-            <div
-              className={`${passwordErrorLowEntropy ? "text-gray-400" : "text-gray-600"} text-sm`}
-            >
-              Password contains personal info.
-            </div>
-          </div>
-          <div className="ml-1 flex flex-row items-center justify-start">
-            {passwordErrorBreached ? (
-              <FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
-            ) : (
-              <FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
-            )}
-            <div className={`${passwordErrorBreached ? "text-gray-400" : "text-gray-600"} text-sm`}>
-              Password was found in a data breach.
-            </div>
-          </div>
-        </div>
-      ) : (
-        <div className="py-2" />
       )}
-      <div className="mx-auto mt-4 flex max-h-20 w-full max-w-md flex-col items-center justify-center text-sm md:p-2">
-        <div className="text-l m-8 mt-6 px-8 py-3 text-lg">
-          <Button type="submit" onClick={() => setLoading(true)} size="lg" isLoading={loading}>
-            Submit New Password
-          </Button>
-        </div>
-      </div>
-    </form>
-  );
-
-  return (
-    <div className="flex h-screen w-full flex-col items-center justify-center bg-bunker-800">
-      {step === 1 && stepConfirmEmail}
-      {step === 2 && stepInputBackupKey}
-      {step === 3 && stepEnterNewPassword}
     </div>
   );
 };
diff --git a/frontend/src/pages/auth/PasswordResetPage/components/ConfirmEmailStep.tsx b/frontend/src/pages/auth/PasswordResetPage/components/ConfirmEmailStep.tsx
new file mode 100644
index 00000000000..1f1a7949038
--- /dev/null
+++ b/frontend/src/pages/auth/PasswordResetPage/components/ConfirmEmailStep.tsx
@@ -0,0 +1,54 @@
+import { useNavigate, useSearch } from "@tanstack/react-router";
+
+import { Button } from "@app/components/v2";
+import { ROUTE_PATHS } from "@app/const/routes";
+import { useVerifyPasswordResetCode } from "@app/hooks/api";
+import { UserEncryptionVersion } from "@app/hooks/api/auth/types";
+
+type Props = {
+  onComplete: (verificationToken: string, encryptionVersion: UserEncryptionVersion) => void;
+};
+
+export const ConfirmEmailStep = ({ onComplete }: Props) => {
+  const navigate = useNavigate();
+  const search = useSearch({ from: ROUTE_PATHS.Auth.PasswordResetPage.id });
+  const { token, to: email } = search;
+
+  const {
+    mutateAsync: verifyPasswordResetCodeMutateAsync,
+    isPending: isVerifyPasswordResetLoading
+  } = useVerifyPasswordResetCode();
+  return (
+    <div className="mx-1 my-32 flex w-full max-w-xs flex-col items-center rounded-xl bg-bunker px-4 py-6 drop-shadow-xl md:max-w-lg md:px-6">
+      <p className="mb-8 flex justify-center bg-gradient-to-br from-sky-400 to-primary bg-clip-text text-center text-4xl font-semibold text-transparent">
+        Confirm your email
+      </p>
+      <img
+        src="/images/envelope.svg"
+        style={{ height: "262px", width: "410px" }}
+        alt="verify email"
+      />
+      <div className="mx-auto mb-2 mt-4 flex max-h-24 max-w-md flex-col items-center justify-center px-4 text-lg md:p-2">
+        <Button
+          onClick={async () => {
+            try {
+              const response = await verifyPasswordResetCodeMutateAsync({
+                email,
+                code: token
+              });
+
+              onComplete(response.token, response.userEncryptionVersion);
+            } catch (err) {
+              console.log("ERROR", err);
+              navigate({ to: "/email-not-verified" });
+            }
+          }}
+          isLoading={isVerifyPasswordResetLoading}
+          size="lg"
+        >
+          Confirm Email
+        </Button>
+      </div>
+    </div>
+  );
+};
diff --git a/frontend/src/pages/auth/PasswordResetPage/components/EnterPasswordStep.tsx b/frontend/src/pages/auth/PasswordResetPage/components/EnterPasswordStep.tsx
new file mode 100644
index 00000000000..de7bcd3f3fa
--- /dev/null
+++ b/frontend/src/pages/auth/PasswordResetPage/components/EnterPasswordStep.tsx
@@ -0,0 +1,325 @@
+import crypto from "crypto";
+
+import { Controller, useForm } from "react-hook-form";
+import { faCheck, faX } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { useSearch } from "@tanstack/react-router";
+import jsrp from "jsrp";
+import { z } from "zod";
+
+import passwordCheck from "@app/components/utilities/checks/password/PasswordCheck";
+import Aes256Gcm from "@app/components/utilities/cryptography/aes-256-gcm";
+import { deriveArgonKey } from "@app/components/utilities/cryptography/crypto";
+import { Button, FormControl, Input } from "@app/components/v2";
+import { ROUTE_PATHS } from "@app/const/routes";
+import { useResetPassword, useResetPasswordV2 } from "@app/hooks/api";
+import { UserEncryptionVersion } from "@app/hooks/api/auth/types";
+
+const formData = z.object({
+  password: z.string(),
+  passwordErrorTooShort: z.boolean().optional(),
+  passwordErrorTooLong: z.boolean().optional(),
+  passwordErrorNoLetterChar: z.boolean().optional(),
+  passwordErrorNoNumOrSpecialChar: z.boolean().optional(),
+  passwordErrorRepeatedChar: z.boolean().optional(),
+  passwordErrorEscapeChar: z.boolean().optional(),
+  passwordErrorLowEntropy: z.boolean().optional(),
+  passwordErrorBreached: z.boolean()
+});
+type TFormData = z.infer<typeof formData>;
+
+type Props = {
+  verificationToken: string;
+  privateKey: string;
+  encryptionVersion: UserEncryptionVersion;
+  onComplete: () => void;
+};
+
+export const EnterPasswordStep = ({
+  verificationToken,
+  encryptionVersion,
+  privateKey,
+  onComplete
+}: Props) => {
+  const search = useSearch({ from: ROUTE_PATHS.Auth.PasswordResetPage.id });
+  const { to: email } = search;
+
+  const {
+    control,
+    watch,
+    handleSubmit,
+    setValue,
+    formState: { isSubmitting }
+  } = useForm<TFormData>({
+    resolver: zodResolver(formData)
+  });
+  const { mutateAsync: resetPassword, isPending: isLoading } = useResetPassword();
+  const { mutateAsync: resetPasswordV2, isPending: isLoadingV2 } = useResetPasswordV2();
+
+  const passwordErrorTooShort = watch("passwordErrorTooShort");
+  const passwordErrorTooLong = watch("passwordErrorTooLong");
+  const passwordErrorNoLetterChar = watch("passwordErrorNoLetterChar");
+  const passwordErrorNoNumOrSpecialChar = watch("passwordErrorNoNumOrSpecialChar");
+  const passwordErrorRepeatedChar = watch("passwordErrorRepeatedChar");
+  const passwordErrorEscapeChar = watch("passwordErrorEscapeChar");
+  const passwordErrorLowEntropy = watch("passwordErrorLowEntropy");
+  const passwordErrorBreached = watch("passwordErrorBreached");
+
+  const isPasswordError =
+    passwordErrorTooShort ||
+    passwordErrorTooLong ||
+    passwordErrorNoLetterChar ||
+    passwordErrorNoNumOrSpecialChar ||
+    passwordErrorRepeatedChar ||
+    passwordErrorEscapeChar ||
+    passwordErrorLowEntropy ||
+    passwordErrorBreached;
+
+  const handlePasswordCheck = async (checkPassword: string) => {
+    const errorCheck = await passwordCheck({
+      password: checkPassword,
+      setPasswordErrorTooShort: (v) => setValue("passwordErrorTooShort", v),
+      setPasswordErrorTooLong: (v) => setValue("passwordErrorTooLong", v),
+      setPasswordErrorNoLetterChar: (v) => setValue("passwordErrorNoLetterChar", v),
+      setPasswordErrorNoNumOrSpecialChar: (v) => setValue("passwordErrorNoNumOrSpecialChar", v),
+      setPasswordErrorRepeatedChar: (v) => setValue("passwordErrorRepeatedChar", v),
+      setPasswordErrorEscapeChar: (v) => setValue("passwordErrorEscapeChar", v),
+      setPasswordErrorLowEntropy: (v) => setValue("passwordErrorLowEntropy", v),
+      setPasswordErrorBreached: (v) => setValue("passwordErrorBreached", v)
+    });
+
+    return errorCheck;
+  };
+
+  const resetPasswordHandler = async (data: TFormData) => {
+    const errorCheck = await handlePasswordCheck(data.password);
+
+    if (errorCheck) return;
+
+    if (encryptionVersion === UserEncryptionVersion.V2) {
+      await resetPasswordV2({
+        newPassword: data.password,
+        verificationToken
+      });
+    } else {
+      // eslint-disable-next-line new-cap
+      const client = new jsrp.client();
+      client.init(
+        {
+          username: email,
+          password: data.password
+        },
+        async () => {
+          client.createVerifier(async (_err: any, result: { salt: string; verifier: string }) => {
+            const derivedKey = await deriveArgonKey({
+              password: data.password,
+              salt: result.salt,
+              mem: 65536,
+              time: 3,
+              parallelism: 1,
+              hashLen: 32
+            });
+
+            if (!derivedKey) throw new Error("Failed to derive key from password");
+
+            const key = crypto.randomBytes(32);
+
+            // create encrypted private key by encrypting the private
+            // key with the symmetric key [key]
+            const {
+              ciphertext: encryptedPrivateKey,
+              iv: encryptedPrivateKeyIV,
+              tag: encryptedPrivateKeyTag
+            } = Aes256Gcm.encrypt({
+              text: privateKey,
+              secret: key
+            });
+
+            // create the protected key by encrypting the symmetric key
+            // [key] with the derived key
+            const {
+              ciphertext: protectedKey,
+              iv: protectedKeyIV,
+              tag: protectedKeyTag
+            } = Aes256Gcm.encrypt({
+              text: key.toString("hex"),
+              secret: Buffer.from(derivedKey.hash)
+            });
+
+            await resetPassword({
+              protectedKey,
+              protectedKeyIV,
+              protectedKeyTag,
+              encryptedPrivateKey,
+              encryptedPrivateKeyIV,
+              encryptedPrivateKeyTag,
+              salt: result.salt,
+              verifier: result.verifier,
+              verificationToken,
+              password: data.password
+            });
+          });
+        }
+      );
+    }
+    onComplete();
+  };
+
+  return (
+    <form
+      onSubmit={handleSubmit(resetPasswordHandler)}
+      className="mx-1 my-32 flex w-full max-w-xs flex-col items-center rounded-xl bg-bunker px-4 pb-3 pt-6 drop-shadow-xl md:max-w-lg md:px-6"
+    >
+      <p className="mx-auto flex w-max justify-center text-2xl font-semibold text-bunker-100 md:text-3xl">
+        Enter new password
+      </p>
+      <div className="mt-1 flex flex-row items-center justify-center md:mx-2 md:pb-4">
+        <p className="flex w-max max-w-md justify-center text-sm text-gray-400">
+          Make sure you save it somewhere safe.
+        </p>
+      </div>
+      <div className="mt-4 flex max-h-24 w-full items-center justify-center rounded-lg md:mt-0 md:max-h-28 md:p-2">
+        <Controller
+          control={control}
+          name="password"
+          render={({ field }) => (
+            <FormControl
+              className="w-full"
+              label="New Password"
+              isRequired
+              isError={isPasswordError}
+            >
+              <Input
+                {...field}
+                onChange={(e) => {
+                  field.onChange(e);
+                  handlePasswordCheck(e.target.value);
+                }}
+                type="password"
+              />
+            </FormControl>
+          )}
+        />
+      </div>
+      {passwordErrorTooShort ||
+      passwordErrorTooLong ||
+      passwordErrorNoLetterChar ||
+      passwordErrorNoNumOrSpecialChar ||
+      passwordErrorRepeatedChar ||
+      passwordErrorEscapeChar ||
+      passwordErrorLowEntropy ||
+      passwordErrorBreached ? (
+        <div className="mx-2 mb-2 mt-3 flex w-full max-w-md flex-col items-start rounded-md bg-white/5 px-2 py-2">
+          <div className="mb-1 text-sm text-gray-400">Password should contain:</div>
+          <div className="ml-1 flex flex-row items-center justify-start">
+            {passwordErrorTooShort ? (
+              <FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
+            ) : (
+              <FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
+            )}
+            <div className={`${passwordErrorTooShort ? "text-gray-400" : "text-gray-600"} text-sm`}>
+              at least 14 characters
+            </div>
+          </div>
+          <div className="ml-1 flex flex-row items-center justify-start">
+            {passwordErrorTooLong ? (
+              <FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
+            ) : (
+              <FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
+            )}
+            <div className={`${passwordErrorTooLong ? "text-gray-400" : "text-gray-600"} text-sm`}>
+              at most 100 characters
+            </div>
+          </div>
+          <div className="ml-1 flex flex-row items-center justify-start">
+            {passwordErrorNoLetterChar ? (
+              <FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
+            ) : (
+              <FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
+            )}
+            <div
+              className={`${passwordErrorNoLetterChar ? "text-gray-400" : "text-gray-600"} text-sm`}
+            >
+              at least 1 letter character
+            </div>
+          </div>
+          <div className="ml-1 flex flex-row items-center justify-start">
+            {passwordErrorNoNumOrSpecialChar ? (
+              <FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
+            ) : (
+              <FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
+            )}
+            <div
+              className={`${
+                passwordErrorNoNumOrSpecialChar ? "text-gray-400" : "text-gray-600"
+              } text-sm`}
+            >
+              at least 1 number or special character
+            </div>
+          </div>
+          <div className="ml-1 flex flex-row items-center justify-start">
+            {passwordErrorRepeatedChar ? (
+              <FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
+            ) : (
+              <FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
+            )}
+            <div
+              className={`${passwordErrorRepeatedChar ? "text-gray-400" : "text-gray-600"} text-sm`}
+            >
+              at most 3 repeated, consecutive characters
+            </div>
+          </div>
+          <div className="ml-1 flex flex-row items-center justify-start">
+            {passwordErrorEscapeChar ? (
+              <FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
+            ) : (
+              <FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
+            )}
+            <div
+              className={`${passwordErrorEscapeChar ? "text-gray-400" : "text-gray-600"} text-sm`}
+            >
+              No escape characters allowed.
+            </div>
+          </div>
+          <div className="ml-1 flex flex-row items-center justify-start">
+            {passwordErrorLowEntropy ? (
+              <FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
+            ) : (
+              <FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
+            )}
+            <div
+              className={`${passwordErrorLowEntropy ? "text-gray-400" : "text-gray-600"} text-sm`}
+            >
+              Password contains personal info.
+            </div>
+          </div>
+          <div className="ml-1 flex flex-row items-center justify-start">
+            {passwordErrorBreached ? (
+              <FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
+            ) : (
+              <FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
+            )}
+            <div className={`${passwordErrorBreached ? "text-gray-400" : "text-gray-600"} text-sm`}>
+              Password was found in a data breach.
+            </div>
+          </div>
+        </div>
+      ) : (
+        <div className="py-2" />
+      )}
+      <div className="mx-auto mt-4 flex max-h-20 w-full max-w-md flex-col items-center justify-center text-sm md:p-2">
+        <div className="text-l m-8 mt-6 px-8 py-3 text-lg">
+          <Button
+            type="submit"
+            colorSchema="secondary"
+            isLoading={isSubmitting || isLoading || isLoadingV2}
+            isDisabled={isSubmitting || isLoading || isLoadingV2}
+          >
+            Change Password
+          </Button>
+        </div>
+      </div>
+    </form>
+  );
+};
diff --git a/frontend/src/pages/auth/PasswordResetPage/components/InputBackupKeyStep.tsx b/frontend/src/pages/auth/PasswordResetPage/components/InputBackupKeyStep.tsx
new file mode 100644
index 00000000000..54a5d5e9d9c
--- /dev/null
+++ b/frontend/src/pages/auth/PasswordResetPage/components/InputBackupKeyStep.tsx
@@ -0,0 +1,87 @@
+import { Controller, useForm } from "react-hook-form";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { z } from "zod";
+
+import Aes256Gcm from "@app/components/utilities/cryptography/aes-256-gcm";
+import { Button, FormControl, Input } from "@app/components/v2";
+import { getBackupEncryptedPrivateKey } from "@app/hooks/api/auth/queries";
+
+type Props = {
+  verificationToken: string;
+  onComplete: (privateKey: string) => void;
+};
+
+const formData = z.object({
+  backupKey: z.string()
+});
+type TFormData = z.infer<typeof formData>;
+
+export const InputBackupKeyStep = ({ verificationToken, onComplete }: Props) => {
+  const { control, handleSubmit, setError } = useForm<TFormData>({
+    resolver: zodResolver(formData)
+  });
+
+  const getEncryptedKeyHandler = async (data: z.infer<typeof formData>) => {
+    try {
+      const result = await getBackupEncryptedPrivateKey({ verificationToken });
+
+      const privateKey = Aes256Gcm.decrypt({
+        ciphertext: result.encryptedPrivateKey,
+        iv: result.iv,
+        tag: result.tag,
+        secret: data.backupKey
+      });
+
+      onComplete(privateKey);
+      // setStep(3);
+    } catch (err) {
+      console.error(err);
+      setError("backupKey", { message: "Failed to decrypt private key" });
+    }
+  };
+
+  return (
+    <form
+      onSubmit={handleSubmit(getEncryptedKeyHandler)}
+      className="mx-1 my-32 flex w-full max-w-xs flex-col items-center rounded-xl bg-bunker px-4 pb-3 pt-6 drop-shadow-xl md:max-w-lg md:px-6"
+    >
+      <p className="mx-auto mb-4 flex w-max justify-center text-2xl font-semibold text-bunker-100">
+        Enter your backup key
+      </p>
+      <div className="mt-4 flex flex-row items-center justify-center md:mx-2 md:pb-4">
+        <p className="flex w-full px-4 text-center text-sm text-gray-400 sm:max-w-md">
+          You can find it in your emergency kit. You had to download the emergency kit during
+          signup.
+        </p>
+      </div>
+      <div className="mt-4 flex max-h-24 w-full items-center justify-center rounded-lg md:mt-0 md:max-h-28 md:p-2">
+        <Controller
+          control={control}
+          name="backupKey"
+          render={({ field: { value, onChange }, fieldState: { error } }) => (
+            <FormControl
+              className="w-full"
+              isError={Boolean(error)}
+              errorText={error?.message}
+              label="Backup Key"
+            >
+              <Input
+                className="w-full"
+                value={value}
+                onChange={onChange}
+                placeholder="08af467b815ffa412f2c98cc3326acdb"
+              />
+            </FormControl>
+          )}
+        />
+      </div>
+      <div className="mx-auto mt-4 flex max-h-20 w-full max-w-md flex-col items-center justify-center text-sm md:p-2">
+        <div className="text-l m-8 mt-6 px-8 py-3 text-lg">
+          <Button type="submit" colorSchema="secondary">
+            Submit Backup Key
+          </Button>
+        </div>
+      </div>
+    </form>
+  );
+};
diff --git a/frontend/src/pages/auth/SignUpInvitePage/SignUpInvitePage.tsx b/frontend/src/pages/auth/SignUpInvitePage/SignUpInvitePage.tsx
index d3106e341dd..7cf4ad57222 100644
--- a/frontend/src/pages/auth/SignUpInvitePage/SignUpInvitePage.tsx
+++ b/frontend/src/pages/auth/SignUpInvitePage/SignUpInvitePage.tsx
@@ -4,7 +4,7 @@ import crypto from "crypto";
 
 import { useState } from "react";
 import { Helmet } from "react-helmet";
-import { faWarning, faXmark } from "@fortawesome/free-solid-svg-icons";
+import { faXmark } from "@fortawesome/free-solid-svg-icons";
 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
 import { Link, useNavigate, useSearch } from "@tanstack/react-router";
 import jsrp from "jsrp";
@@ -16,7 +16,6 @@ import InputField from "@app/components/basic/InputField";
 import checkPassword from "@app/components/utilities/checks/password/checkPassword";
 import Aes256Gcm from "@app/components/utilities/cryptography/aes-256-gcm";
 import { deriveArgonKey } from "@app/components/utilities/cryptography/crypto";
-import issueBackupKey from "@app/components/utilities/cryptography/issueBackupKey";
 import { saveTokenToLocalStorage } from "@app/components/utilities/saveTokenToLocalStorage";
 import SecurityClient from "@app/components/utilities/SecurityClient";
 import { Button } from "@app/components/v2";
@@ -54,8 +53,6 @@ export const SignupInvitePage = () => {
   const [lastNameError, setLastNameError] = useState(false);
   const [isLoading, setIsLoading] = useState(false);
   const [step, setStep] = useState(1);
-  const [, setBackupKeyError] = useState(false);
-  const [, setBackupKeyIssued] = useState(false);
   const [errors, setErrors] = useState<Errors>({});
 
   const [shouldShowMfa, toggleShowMfa] = useToggle(false);
@@ -205,7 +202,9 @@ export const SignupInvitePage = () => {
 
                 localStorage.setItem("orgData.id", orgId);
 
-                setStep(3);
+                navigate({
+                  to: `/organization/${ProjectType.SecretManager}/overview` as const
+                });
               };
 
               await completeSignupFlow();
@@ -367,44 +366,6 @@ export const SignupInvitePage = () => {
     </div>
   );
 
-  // Step 4 of the sign up process (download the emergency kit pdf)
-  const step4 = (
-    <div className="h-7/12 mx-1 mb-36 flex w-full max-w-xs flex-col items-center rounded-xl border border-mineshaft-600 bg-mineshaft-800 px-4 pb-6 pt-8 drop-shadow-xl md:mb-16 md:max-w-lg md:px-6">
-      <p className="flex justify-center bg-gradient-to-br from-white to-mineshaft-300 bg-clip-text text-center text-4xl font-semibold text-transparent">
-        Save your Emergency Kit
-      </p>
-      <div className="text-md mt-4 flex w-full max-w-md flex-col items-center justify-center rounded-md px-2 text-gray-400 md:mt-8">
-        <div>
-          If you get locked out of your account, your Emergency Kit is the only way to sign in.
-        </div>
-        <div className="mt-3">We recommend you download it and keep it somewhere safe.</div>
-      </div>
-      <div className="mx-auto mt-4 flex w-full max-w-xs flex-row items-center rounded-md bg-white/10 p-2 text-gray-400 md:max-w-md">
-        <FontAwesomeIcon icon={faWarning} className="ml-2 mr-4 text-4xl" />
-        It contains your Secret Key which we cannot access or recover for you if you lose it.
-      </div>
-      <div className="mx-auto mt-4 flex max-h-24 max-w-max flex-col items-center justify-center px-2 py-3 text-lg md:px-4 md:py-5">
-        <Button
-          onClick={async () => {
-            await issueBackupKey({
-              email,
-              password,
-              personalName: `${firstName} ${lastName}`,
-              setBackupKeyError,
-              setBackupKeyIssued
-            });
-            navigate({
-              to: `/organization/${ProjectType.SecretManager}/overview` as const
-            });
-          }}
-          size="lg"
-        >
-          Download PDF
-        </Button>
-      </div>
-    </div>
-  );
-
   return (
     <div className="flex h-screen flex-col items-center justify-center bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700">
       <Helmet>
@@ -425,7 +386,8 @@ export const SignupInvitePage = () => {
               <img src="/images/gradientLogo.svg" height={90} width={120} alt="Infisical Logo" />
             </div>
           </Link>
-          {step === 1 ? stepConfirmEmail : step === 2 ? main : step4}
+          {step === 1 && stepConfirmEmail}
+          {step === 2 && main}
         </>
       )}
     </div>
diff --git a/frontend/src/pages/auth/SignUpPage/SignUpPage.tsx b/frontend/src/pages/auth/SignUpPage/SignUpPage.tsx
index c1fcc30c4b4..75af4ff486c 100644
--- a/frontend/src/pages/auth/SignUpPage/SignUpPage.tsx
+++ b/frontend/src/pages/auth/SignUpPage/SignUpPage.tsx
@@ -5,7 +5,6 @@ import { useTranslation } from "react-i18next";
 import { useNavigate } from "@tanstack/react-router";
 
 import CodeInputStep from "@app/components/auth/CodeInputStep";
-import DownloadBackupPDF from "@app/components/auth/DonwloadBackupPDFStep";
 import EnterEmailStep from "@app/components/auth/EnterEmailStep";
 import InitialSignupStep from "@app/components/auth/InitialSignupStep";
 import TeamInviteStep from "@app/components/auth/TeamInviteStep";
@@ -72,7 +71,7 @@ export const SignUpPage = () => {
         incrementStep();
       }
 
-      if (!serverDetails?.emailConfigured && step === 5) {
+      if (!serverDetails?.emailConfigured && step === 4) {
         navigate({
           to: `/organization/${ProjectType.SecretManager}/overview` as const
         });
@@ -119,17 +118,6 @@ export const SignUpPage = () => {
       );
     }
 
-    if (registerStep === 4) {
-      return (
-        <DownloadBackupPDF
-          incrementStep={incrementStep}
-          email={email}
-          password={password}
-          name={name}
-        />
-      );
-    }
-
     if (serverDetails?.emailConfigured) {
       return <TeamInviteStep />;
     }
diff --git a/frontend/src/pages/auth/SignUpSsoPage/SignUpSsoPage.tsx b/frontend/src/pages/auth/SignUpSsoPage/SignUpSsoPage.tsx
index a13cfcfc676..40075ae547c 100644
--- a/frontend/src/pages/auth/SignUpSsoPage/SignUpSsoPage.tsx
+++ b/frontend/src/pages/auth/SignUpSsoPage/SignUpSsoPage.tsx
@@ -6,7 +6,6 @@ import { jwtDecode } from "jwt-decode";
 
 import { ROUTE_PATHS } from "@app/const/routes";
 
-import { BackupPDFStep } from "./components/BackupPDFStep";
 import { EmailConfirmationStep } from "./components/EmailConfirmationStep";
 import { UserInfoSSOStep } from "./components/UserInfoSSOStep";
 
@@ -57,14 +56,9 @@ export const SignupSsoPage = () => {
             providerOrganizationName={organizationName}
             password={password}
             setPassword={setPassword}
-            setStep={setStep}
             providerAuthToken={token}
           />
         );
-      case 2:
-        return (
-          <BackupPDFStep email={username} password={password} name={`${firstName} ${lastName}`} />
-        );
       default:
         return <div />;
     }
diff --git a/frontend/src/pages/auth/SignUpSsoPage/components/BackupPDFStep/BackupPDFStep.tsx b/frontend/src/pages/auth/SignUpSsoPage/components/BackupPDFStep/BackupPDFStep.tsx
deleted file mode 100644
index 9a236917055..00000000000
--- a/frontend/src/pages/auth/SignUpSsoPage/components/BackupPDFStep/BackupPDFStep.tsx
+++ /dev/null
@@ -1,71 +0,0 @@
-import { useTranslation } from "react-i18next";
-import { faWarning } from "@fortawesome/free-solid-svg-icons";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { useNavigate } from "@tanstack/react-router";
-
-import issueBackupKey from "@app/components/utilities/cryptography/issueBackupKey";
-import { Button } from "@app/components/v2";
-import { ProjectType } from "@app/hooks/api/workspace/types";
-
-interface DownloadBackupPDFStepProps {
-  email: string;
-  password: string;
-  name: string;
-}
-
-/**
- * This is the step of the signup flow where the user downloads the backup pdf
- * @param {object} obj
- * @param {function} obj.incrementStep - function that moves the user on to the next stage of signup
- * @param {string} obj.email - user's email
- * @param {string} obj.password - user's password
- * @param {string} obj.name - user's name
- * @returns
- */
-export const BackupPDFStep = ({ email, password, name }: DownloadBackupPDFStepProps) => {
-  const { t } = useTranslation();
-  const navigate = useNavigate();
-
-  return (
-    <div className="mx-auto mb-36 flex h-full w-full flex-col items-center md:mb-16 md:px-6">
-      <p className="flex justify-center bg-gradient-to-b from-white to-bunker-200 bg-clip-text text-center text-xl font-medium text-transparent">
-        <FontAwesomeIcon icon={faWarning} className="ml-2 mr-3 pt-1 text-2xl text-bunker-200" />
-        {t("signup.step4-message")}
-      </p>
-      <div className="text-md mt-8 flex w-full max-w-md flex-col items-center justify-center rounded-md border border-mineshaft-600 bg-mineshaft-800 pb-2 text-center text-bunker-300 md:min-w-[24rem] lg:w-1/6">
-        <div className="m-2 mx-auto mt-4 flex w-full flex-row items-center rounded-md px-3 text-center text-bunker-300 md:mt-8 md:min-w-[23rem] lg:w-1/6">
-          <span className="mb-2">
-            {t("signup.step4-description1")} {t("signup.step4-description3")}
-          </span>
-        </div>
-        <div className="mx-auto mb-2 mt-2 flex w-full flex-col items-center justify-center px-3 text-center text-sm md:mb-4 md:mt-4 md:min-w-[20rem] md:max-w-md md:text-left lg:w-1/6">
-          <div className="text-l w-full py-1 text-lg">
-            <Button
-              onClick={async () => {
-                await issueBackupKey({
-                  email,
-                  password,
-                  personalName: name,
-                  setBackupKeyError: () => {},
-                  setBackupKeyIssued: () => {}
-                });
-
-                navigate({
-                  to: `/organization/${ProjectType.SecretManager}/overview` as const
-                });
-              }}
-              size="sm"
-              isFullWidth
-              className="h-12"
-              colorSchema="primary"
-              variant="outline_bg"
-            >
-              {" "}
-              Download PDF{" "}
-            </Button>
-          </div>
-        </div>
-      </div>
-    </div>
-  );
-};
diff --git a/frontend/src/pages/auth/SignUpSsoPage/components/BackupPDFStep/index.tsx b/frontend/src/pages/auth/SignUpSsoPage/components/BackupPDFStep/index.tsx
deleted file mode 100644
index 01f7745cda9..00000000000
--- a/frontend/src/pages/auth/SignUpSsoPage/components/BackupPDFStep/index.tsx
+++ /dev/null
@@ -1 +0,0 @@
-export { BackupPDFStep } from "./BackupPDFStep";
diff --git a/frontend/src/pages/auth/SignUpSsoPage/components/UserInfoSSOStep/UserInfoSSOStep.tsx b/frontend/src/pages/auth/SignUpSsoPage/components/UserInfoSSOStep/UserInfoSSOStep.tsx
index c3d131708cc..a6ab0623b73 100644
--- a/frontend/src/pages/auth/SignUpSsoPage/components/UserInfoSSOStep/UserInfoSSOStep.tsx
+++ b/frontend/src/pages/auth/SignUpSsoPage/components/UserInfoSSOStep/UserInfoSSOStep.tsx
@@ -2,6 +2,7 @@ import crypto from "crypto";
 
 import { useEffect, useState } from "react";
 import { useTranslation } from "react-i18next";
+import { useNavigate } from "@tanstack/react-router";
 import jsrp from "jsrp";
 import nacl from "tweetnacl";
 import { encodeBase64 } from "tweetnacl-util";
@@ -17,12 +18,12 @@ import { useToggle } from "@app/hooks";
 import { completeAccountSignup, useSelectOrganization } from "@app/hooks/api/auth/queries";
 import { MfaMethod } from "@app/hooks/api/auth/types";
 import { fetchOrganizations } from "@app/hooks/api/organization/queries";
+import { ProjectType } from "@app/hooks/api/workspace/types";
 
 // eslint-disable-next-line new-cap
 const client = new jsrp.client();
 
 type Props = {
-  setStep: (step: number) => void;
   username: string;
   password: string;
   setPassword: (value: string) => void;
@@ -50,7 +51,6 @@ export const UserInfoSSOStep = ({
   providerOrganizationName,
   password,
   setPassword,
-  setStep,
   providerAuthToken
 }: Props) => {
   const [nameError, setNameError] = useState(false);
@@ -63,6 +63,7 @@ export const UserInfoSSOStep = ({
   const { t } = useTranslation();
   const { mutateAsync: selectOrganization } = useSelectOrganization();
   const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {});
+  const navigate = useNavigate();
 
   useEffect(() => {
     const randomPassword = crypto.randomBytes(32).toString("hex");
@@ -202,7 +203,9 @@ export const UserInfoSSOStep = ({
                   }
 
                   localStorage.setItem("orgData.id", orgId);
-                  setStep(2);
+                  navigate({
+                    to: `/organization/${ProjectType.SecretManager}/overview` as const
+                  });
                 } catch (error) {
                   setIsLoading(false);
                   console.error(error);
diff --git a/frontend/src/pages/auth/VerifyEmailPage/VerifyEmailPage.tsx b/frontend/src/pages/auth/VerifyEmailPage/VerifyEmailPage.tsx
index 418529bdcc9..47012e2cb3e 100644
--- a/frontend/src/pages/auth/VerifyEmailPage/VerifyEmailPage.tsx
+++ b/frontend/src/pages/auth/VerifyEmailPage/VerifyEmailPage.tsx
@@ -72,8 +72,9 @@ export const VerifyEmailPage = () => {
             Forgot your password?
           </p>
           <div className="mt-4 flex flex-row items-center justify-center md:mx-2 md:pb-4">
-            <p className="flex w-max justify-center text-sm text-gray-400">
-              You will need your emergency kit. Enter your email to start account recovery.
+            <p className="flex w-max justify-center text-center text-sm text-gray-400">
+              Enter your email to start the password reset process. You will receive an email with
+              instructions.
             </p>
           </div>
           <div className="mt-4 flex max-h-24 w-full items-center justify-center rounded-lg md:mt-0 md:max-h-28 md:p-2">
@@ -102,8 +103,10 @@ export const VerifyEmailPage = () => {
             Look for an email in your inbox.
           </p>
           <div className="mt-4 flex flex-row items-center justify-center md:mx-2 md:pb-4">
-            <p className="flex w-max justify-center text-center text-sm text-gray-400">
-              An email with instructions has been sent to {email}.
+            <p className="w-max text-center text-sm text-gray-400">
+              If the email is in our system, you will receive an email at{" "}
+              <span className="italic">{email}</span> with instructions on how to reset your
+              password.
             </p>
           </div>
         </div>
diff --git a/frontend/src/pages/user/PersonalSettingsPage/components/ChangePasswordSection/ChangePasswordSection.tsx b/frontend/src/pages/user/PersonalSettingsPage/components/ChangePasswordSection/ChangePasswordSection.tsx
index 9c34693df48..d6842c755b0 100644
--- a/frontend/src/pages/user/PersonalSettingsPage/components/ChangePasswordSection/ChangePasswordSection.tsx
+++ b/frontend/src/pages/user/PersonalSettingsPage/components/ChangePasswordSection/ChangePasswordSection.tsx
@@ -11,7 +11,9 @@ import attemptChangePassword from "@app/components/utilities/attemptChangePasswo
 import checkPassword from "@app/components/utilities/checks/password/checkPassword";
 import { Button, FormControl, Input } from "@app/components/v2";
 import { useUser } from "@app/context";
-import { useSendPasswordSetupEmail } from "@app/hooks/api/auth/queries";
+import { useResetUserPasswordV2, useSendPasswordSetupEmail } from "@app/hooks/api/auth/queries";
+import { UserEncryptionVersion } from "@app/hooks/api/auth/types";
+import { useNavigate } from "@tanstack/react-router";
 
 type Errors = {
   tooShort?: string;
@@ -35,6 +37,7 @@ export type FormData = z.infer<typeof schema>;
 
 export const ChangePasswordSection = () => {
   const { t } = useTranslation();
+  const navigate = useNavigate();
 
   const { user } = useUser();
   const { reset, control, handleSubmit } = useForm({
@@ -47,6 +50,7 @@ export const ChangePasswordSection = () => {
   const [errors, setErrors] = useState<Errors>({});
   const [isLoading, setIsLoading] = useState(false);
   const sendSetupPasswordEmail = useSendPasswordSetupEmail();
+  const { mutateAsync: resetPasswordV2 } = useResetUserPasswordV2();
 
   const onFormSubmit = async ({ oldPassword, newPassword }: FormData) => {
     try {
@@ -56,13 +60,20 @@ export const ChangePasswordSection = () => {
       });
 
       if (errorCheck) return;
-
       setIsLoading(true);
-      await attemptChangePassword({
-        email: user.username,
-        currentPassword: oldPassword,
-        newPassword
-      });
+
+      if (user.encryptionVersion === UserEncryptionVersion.V2) {
+        await resetPasswordV2({
+          oldPassword,
+          newPassword
+        });
+      } else {
+        await attemptChangePassword({
+          email: user.username,
+          currentPassword: oldPassword,
+          newPassword
+        });
+      }
 
       setIsLoading(false);
       createNotification({
@@ -71,7 +82,7 @@ export const ChangePasswordSection = () => {
       });
 
       reset();
-      window.location.href = "/login";
+      navigate({ to: "/login" });
     } catch (err) {
       console.error(err);
       setIsLoading(false);
diff --git a/frontend/src/pages/user/PersonalSettingsPage/components/PersonalGeneralTab/PersonalGeneralTab.tsx b/frontend/src/pages/user/PersonalSettingsPage/components/PersonalGeneralTab/PersonalGeneralTab.tsx
index ac4c5bea2cb..dcf35e67266 100644
--- a/frontend/src/pages/user/PersonalSettingsPage/components/PersonalGeneralTab/PersonalGeneralTab.tsx
+++ b/frontend/src/pages/user/PersonalSettingsPage/components/PersonalGeneralTab/PersonalGeneralTab.tsx
@@ -1,14 +1,20 @@
+import { useUser } from "@app/context";
+import { UserEncryptionVersion } from "@app/hooks/api/auth/types";
+
 import { DeleteAccountSection } from "../DeleteAccountSection";
 import { EmergencyKitSection } from "../EmergencyKitSection";
 import { SessionsSection } from "../SessionsSection";
 import { UserNameSection } from "../UserNameSection";
 
 export const PersonalGeneralTab = () => {
+  const { user } = useUser();
+  const encryptionVersion = user?.encryptionVersion ?? UserEncryptionVersion.V2;
+
   return (
     <div>
       <UserNameSection />
       <SessionsSection />
-      <EmergencyKitSection />
+      {encryptionVersion === UserEncryptionVersion.V1 && <EmergencyKitSection />}
       <DeleteAccountSection />
     </div>
   );


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 3**

<details>
<summary>🔭 Outside diff range comments (1)</summary><blockquote>

<details>
<summary>backend/src/server/routes/v1/password-router.ts (1)</summary><blockquote>

`95-101`: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Consider implementing constant time response for password reset email request**

According to the PR objectives, a timing attack vulnerability was addressed in the email password reset endpoint by implementing a fixed delay. However, I don't see this implementation in the code. If not already implemented in the service layer, consider adding a fixed delay to this handler.

---

🏁 Script executed:

```shell
#!/bin/bash
# Check if a fixed delay is implemented in the password service
rg -A 10 "sendPasswordResetEmail" --glob "*.ts" | grep -E "delay|setTimeout|wait"
```

Length of output: 426

---



</details>

**Implement a fixed delay for the password reset request**

It appears that no fixed delay mechanism (e.g., using `setTimeout`, a promise-based wait, or similar) is implemented either in the endpoint handler or in the underlying `sendPasswordResetEmail` service, as verified by the search command output. This missing delay could leave the endpoint vulnerable to timing attacks. Please add a constant delay—either in the service layer or directly in the router—to ensure that the response time remains constant regardless of whether the email exists.

- **Location:** `backend/src/server/routes/v1/password-router.ts` (lines 95-101)
- **Context:** The delay should be implemented after calling `await server.services.password.sendPasswordResetEmail(req.body.email)` and before returning the response.

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (12)</summary><blockquote>

<details>
<summary>frontend/src/pages/auth/SignUpPage/SignUpPage.tsx (1)</summary><blockquote>

`41-48`: **Update code comments to reflect the new signup flow**

The comments still describe a 5-step signup process with step 4 being "downloading a backup pdf", but this step has been removed from the actual implementation. The comments should be updated to match the new 4-step process.


```diff
 /**
- * Goes to the following step (out of 5) of the signup process.
+ * Goes to the following step (out of 4) of the signup process.
  *
  * Step 1 is submitting your email
  * Step 2 is Verifying your email with the code that you received
  * Step 3 is asking the final info.
- * Step 4 is downloading a backup pdf
- * Step 5 is inviting users
+ * Step 4 is inviting users
  */
```

</blockquote></details>
<details>
<summary>frontend/src/pages/user/PersonalSettingsPage/components/ChangePasswordSection/ChangePasswordSection.tsx (1)</summary><blockquote>

`83-83`: **Consider improving the post-password-change experience.**

After password change, the code redirects users to the login page. Consider keeping users logged in for a better experience, perhaps using token refresh or session extension mechanisms.

```diff
-      window.location.href = "/login";
+      // Refresh user session with new credentials instead of full redirect
+      // This could involve token refresh or other session management
+      // For now, we'll keep the user logged in
```

</blockquote></details>
<details>
<summary>backend/src/services/auth/auth-fns.ts (1)</summary><blockquote>

`67-69`: **Consider validating token type for password reset authorization.**

Unlike `validateSignUpAuthorization` which verifies the `authTokenType` (line 44), this function doesn't check the token type after decoding. Consider adding validation to ensure the token was specifically issued for password reset operations.

```diff
  const decodedToken = jwt.verify(AUTH_TOKEN_VALUE, appCfg.AUTH_SECRET) as AuthModeProviderSignUpTokenPayload;

+  if (decodedToken.authTokenType !== AuthTokenType.PASSWORD_RESET_TOKEN) throw new UnauthorizedError();
+
  return decodedToken;
```

</blockquote></details>
<details>
<summary>frontend/src/pages/auth/PasswordResetPage/components/InputBackupKeyStep.tsx (1)</summary><blockquote>

`36-36`: **Remove commented-out code**

There's a commented-out line `// setStep(3);` that should be removed since it's not serving any purpose and could cause confusion for future developers.


```diff
 onComplete(privateKey);
- // setStep(3);
```

</blockquote></details>
<details>
<summary>frontend/src/pages/auth/PasswordResetPage/components/ConfirmEmailStep.tsx (1)</summary><blockquote>

`33-44`: **Improve error handling and messaging**

The error handling could be improved to provide more meaningful feedback to users:

1. The error is just logged as "ERROR" in all caps, which isn't consistent with typical logging patterns
2. There's no user-facing error message displayed


```diff
  try {
    const response = await verifyPasswordResetCodeMutateAsync({
      email,
      code: token
    });

    onComplete(response.token, response.userEncryptionVersion);
  } catch (err) {
-   console.log("ERROR", err);
+   console.error("Failed to verify password reset code:", err);
    navigate({ to: "/email-not-verified" });
  }
```

</blockquote></details>
<details>
<summary>frontend/src/pages/admin/SignUpPage/SignUpPage.tsx (1)</summary><blockquote>

`77-81`: **Consider more specific error messages**

The error message has been changed to a generic "Failed to create admin" which could make troubleshooting more difficult. Consider retaining more specific error information to help users understand what went wrong.


```diff
  } catch (err) {
    console.log(err);
    createNotification({
      type: "error",
-     text: "Failed to create admin"
+     text: err instanceof Error ? err.message : "Failed to create admin"
    });
  }
```

</blockquote></details>
<details>
<summary>frontend/src/hooks/api/auth/queries.tsx (1)</summary><blockquote>

`311-321`: **Consider error handling for the new `useResetPasswordV2`.**
While the POST request is straightforward, it may be prudent to handle possible request failures more robustly (e.g., returning errors for invalid tokens, invalid password, etc.). This could be done via `onError` in `useMutation` or a try/catch block within the mutation function.

```diff
 useResetPasswordV2 = () => {
   return useMutation({
     mutationFn: async (details: ResetPasswordV2DTO) => {
       try {
         await apiRequest.post("/api/v2/password/password-reset", details, {
           headers: {
             Authorization: `Bearer ${details.verificationToken}`
           }
         });
+      } catch (err) {
+        // Possible place to provide user feedback or logging
+        throw err;
       }
     }
   });
};
```

</blockquote></details>
<details>
<summary>frontend/src/pages/auth/PasswordResetPage/PasswordResetPage.tsx (1)</summary><blockquote>

`19-23`: **Schema definition for `formData`.**
Defining the schema via Zod for `verificationToken`, `privateKey`, and `userEncryptionVersion` is concise and enforces early validation. Consider adding more constraints if the verification token or other fields demand specific formats (e.g., lengths).

</blockquote></details>
<details>
<summary>frontend/src/pages/auth/PasswordResetPage/components/EnterPasswordStep.tsx (1)</summary><blockquote>

`95-167`: **No inline error feedback for failed server requests.**
Although the decryption/encryption pipeline and the password-based re-encryption logic are correct, there is no direct in-form error message if the server call fails (e.g., network error, invalid token, etc.). You may want to catch and display errors so users see immediate feedback.

```diff
+    try {
       if (encryptionVersion === UserEncryptionVersion.V2) {
         await resetPasswordV2({
           newPassword: data.password,
           verificationToken
         });
       } else {
         // existing JSRP logic
       }
+    } catch (err) {
+      // Provide user-facing error message or call a toast notification, for example
+      console.error(err);
+      return;
     }
```

</blockquote></details>
<details>
<summary>backend/src/services/auth/auth-password-service.ts (3)</summary><blockquote>

`122-150`: **Good security measure but consider the concurrency impact.**
Enforcing a minimum 8-second delay helps mitigate timing attacks. However, this synchronous delay might raise concerns on a high-traffic system. Consider verifying whether this blocking approach could lead to performance bottlenecks under heavy load.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 126-126: Change to an optional chain.

Unsafe fix: Change to an optional chain.


(lint/complexity/useOptionalChain)

</details>

</details>

---

`126-126`: **Optional chain suggestion.**
Static analysis recommends using optional chaining: `if (user?.isAccepted)`. This is a minor style preference; your existing null-checking is valid, though you may consider optional chaining for brevity.

```diff
-if (user && user.isAccepted) {
+if (user?.isAccepted) {
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 126-126: Change to an optional chain.

Unsafe fix: Change to an optional chain.


(lint/complexity/useOptionalChain)

</details>

</details>

---

`159-163`: **Ensure consistent error status.**
Throwing a `BadRequestError` here is logically fine. However, consider whether a 404 or 401 code might convey the meaning more precisely (e.g., “No encryption data found” can hint at a missing resource). Keep the error semantics consistent across similar checks.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 706feafbf203c648f34f93e6c6c6cccdcc95713b and 7003ad608a1fa1bf54d65aefc744e104d5a136c5.

</details>

<details>
<summary>📒 Files selected for processing (27)</summary>

* `backend/src/server/routes/v1/password-router.ts` (2 hunks)
* `backend/src/server/routes/v2/index.ts` (2 hunks)
* `backend/src/server/routes/v2/password-router.ts` (1 hunks)
* `backend/src/services/auth/auth-fns.ts` (1 hunks)
* `backend/src/services/auth/auth-password-service.ts` (6 hunks)
* `backend/src/services/auth/auth-password-type.ts` (1 hunks)
* `frontend/src/components/auth/DonwloadBackupPDFStep.tsx` (0 hunks)
* `frontend/src/components/utilities/checks/password/PasswordCheck.ts` (1 hunks)
* `frontend/src/hooks/api/auth/index.tsx` (1 hunks)
* `frontend/src/hooks/api/auth/queries.tsx` (3 hunks)
* `frontend/src/hooks/api/auth/types.ts` (2 hunks)
* `frontend/src/pages/admin/SignUpPage/SignUpPage.tsx` (4 hunks)
* `frontend/src/pages/admin/SignUpPage/components/DownloadBackupKeys/DownloadBackupKeys.tsx` (0 hunks)
* `frontend/src/pages/admin/SignUpPage/components/DownloadBackupKeys/index.tsx` (0 hunks)
* `frontend/src/pages/auth/PasswordResetPage/PasswordResetPage.tsx` (1 hunks)
* `frontend/src/pages/auth/PasswordResetPage/components/ConfirmEmailStep.tsx` (1 hunks)
* `frontend/src/pages/auth/PasswordResetPage/components/EnterPasswordStep.tsx` (1 hunks)
* `frontend/src/pages/auth/PasswordResetPage/components/InputBackupKeyStep.tsx` (1 hunks)
* `frontend/src/pages/auth/SignUpInvitePage/SignUpInvitePage.tsx` (3 hunks)
* `frontend/src/pages/auth/SignUpPage/SignUpPage.tsx` (1 hunks)
* `frontend/src/pages/auth/SignUpSsoPage/SignUpSsoPage.tsx` (0 hunks)
* `frontend/src/pages/auth/SignUpSsoPage/components/BackupPDFStep/BackupPDFStep.tsx` (0 hunks)
* `frontend/src/pages/auth/SignUpSsoPage/components/BackupPDFStep/index.tsx` (0 hunks)
* `frontend/src/pages/auth/SignUpSsoPage/components/UserInfoSSOStep/UserInfoSSOStep.tsx` (4 hunks)
* `frontend/src/pages/auth/VerifyEmailPage/VerifyEmailPage.tsx` (1 hunks)
* `frontend/src/pages/user/PersonalSettingsPage/components/ChangePasswordSection/ChangePasswordSection.tsx` (3 hunks)
* `frontend/src/pages/user/PersonalSettingsPage/components/PersonalGeneralTab/PersonalGeneralTab.tsx` (1 hunks)

</details>

<details>
<summary>💤 Files with no reviewable changes (6)</summary>

* frontend/src/pages/auth/SignUpSsoPage/components/BackupPDFStep/index.tsx
* frontend/src/pages/auth/SignUpSsoPage/SignUpSsoPage.tsx
* frontend/src/pages/auth/SignUpSsoPage/components/BackupPDFStep/BackupPDFStep.tsx
* frontend/src/pages/admin/SignUpPage/components/DownloadBackupKeys/index.tsx
* frontend/src/pages/admin/SignUpPage/components/DownloadBackupKeys/DownloadBackupKeys.tsx
* frontend/src/components/auth/DonwloadBackupPDFStep.tsx

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

<details>
<summary>backend/src/services/auth/auth-password-service.ts</summary>

[error] 126-126: Change to an optional chain.

Unsafe fix: Change to an optional chain.


(lint/complexity/useOptionalChain)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (4)</summary>

* GitHub Check: Check API Changes
* GitHub Check: Check TS and Lint
* GitHub Check: Run integration test
* GitHub Check: Check Frontend Type and Lint check

</details>

<details>
<summary>🔇 Additional comments (42)</summary><blockquote>

<details>
<summary>frontend/src/pages/auth/SignUpSsoPage/components/UserInfoSSOStep/UserInfoSSOStep.tsx (1)</summary>

`5-5`: **Improved navigation approach using router instead of state management.**

The change from using a step state approach to router-based navigation is a clean improvement. This aligns with the PR objective of transitioning from React state to better state management approaches and simplifies the signup flow by removing the backup key download step.



Also applies to: 21-21, 66-66, 206-208

</details>
<details>
<summary>frontend/src/hooks/api/auth/index.tsx (1)</summary>

`5-6`: **Nicely organized additions of new API hooks.**

The addition of `useResetPasswordV2` and `useResetUserPasswordV2` hooks maintains the existing export pattern while extending functionality to support the new v2 password reset flow.

</details>
<details>
<summary>backend/src/server/routes/v2/index.ts (2)</summary>

`6-6`: **Clean import addition.**

Import statement follows the existing pattern and naming convention.

---

`16-16`: **Well-structured route registration.**

Registration of the password router follows the established pattern with appropriate prefix. The positioning within the file is logical, keeping it with other authentication-related registrations.

</details>
<details>
<summary>backend/src/services/auth/auth-password-type.ts (2)</summary>

`16-19`: **Clear enum definition for password reset types.**

The enum clearly distinguishes between recovery (forgotten password) and logged-in reset scenarios, which aligns well with the PR objective of supporting password resets without emergency kits.

---

`21-26`: **Well-structured DTO for password reset operations.**

The type definition includes all necessary fields and handles both reset scenarios appropriately. The optional `oldPassword` field is a good choice for the logged-in reset flow where password verification may be needed.

</details>
<details>
<summary>frontend/src/pages/auth/VerifyEmailPage/VerifyEmailPage.tsx (1)</summary>

`75-77`: **Improved user guidance for password reset.**

The updated text provides clearer instructions about the password reset process, removing references to "emergency kit" which aligns with the PR's objective of enabling password resets without requiring such kits. The center-aligned, simplified messaging improves user experience.

</details>
<details>
<summary>frontend/src/components/utilities/checks/password/PasswordCheck.ts (2)</summary>

`37-37`: <details>
<summary>✅ Verification successful</summary>

**Consider the implications of optional chaining in the password validator**

The introduction of optional chaining (`pwd?.length`) seems unnecessary since `pwd` is explicitly typed as a non-optional `string` parameter and is directly passed from the `password` variable which is required in the interface.

If there are legitimate cases where `pwd` could be `undefined`, this optional chaining would cause the validation to always fail since `undefined >= 14` evaluates to `false`.

---

🏁 Script executed:

```shell
#!/bin/bash
# Check if there are any places in the codebase where the password validator is called with undefined or null
rg -A 5 "validator\(.*null.*\)" --glob "*.ts*"
rg -A 5 "validator\(.*undefined.*\)" --glob "*.ts*"
```

Length of output: 102

---



</details>

**Ensure Consistent Validator Behavior in PasswordCheck**

The password validator’s use of optional chaining appears unnecessary. Since `pwd` is explicitly typed as a non-optional string and our search did not reveal any cases where the validator is invoked with `undefined` or `null`, it is safe to remove the optional chaining. This change will make the validator’s intent clearer and avoid any misinterpretation.

- **File:** `frontend/src/components/utilities/checks/password/PasswordCheck.ts`  
- **Line 37:** Change from  
  ```ts
  validator: (pwd: string) => pwd?.length >= 14,
  ```  
  to  
  ```ts
  validator: (pwd: string) => pwd.length >= 14,
  ```

---

`42-42`: <details>
<summary>❓ Verification inconclusive</summary>

**Same optional chaining concern as in the previous validator**

The optional chaining pattern is repeated here. If this is intentional defensive programming, consider documenting why it's needed or refactoring the validator signature to explicitly accept potentially undefined values.

---

🏁 Script executed:

```shell
#!/bin/bash
# Check how the password validators are typically called throughout the codebase
rg "test\.validator\(" --glob "*.ts*"
```

Length of output: 265

---



</details>

**Optional Chaining on `pwd` Parameter Needs Clarification**  
At line 42 in `frontend/src/components/utilities/checks/password/PasswordCheck.ts`, the validator is defined as:
  
```ts
validator: (pwd: string) => pwd?.length < 101,
```

Since the parameter is typed as a string, using `pwd?.length` suggests defensive coding against a potential undefined value. However, review of the call sites shows that the validator is typically invoked with an explicit value (e.g., via `if (!test.validator(password))`). Please consider one of the following approaches:
  
- **Remove optional chaining:** If the password is guaranteed to be a defined string, refactor the code to use `pwd.length` to avoid ambiguity.
- **Refine the function signature:** Update the parameter type to `pwd?: string` if it's possible for an undefined value to be passed, and add documentation explaining why this defensive pattern is needed.

</details>
<details>
<summary>frontend/src/pages/user/PersonalSettingsPage/components/PersonalGeneralTab/PersonalGeneralTab.tsx (1)</summary>

`10-17`: **LGTM! Good conditional rendering based on encryption version**

The conditional rendering of the `EmergencyKitSection` only for users with encryption version V1 aligns well with the PR objective of maintaining backward compatibility while removing backup key downloads for V2 users.

The fallback to `UserEncryptionVersion.V2` when user data is not available is a good defensive programming practice.

</details>
<details>
<summary>frontend/src/pages/auth/SignUpPage/SignUpPage.tsx (1)</summary>

`74-74`: **Update the step number in the condition to reflect removal of backup PDF download**

This change correctly adjusts the navigation logic after removing the DownloadBackupPDF step.

</details>
<details>
<summary>backend/src/server/routes/v1/password-router.ts (2)</summary>

`117-120`: **LGTM! Good update to include encryption version in the response**

The response schema update properly adds the user encryption version, which is crucial information for the frontend to determine which password reset flow to use.

---

`124-127`: **LGTM! Simplified return statement**

The handler now directly returns the result of `verifyPasswordResetEmail`, which is cleaner and more maintainable.

</details>
<details>
<summary>frontend/src/hooks/api/auth/types.ts (3)</summary>

`6-9`: **Well-structured enum for versioning user encryption.**

The `UserEncryptionVersion` enum clearly defines the supported encryption versions, making it easy to perform version-specific logic throughout the codebase. This supports the PR's goal of adding backward compatibility for the password reset feature.

---

`144-147`: **LGTM: Properly structured DTO for V2 password reset.**

This type correctly captures the essential fields needed for the new V2 password reset process - the new password and verification token. This is consistent with the token-based approach mentioned in the PR objectives.

---

`149-152`: **LGTM: Clear DTO for logged-in user password changes.**

The separation between `ResetPasswordV2DTO` (for recovery) and `ResetUserPasswordV2DTO` (for logged-in users) creates a clear distinction between the two password reset flows, enhancing type safety in the codebase.

</details>
<details>
<summary>frontend/src/pages/user/PersonalSettingsPage/components/ChangePasswordSection/ChangePasswordSection.tsx (2)</summary>

`51-51`: **Good use of React Query hook for the API call.**

Using a mutation hook from React Query for the password reset operation provides automatic loading state management, error handling, and request deduplication.

---

`63-74`: **Great implementation of conditional password reset logic based on encryption version.**

The code elegantly handles two different password reset flows based on the user's encryption version, fulfilling the PR's requirement to maintain backward compatibility while introducing the new V2 password reset functionality.

</details>
<details>
<summary>backend/src/server/routes/v2/password-router.ts (3)</summary>

`10-29`: **Secure implementation of the recovery token password reset endpoint.**

The route properly:
- Applies rate limiting to prevent brute force attacks
- Validates the token from authorization header
- Only requires the new password in the request body
- Uses a dedicated service method for the reset operation

This satisfies the PR objective of providing a secure password reset workflow.

---

`31-52`: **Well-designed logged-in user password reset endpoint.**

The implementation includes:
- Rate limiting protection
- Authentication verification through `verifyAuth` middleware
- Proper validation of both old and new passwords
- Clear separation between recovery and logged-in reset flows

This supports the PR objective of enhancing the password reset functionality.

---

`1-53`: <details>
<summary>✅ Verification successful</summary>

**Verify handling of timing attacks for the email password reset endpoint.**

The PR objectives mention addressing a timing attack vulnerability in the email password reset endpoint with a fixed delay of 8 seconds. I don't see this implementation in this file.

---

🏁 Script executed:

```shell
#!/bin/bash
# Check for implementation of timing attack protection in the server code

# Look for any code that implements fixed delays in password reset endpoints
rg -A 3 -B 3 "setTimeout|delay|sleep" --type ts backend/src/
```

Length of output: 44527

---



</details>

**Timing Attack Mitigation Confirmed in Service Implementation**

The 8‑second fixed delay to mitigate timing attacks isn’t applied directly in the router file. Instead, our verification shows that the delay is implemented within the password service (specifically in `backend/src/services/auth/auth-password-service.ts`), where it ensures that each password reset request takes at least 8 seconds. The router appropriately delegates to this service. No further changes are needed here, but please confirm that all reset flows invoke this service so the protection applies uniformly.

</details>
<details>
<summary>backend/src/services/auth/auth-fns.ts (1)</summary>

`48-70`: **Comprehensive token validation for password reset.**

The `validatePasswordResetAuthorization` function performs thorough validation:
- Checks for token presence
- Validates the token type is "bearer"
- Verifies the JWT signature with proper error handling

This supports the security requirements for the password reset feature.

</details>
<details>
<summary>frontend/src/pages/auth/PasswordResetPage/components/InputBackupKeyStep.tsx (2)</summary>

`24-41`: **LGTM! Good error handling implementation**

The function correctly handles API calls and encryption/decryption operations with proper error handling. The form error is set appropriately when decryption fails.

---

`1-87`: **Well-structured component using modern form handling patterns**

This component is well-implemented using react-hook-form with Zod for validation. The form structure and error handling follow best practices, and the component has clear separation of concerns between UI rendering and business logic.

</details>
<details>
<summary>frontend/src/pages/auth/PasswordResetPage/components/ConfirmEmailStep.tsx (1)</summary>

`1-54`: **Good implementation of the email confirmation step**

The component is well-structured and follows the single responsibility principle. It correctly extracts data from the URL and handles the verification process appropriately.

</details>
<details>
<summary>frontend/src/pages/auth/SignUpInvitePage/SignUpInvitePage.tsx (3)</summary>

`389-390`: **Successfully removed backup key step**

The code has been properly updated to remove the backup key generation and download functionality, which aligns with the PR's objective to remove backup key downloads. The conditional rendering now only includes the email confirmation and main signup steps.

---

`205-207`: **Updated navigation syntax correctly**

The navigation syntax has been updated to use the newer pattern with the `to:` property, which is the correct approach.

---

`183-185`: <details>
<summary>❓ Verification inconclusive</summary>

**Test token-based navigation behavior**

Make sure to verify that the navigation to the SecretManager works correctly after the reorganization of the signup flow. Since the backup key step was removed, it's important to ensure that the user is still redirected to the correct location after signup.

---

🏁 Script executed:

```shell
#!/bin/bash
# Check for any other occurrences of similar navigation patterns in the codebase

echo "Searching for similar navigation patterns..."
rg "navigate\(\{[\s\n]*to:" --type=tsx -A 1 -B 1
```

Length of output: 177

---

I've noticed that the initial script failed because the file type "tsx" wasn't recognized. To gather the necessary information, please run the following shell script to search for occurrences of the navigation pattern within TSX files (which should include the token-based navigation behavior):




After running this script, please verify that the navigation logic correctly directs the user to the SecretManager after signup (especially given the removal of the backup key step). Once confirmed, you can update or remove the comment accordingly.

****

---

🏁 Script executed:

```shell
#!/bin/bash
echo "Searching for navigation patterns using 'navigate(' in TSX files..."
rg "navigate\(" -g "*.tsx" --sort-files -n
```

Length of output: 24758

---



</details>

**Verify Redirection Post-Signup in Token-Based Flow**

Please ensure that after completing the signup flow on SignUpInvitePage (starting at lines 183–185), a user with a valid token is routed to the correct page (e.g., the SecretManager overview). Since the backup key step was removed, manually verify that the redirection logic in `completeSignupFlow` behaves correctly, matching the expected behavior seen in similar navigation patterns across the codebase.

</details>
<details>
<summary>frontend/src/pages/admin/SignUpPage/SignUpPage.tsx (3)</summary>

`85-85`: **Simplified conditional rendering**

The conditional rendering has been simplified, which is a good improvement for readability.

---

`98-188`: **Successfully streamlined signup flow**

The signup flow has been appropriately streamlined from a multi-step process to a single form, which improves the user experience by making it more straightforward. The form structure and validation are well-implemented.

---

`48-83`: <details>
<summary>❓ Verification inconclusive</summary>

**Verify removal of backup key logic is complete**

Since the PR objective includes removing backup key downloads, ensure that all related functionality has been removed completely, not just the UI components. This includes checking if there are any leftover API calls or utility functions that were used solely for backup key generation.

---

🏁 Script executed:

```shell
#!/bin/bash
# Search for any remaining references to backup keys in the codebase

echo "Searching for backup key references..."
rg "backup(Key|PDF)" --type=tsx
```

Length of output: 148

---



</details>

Below is a new shell script to re-run the search correctly (using a glob instead of the unrecognized file type):

```shell
#!/bin/bash
echo "Searching for backup key references in all .tsx files using a glob..."
rg "backup(Key|PDF)" -g "*.tsx"
```

**Backup Key Logic Removal Verification**

Since the previous command failed due to an unrecognized file type, please run the above script. Verify manually that no output is returned—this confirms that all backup key logic (including API calls or utility functions) has been completely removed as intended.

</details>
<details>
<summary>frontend/src/hooks/api/auth/queries.tsx (4)</summary>

`25-26`: **No issues with new DTO types.**
The introduction of `ResetPasswordV2DTO` and `ResetUserPasswordV2DTO` looks consistent with the usage in this PR. Ensure these types remain aligned with the backend endpoint schemas as they evolve.

---

`33-33`: **Ensure consistent usage of `UserEncryptionVersion` across the codebase.**
`UserEncryptionVersion` is now being referenced in several new places. Verify all relevant areas (backend, components, error handlers) are updated to expect/support these enumerations properly.

---

`253-256`: **Type-safe response in `useVerifyPasswordResetCode`.**
This explicit type definition for `{ token, userEncryptionVersion }` is clear and helps with type safety. Confirm that any calling code properly handles scenarios in which `userEncryptionVersion` could be unexpectedly missing or invalid.

---

`323-329`: **Verify whether authentication is required for `useResetUserPasswordV2`.**
The `Bearer` token header is omitted here. If this endpoint intentionally does not require auth, that's acceptable. Otherwise, consider adding a similar header to ensure security consistency across both password reset endpoints.

</details>
<details>
<summary>frontend/src/pages/auth/PasswordResetPage/PasswordResetPage.tsx (2)</summary>

`1-17`: **Overall structure and enum-based step flow look good.**
Using `Steps` to guide the flow is a clean approach. It neatly separates the password reset process into logical steps. The imports and setup for `react-hook-form` and `zod` are straightforward.

---

`27-75`: **Effective step-handling for multi-stage password reset.**
1. `ConfirmEmailStep` captures and sets `verificationToken` + `userEncryptionVersion`.
2. Logic branches to either `InputBackupKeyStep` or directly `EnterNewPasswordStep` based on encryption version.
3. Navigation uses `react-router` upon completion.

This is a well-structured approach. Just ensure that step transitions handle any edge cases (e.g., missing or invalid tokens) and gracefully degrade with user-friendly error feedback.

</details>
<details>
<summary>frontend/src/pages/auth/PasswordResetPage/components/EnterPasswordStep.tsx (2)</summary>

`1-29`: **Check for potential oversharing of sensitive data.**
The import of `crypto`, usage of `privateKey`, and handling of user passwords are necessary for encryption purposes. Just confirm that no logs or extraneous console statements leak these sensitive values anywhere in the code or dev tools.

---

`169-326`: **Appropriate password policy enforcement with real-time checks.**
1. `passwordCheck` for length, characters, breach check, etc., appears comprehensive.
2. The UI feedback with icons is user-friendly.
3. Good job using dynamic states to guide the user on the requirements.

Ensure that the `passwordCheck` includes any newly introduced constraints from your security team in future iterations (e.g., more granular checks or updated patterns).

</details>
<details>
<summary>backend/src/services/auth/auth-password-service.ts (3)</summary>

`17-17`: **Import of UserEncryption type is correct.**
This import is duly used in the file for handling V2 encryption references.

---

`20-23`: **New types for the V2 password reset.**
The introduction of `ResetPasswordV2Type` and `TResetPasswordV2DTO` is clear and aligns with the new password reset logic.

---

`500-501`: **Export of new functions.**
Exporting `resetPasswordV2` alongside `setupPassword` ensures external modules can access the new logic for V2 flows. Good addition.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-07T00:45:35Z
user: login: varonix0
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-07T00:46:26Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-07T00:47:30Z
user: login: varonix0
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-07T00:54:26Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-07T00:55:57Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>backend/src/services/auth/auth-password-service.ts (1)</summary><blockquote>

`126-126`: **Consider using optional chaining for better code safety.**

The static analysis tool suggests using optional chaining here.

```diff
-    if (user && user.isAccepted) {
+    if (user?.isAccepted) {
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 126-126: Change to an optional chain.

Unsafe fix: Change to an optional chain.


(lint/complexity/useOptionalChain)

</details>

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 7003ad608a1fa1bf54d65aefc744e104d5a136c5 and c48c9ae628ab64c407cb29bb8716b8a664f0c2e6.

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `backend/src/services/auth/auth-password-service.ts` (6 hunks)
* `frontend/src/pages/user/PersonalSettingsPage/components/ChangePasswordSection/ChangePasswordSection.tsx` (5 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧠 Learnings (1)</summary>

<details>
<summary>backend/src/services/auth/auth-password-service.ts (2)</summary>

```
Learnt from: DanielHougaard
PR: Infisical/infisical#3198
File: backend/src/services/auth/auth-password-service.ts:187-271
Timestamp: 2025-03-07T00:55:56.068Z
Learning: The newly implemented resetPasswordV2 function in auth-password-service.ts handles password resets for the v2 user encryption architecture but should include session invalidation when passwords are changed.
```

```
Learnt from: DanielHougaard
PR: Infisical/infisical#3198
File: backend/src/services/auth/auth-password-service.ts:7-8
Timestamp: 2025-03-07T00:47:28.763Z
Learning: The encryption function name has a known typo and is consistently defined as "infisicalSymmetricEncypt" (missing 'r') throughout the Infisical codebase. This spelling should be maintained for consistency until a codebase-wide renaming effort is undertaken.
```

</details>

</details><details>
<summary>🪛 Biome (1.9.4)</summary>

<details>
<summary>backend/src/services/auth/auth-password-service.ts</summary>

[error] 126-126: Change to an optional chain.

Unsafe fix: Change to an optional chain.


(lint/complexity/useOptionalChain)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (4)</summary>

* GitHub Check: Check Frontend Type and Lint check
* GitHub Check: Check API Changes
* GitHub Check: Check TS and Lint
* GitHub Check: Run integration test

</details>

<details>
<summary>🔇 Additional comments (11)</summary><blockquote>

<details>
<summary>frontend/src/pages/user/PersonalSettingsPage/components/ChangePasswordSection/ChangePasswordSection.tsx (5)</summary>

`14-16`: **Good addition of imports for the new password reset flow.**

The imports for `useResetUserPasswordV2`, `UserEncryptionVersion`, and `useNavigate` are appropriate additions to support the new password reset functionality and improved navigation.

---

`40-40`: **Good implementation of React Router's navigation.**

Using the `useNavigate` hook from @tanstack/react-router is a better practice than directly manipulating `window.location.href`.

---

`53-53`: **Appropriate setup of the mutation hook.**

Destructuring the `mutateAsync` method from the `useResetUserPasswordV2` hook allows for clean and convenient usage in the component.

---

`64-76`: **Well-structured conditional password reset logic.**

The implementation properly handles different user encryption versions:
1. For V2 users, it uses the new `resetPasswordV2` function
2. For non-V2 users, it maintains backward compatibility with `attemptChangePassword`

This matches the PR objective of supporting password reset for v2 users while maintaining compatibility.

---

`85-85`: **Improved navigation implementation.**

Replacing direct window location manipulation with React Router's navigation is a better practice for single-page applications.

</details>
<details>
<summary>backend/src/services/auth/auth-password-service.ts (6)</summary>

`7-8`: **The encryption function import retains the known typo.**

Based on the learned context, the typo in `infisicalSymmetricEncypt` (missing 'r') is consistent throughout the codebase and should be maintained until a codebase-wide renaming effort.

---

`122-150`: **Good implementation of timing attack protection.**

The changes to `sendPasswordResetEmail` include an excellent security improvement:
1. Conditional email sending if user exists and is accepted
2. A fixed 8-second request duration to mitigate timing attacks that could reveal whether an email exists

This addresses the timing attack vulnerability mentioned in the PR objectives.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 126-126: Change to an optional chain.

Unsafe fix: Change to an optional chain.


(lint/complexity/useOptionalChain)

</details>

</details>

---

`160-163`: **Proper user encryption data validation.**

Adding a check for user encryption data existence and throwing a `BadRequestError` with a clear message improves error handling.

---

`184-184`: **Enhanced response with encryption version information.**

Including the `userEncryptionVersion` in the response is important for the frontend to determine which password reset flow to use.

---

`187-267`: **Well-implemented password reset logic for V2 users.**

The `resetPasswordV2` function is comprehensive and well-structured:
1. Validates user existence and appropriate conditions
2. Handles different reset types (LoggedInReset requiring old password verification)
3. Properly decrypts and re-encrypts the user's private key with the new password
4. Updates all necessary user encryption data
5. Revokes all sessions after password change for security

I notice you've implemented session invalidation as previously suggested, which is an important security measure.

---

`496-497`: **Export of new functionalities is properly handled.**

The `resetPasswordV2` function is correctly exported, making it available for use in other modules.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-07T00:58:06Z
user: login: akhilmhdh
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-03-07T09:54:25Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>backend/src/services/auth/auth-password-service.ts (1)</summary><blockquote>

`122-148`: **Good security improvement to prevent timing attacks.**

The refactoring of `sendPasswordResetEmail` to run the email sending process asynchronously in the background is an excellent security practice. This prevents timing attacks by ensuring consistent response times regardless of whether a user exists in the system.

Consider an alternative to the 8-second delay mentioned in the previous comments:

```diff
 void sendEmail().catch((err) => logger.error(err, "Failed to send password reset email"));
+ return { message: "If your email exists in our system, you will receive a password reset link shortly." };
```

This approach provides immediate feedback to users while maintaining security, similar to how MongoDB and SendGrid handle password resets.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 126-126: Change to an optional chain.

Unsafe fix: Change to an optional chain.


(lint/complexity/useOptionalChain)

</details>

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 67f2e4671a61a759af0a4b77b6cf8423b5b49822 and 795d9e44139348e11cca7ce171752d66f00c784c.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `backend/src/services/auth/auth-password-service.ts` (6 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧠 Learnings (1)</summary>

<details>
<summary>backend/src/services/auth/auth-password-service.ts (2)</summary>

```
Learnt from: DanielHougaard
PR: Infisical/infisical#3198
File: backend/src/services/auth/auth-password-service.ts:187-271
Timestamp: 2025-03-07T00:55:56.068Z
Learning: The newly implemented resetPasswordV2 function in auth-password-service.ts handles password resets for the v2 user encryption architecture but should include session invalidation when passwords are changed.
```

```
Learnt from: DanielHougaard
PR: Infisical/infisical#3198
File: backend/src/services/auth/auth-password-service.ts:7-8
Timestamp: 2025-03-07T00:47:28.763Z
Learning: The encryption function name has a known typo and is consistently defined as "infisicalSymmetricEncypt" (missing 'r') throughout the Infisical codebase. This spelling should be maintained for consistency until a codebase-wide renaming effort is undertaken.
```

</details>

</details><details>
<summary>🪛 Biome (1.9.4)</summary>

<details>
<summary>backend/src/services/auth/auth-password-service.ts</summary>

[error] 126-126: Change to an optional chain.

Unsafe fix: Change to an optional chain.


(lint/complexity/useOptionalChain)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (3)</summary>

* GitHub Check: Check API Changes
* GitHub Check: Check TS and Lint
* GitHub Check: Run integration test

</details>

<details>
<summary>🔇 Additional comments (5)</summary><blockquote>

<details>
<summary>backend/src/services/auth/auth-password-service.ts (5)</summary>

`7-8`: **The encryption function name is correctly spelled as per codebase convention.**

The function is consistently defined with this spelling (`infisicalSymmetricEncypt` missing the 'r') throughout the Infisical codebase. Maintaining this spelling is appropriate until a codebase-wide renaming effort is undertaken.

---

`181-182`: **Appropriate return value enhancement for encryption version.**

Including the user's encryption version in the response is a good addition that enables the client to determine the appropriate password reset flow.

---

`184-264`: **Well-implemented password reset functionality with proper security measures.**

The `resetPasswordV2` function implements a comprehensive and secure approach to password resets for the v2 user encryption architecture. Key strengths include:

1. Thorough validation of user existence, password presence, and authentication methods
2. Different handling for logged-in users vs. recovery token resets
3. Proper decryption and re-encryption of the user's private key
4. Complete session invalidation after password reset for security

The implementation correctly addresses the requirement to allow password resets without an emergency kit while maintaining security.

---

`263-263`: **Excellent security practice for session management.**

Invalidating all user sessions after a password change is a critical security best practice. This ensures that any potentially compromised sessions are terminated when credentials change.

---

`493-495`: **Function exports are properly updated.**

The `resetPasswordV2` function is correctly added to the exported object, making it available for the new password reset routes.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-07T16:19:50Z
user: login: akhilmhdh
body: Rest looks good to me
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-10T10:24:28Z
user: login: akhilmhdh
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-10T18:30:34Z

Review comments:
diff_hunk: @@ -0,0 +1,54 @@
+import { useNavigate, useSearch } from "@tanstack/react-router";
+
+import { Button } from "@app/components/v2";
+import { ROUTE_PATHS } from "@app/const/routes";
+import { useVerifyPasswordResetCode } from "@app/hooks/api";
+import { UserEncryptionVersion } from "@app/hooks/api/auth/types";
+
+type Props = {
+  onComplete: (verificationToken: string, encryptionVersion: UserEncryptionVersion) => void;
+};
+
+export const ConfirmEmailStep = ({ onComplete }: Props) => {
+  const navigate = useNavigate();
+  const search = useSearch({ from: ROUTE_PATHS.Auth.PasswordResetPage.id });
+  const { token, to: email } = search;
path: frontend/src/pages/auth/PasswordResetPage/components/ConfirmEmailStep.tsx
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Add validation for URL parameters**

The code assumes that `token` and `email` will always exist in the search parameters. Consider adding validation to handle cases where these parameters might be missing.

---

🏁 Script executed:

```shell
#!/bin/bash
# Check how token and email parameters are validated elsewhere in the codebase

echo "Searching for validation patterns for URL parameters..."
rg -A 3 "useSearch|search\." --type=tsx
```

Length of output: 190

---



</details>

**Attention: Missing URL Parameter Validation**

In `frontend/src/pages/auth/PasswordResetPage/components/ConfirmEmailStep.tsx`, the code destructures `token` and `email` directly from `search` without checking whether these parameters actually exist. This could lead to runtime errors if either parameter is absent. Please add appropriate validation (for example, checking that `token` and `email` are defined before using them) to handle cases where one or both parameters might be missing.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-07T00:45:34Z
author_association: NONE
diff_hunk: @@ -162,8 +181,95 @@ export const authPaswordServiceFactory = ({
       { expiresIn: cfg.JWT_SIGNUP_LIFETIME }
     );
 
-    return { token, user };
+    return { token, user, userEncryptionVersion: userEnc.encryptionVersion as UserEncryption };
   };
+
+  const resetPasswordV2 = async ({ userId, newPassword, type, oldPassword }: TResetPasswordV2DTO) => {
+    const cfg = getConfig();
+
+    const user = await userDAL.findUserEncKeyByUserId(userId);
+    if (!user) {
+      throw new BadRequestError({ message: `User encryption key not found for user with ID '${userId}'` });
+    }
+
+    if (!user.hashedPassword) {
+      throw new BadRequestError({ message: "Unable to reset password, no password is set" });
+    }
+
+    if (!user.authMethods?.includes(AuthMethod.EMAIL)) {
+      throw new BadRequestError({ message: "Unable to reset password, no email authentication method is configured" });
+    }
+
+    // we check the old password if the user is resetting their password while logged in
+    if (type === ResetPasswordV2Type.LoggedInReset) {
+      if (!oldPassword) {
+        throw new BadRequestError({ message: "Current password is required." });
+      }
+
+      const isValid = await bcrypt.compare(oldPassword, user.hashedPassword);
+      if (!isValid) {
+        throw new BadRequestError({ message: "Incorrect current password." });
+      }
+    }
+
+    const newHashedPassword = await bcrypt.hash(newPassword, cfg.BCRYPT_SALT_ROUND);
+
+    // we need to get the original private key first for v2
+    let privateKey: string;
+    if (
+      user.serverEncryptedPrivateKey &&
+      user.serverEncryptedPrivateKeyTag &&
+      user.serverEncryptedPrivateKeyIV &&
+      user.serverEncryptedPrivateKeyEncoding &&
+      user.encryptionVersion === UserEncryption.V2
+    ) {
+      privateKey = infisicalSymmetricDecrypt({
+        iv: user.serverEncryptedPrivateKeyIV,
+        tag: user.serverEncryptedPrivateKeyTag,
+        ciphertext: user.serverEncryptedPrivateKey,
+        keyEncoding: user.serverEncryptedPrivateKeyEncoding as SecretKeyEncoding
+      });
+    } else {
+      throw new BadRequestError({
+        message: "Cannot reset password without current credentials or recovery method",
+        name: "Reset password"
+      });
+    }
+
+    const encKeys = await generateUserSrpKeys(user.username, newPassword, {
+      publicKey: user.publicKey,
+      privateKey
+    });
+
+    const { tag, iv, ciphertext, encoding } = infisicalSymmetricEncypt(privateKey);
+
+    await userDAL.transaction(async (tx) => {
+      await userDAL.updateUserEncryptionByUserId(
+        userId,
+        {
+          hashedPassword: newHashedPassword,
+
+          // srp params
+          salt: encKeys.salt,
+          verifier: encKeys.verifier,
+
+          protectedKey: encKeys.protectedKey,
+          protectedKeyIV: encKeys.protectedKeyIV,
+          protectedKeyTag: encKeys.protectedKeyTag,
+          encryptedPrivateKey: encKeys.encryptedPrivateKey,
+          iv: encKeys.encryptedPrivateKeyIV,
+          tag: encKeys.encryptedPrivateKeyTag,
+
+          serverEncryptedPrivateKey: ciphertext,
+          serverEncryptedPrivateKeyIV: iv,
+          serverEncryptedPrivateKeyTag: tag,
+          serverEncryptedPrivateKeyEncoding: encoding
+        },
+        tx
+      );
+    });
+  };
path: backend/src/services/auth/auth-password-service.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Validate new password strength and handle session revocations.**
The transactional update for `resetPasswordV2` is well-structured, but consider:
1. Adding a password-strength validation step to enforce security best practices.
2. Invalidating existing sessions/tokens where necessary to prevent unauthorized use of the old password.

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-03-07T00:45:34Z
author_association: NONE
diff_hunk: @@ -4,6 +4,8 @@
 import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas";
 import { getConfig } from "@app/lib/config/env";
 import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
+import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
+import { generateUserSrpKeys } from "@app/lib/crypto/srp";
path: backend/src/services/auth/auth-password-service.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Fix the typo in the second import.**
Currently, it's spelled `infisicalSymmetricEncypt`. Please rename it to match the actual function name (`infisicalSymmetricEncrypt`) to avoid confusion or runtime errors.


```diff
-import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
+import { infisicalSymmetricDecrypt, infisicalSymmetricEncrypt } from "@app/lib/crypto/encryption";
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
import { infisicalSymmetricDecrypt, infisicalSymmetricEncrypt } from "@app/lib/crypto/encryption";
import { generateUserSrpKeys } from "@app/lib/crypto/srp";
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-03-07T00:45:34Z
author_association: NONE
diff_hunk: @@ -4,6 +4,8 @@
 import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas";
 import { getConfig } from "@app/lib/config/env";
 import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
+import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
+import { generateUserSrpKeys } from "@app/lib/crypto/srp";
path: backend/src/services/auth/auth-password-service.ts
user: login: varonix0
body: It's spelled wrong everywhere. One day we'll fix up the spelling mistakes across the codebase. _One day_
created_at: 2025-03-07T00:46:26Z
author_association: MEMBER
diff_hunk: @@ -4,6 +4,8 @@
 import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas";
 import { getConfig } from "@app/lib/config/env";
 import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
+import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
+import { generateUserSrpKeys } from "@app/lib/crypto/srp";
path: backend/src/services/auth/auth-password-service.ts
user: login: coderabbitai[bot]
body: <details>
<summary>🧩 Analysis chain</summary>

@DanielHougaard I understand - the function is indeed defined as `infisicalSymmetricEncypt` throughout the codebase. Changing just this import would cause errors. You're right to keep the spelling consistent until a dedicated effort can be made to fix all instances at once.



---

🏁 Script executed:
```
#!/bin/bash
# Search for both spellings in the codebase
echo "Searching for 'infisicalSymmetricEncypt'..."
rg "infisicalSymmetricEncypt" -A 1 --no-filename

echo -e "\nSearching for 'infisicalSymmetricEncrypt'..."
rg "infisicalSymmetricEncrypt" -A 1 --no-filename

echo -e "\nChecking the actual definition or export in the crypto/encryption file..."
cat $(fd "encryption\.ts" --type f | grep -v "node_modules")
```

Length of output: 19700

---



</details>

@DanielHougaard I understand - the function is indeed defined as `infisicalSymmetricEncypt` throughout the codebase. Changing just this import would cause errors. You're right to keep the spelling consistent until a dedicated effort can be made to fix all instances at once.

---

<details>
<summary>✏️ Learnings added</summary>

```
Learnt from: DanielHougaard
PR: Infisical/infisical#3198
File: backend/src/services/auth/auth-password-service.ts:7-8
Timestamp: 2025-03-07T00:47:28.607Z
Learning: The encryption function name has a known typo and is consistently defined as "infisicalSymmetricEncypt" (missing 'r') throughout the Infisical codebase. This spelling should be maintained for consistency until a codebase-wide renaming effort is undertaken.
```

---

```
Learnt from: DanielHougaard
PR: Infisical/infisical#3198
File: backend/src/services/auth/auth-password-service.ts:7-8
Timestamp: 2025-03-07T00:47:28.607Z
Learning: The encryption function name has a known typo and is consistently defined as "infisicalSymmetricEncypt" (missing 'r') throughout the Infisical codebase. This spelling should be maintained for consistency until a codebase-wide renaming effort is undertaken.
```

> Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure [review instructions](https://docs.coderabbit.ai/guides/review-instructions). You can manage existing learnings in the [app](https://app.coderabbit.ai/learnings).

</details>

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-03-07T00:47:29Z
author_association: NONE
diff_hunk: @@ -162,8 +181,95 @@ export const authPaswordServiceFactory = ({
       { expiresIn: cfg.JWT_SIGNUP_LIFETIME }
     );
 
-    return { token, user };
+    return { token, user, userEncryptionVersion: userEnc.encryptionVersion as UserEncryption };
   };
+
+  const resetPasswordV2 = async ({ userId, newPassword, type, oldPassword }: TResetPasswordV2DTO) => {
+    const cfg = getConfig();
+
+    const user = await userDAL.findUserEncKeyByUserId(userId);
+    if (!user) {
+      throw new BadRequestError({ message: `User encryption key not found for user with ID '${userId}'` });
+    }
+
+    if (!user.hashedPassword) {
+      throw new BadRequestError({ message: "Unable to reset password, no password is set" });
+    }
+
+    if (!user.authMethods?.includes(AuthMethod.EMAIL)) {
+      throw new BadRequestError({ message: "Unable to reset password, no email authentication method is configured" });
+    }
+
+    // we check the old password if the user is resetting their password while logged in
+    if (type === ResetPasswordV2Type.LoggedInReset) {
+      if (!oldPassword) {
+        throw new BadRequestError({ message: "Current password is required." });
+      }
+
+      const isValid = await bcrypt.compare(oldPassword, user.hashedPassword);
+      if (!isValid) {
+        throw new BadRequestError({ message: "Incorrect current password." });
+      }
+    }
+
+    const newHashedPassword = await bcrypt.hash(newPassword, cfg.BCRYPT_SALT_ROUND);
+
+    // we need to get the original private key first for v2
+    let privateKey: string;
+    if (
+      user.serverEncryptedPrivateKey &&
+      user.serverEncryptedPrivateKeyTag &&
+      user.serverEncryptedPrivateKeyIV &&
+      user.serverEncryptedPrivateKeyEncoding &&
+      user.encryptionVersion === UserEncryption.V2
+    ) {
+      privateKey = infisicalSymmetricDecrypt({
+        iv: user.serverEncryptedPrivateKeyIV,
+        tag: user.serverEncryptedPrivateKeyTag,
+        ciphertext: user.serverEncryptedPrivateKey,
+        keyEncoding: user.serverEncryptedPrivateKeyEncoding as SecretKeyEncoding
+      });
+    } else {
+      throw new BadRequestError({
+        message: "Cannot reset password without current credentials or recovery method",
+        name: "Reset password"
+      });
+    }
+
+    const encKeys = await generateUserSrpKeys(user.username, newPassword, {
+      publicKey: user.publicKey,
+      privateKey
+    });
+
+    const { tag, iv, ciphertext, encoding } = infisicalSymmetricEncypt(privateKey);
+
+    await userDAL.transaction(async (tx) => {
+      await userDAL.updateUserEncryptionByUserId(
+        userId,
+        {
+          hashedPassword: newHashedPassword,
+
+          // srp params
+          salt: encKeys.salt,
+          verifier: encKeys.verifier,
+
+          protectedKey: encKeys.protectedKey,
+          protectedKeyIV: encKeys.protectedKeyIV,
+          protectedKeyTag: encKeys.protectedKeyTag,
+          encryptedPrivateKey: encKeys.encryptedPrivateKey,
+          iv: encKeys.encryptedPrivateKeyIV,
+          tag: encKeys.encryptedPrivateKeyTag,
+
+          serverEncryptedPrivateKey: ciphertext,
+          serverEncryptedPrivateKeyIV: iv,
+          serverEncryptedPrivateKeyTag: tag,
+          serverEncryptedPrivateKeyEncoding: encoding
+        },
+        tx
+      );
+    });
+  };
path: backend/src/services/auth/auth-password-service.ts
user: login: varonix0
body: Added session deletion on password change
created_at: 2025-03-07T00:54:26Z
author_association: MEMBER
diff_hunk: @@ -162,8 +181,95 @@ export const authPaswordServiceFactory = ({
       { expiresIn: cfg.JWT_SIGNUP_LIFETIME }
     );
 
-    return { token, user };
+    return { token, user, userEncryptionVersion: userEnc.encryptionVersion as UserEncryption };
   };
+
+  const resetPasswordV2 = async ({ userId, newPassword, type, oldPassword }: TResetPasswordV2DTO) => {
+    const cfg = getConfig();
+
+    const user = await userDAL.findUserEncKeyByUserId(userId);
+    if (!user) {
+      throw new BadRequestError({ message: `User encryption key not found for user with ID '${userId}'` });
+    }
+
+    if (!user.hashedPassword) {
+      throw new BadRequestError({ message: "Unable to reset password, no password is set" });
+    }
+
+    if (!user.authMethods?.includes(AuthMethod.EMAIL)) {
+      throw new BadRequestError({ message: "Unable to reset password, no email authentication method is configured" });
+    }
+
+    // we check the old password if the user is resetting their password while logged in
+    if (type === ResetPasswordV2Type.LoggedInReset) {
+      if (!oldPassword) {
+        throw new BadRequestError({ message: "Current password is required." });
+      }
+
+      const isValid = await bcrypt.compare(oldPassword, user.hashedPassword);
+      if (!isValid) {
+        throw new BadRequestError({ message: "Incorrect current password." });
+      }
+    }
+
+    const newHashedPassword = await bcrypt.hash(newPassword, cfg.BCRYPT_SALT_ROUND);
+
+    // we need to get the original private key first for v2
+    let privateKey: string;
+    if (
+      user.serverEncryptedPrivateKey &&
+      user.serverEncryptedPrivateKeyTag &&
+      user.serverEncryptedPrivateKeyIV &&
+      user.serverEncryptedPrivateKeyEncoding &&
+      user.encryptionVersion === UserEncryption.V2
+    ) {
+      privateKey = infisicalSymmetricDecrypt({
+        iv: user.serverEncryptedPrivateKeyIV,
+        tag: user.serverEncryptedPrivateKeyTag,
+        ciphertext: user.serverEncryptedPrivateKey,
+        keyEncoding: user.serverEncryptedPrivateKeyEncoding as SecretKeyEncoding
+      });
+    } else {
+      throw new BadRequestError({
+        message: "Cannot reset password without current credentials or recovery method",
+        name: "Reset password"
+      });
+    }
+
+    const encKeys = await generateUserSrpKeys(user.username, newPassword, {
+      publicKey: user.publicKey,
+      privateKey
+    });
+
+    const { tag, iv, ciphertext, encoding } = infisicalSymmetricEncypt(privateKey);
+
+    await userDAL.transaction(async (tx) => {
+      await userDAL.updateUserEncryptionByUserId(
+        userId,
+        {
+          hashedPassword: newHashedPassword,
+
+          // srp params
+          salt: encKeys.salt,
+          verifier: encKeys.verifier,
+
+          protectedKey: encKeys.protectedKey,
+          protectedKeyIV: encKeys.protectedKeyIV,
+          protectedKeyTag: encKeys.protectedKeyTag,
+          encryptedPrivateKey: encKeys.encryptedPrivateKey,
+          iv: encKeys.encryptedPrivateKeyIV,
+          tag: encKeys.encryptedPrivateKeyTag,
+
+          serverEncryptedPrivateKey: ciphertext,
+          serverEncryptedPrivateKeyIV: iv,
+          serverEncryptedPrivateKeyTag: tag,
+          serverEncryptedPrivateKeyEncoding: encoding
+        },
+        tx
+      );
+    });
+  };
path: backend/src/services/auth/auth-password-service.ts
user: login: coderabbitai[bot]
body: <details>
<summary>🧩 Analysis chain</summary>

🏁 Script executed:
```
#!/bin/bash
# Let's look for any functions related to session/token deletion that might be relevant
# First, search for token revocation functions that might be used
rg "token.*revoke|revoke.*token|invalidate.*session|clear.*session" --type ts

# Look specifically in auth services for session management related to password changes
rg "session|token" backend/src/services/auth

# Check for any functions related to clearing sessions
rg "clearToken|clearSession|revokeToken|revokeSession" --type ts

# Look at the token service specifically
fd "auth-token-service" --type f --exec cat {} \;
```

Length of output: 24512

---

🏁 Script executed:
```
#!/bin/bash
# Check the complete resetPasswordV2 function implementation in auth-password-service.ts
grep -A 90 "const resetPasswordV2" backend/src/services/auth/auth-password-service.ts

# Let's also check if there are any additional changes or implementations related to session deletion
# after the resetPasswordV2 function in the file
grep -A 10 -B 10 "tokenService.revokeAllMySessions" backend/src/services/auth/auth-password-service.ts
```

Length of output: 3748

---



</details>

@DanielHougaard Thank you for addressing the session revocation requirement. I've reviewed the code and while I can see that the `setupPassword` function includes `await tokenService.revokeAllMySessions(actor.id)`, I don't see similar session invalidation in the newly added `resetPasswordV2` function. 

For security best practices, when a password is changed or reset, all existing sessions should be invalidated to force re-authentication with the new credentials. Was this implemented somewhere else, or should it be added to the `resetPasswordV2` function as well?

---

<details>
<summary>✏️ Learnings added</summary>

```
Learnt from: DanielHougaard
PR: Infisical/infisical#3198
File: backend/src/services/auth/auth-password-service.ts:187-271
Timestamp: 2025-03-07T00:55:55.918Z
Learning: The newly implemented resetPasswordV2 function in auth-password-service.ts handles password resets for the v2 user encryption architecture but should include session invalidation when passwords are changed.
```

> Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure [review instructions](https://docs.coderabbit.ai/guides/review-instructions). You can manage existing learnings in the [app](https://app.coderabbit.ai/learnings).

</details>

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-03-07T00:55:56Z
author_association: NONE
diff_hunk: @@ -114,26 +119,35 @@ export const authPaswordServiceFactory = ({
    * Email password reset flow via email. Step 1 send email
    */
   const sendPasswordResetEmail = async (email: string) => {
+    const startTime = Date.now();
+
     const user = await userDAL.findUserByUsername(email);
-    // ignore as user is not found to avoid an outside entity to identify infisical registered accounts
-    if (!user || (user && !user.isAccepted)) return;
 
-    const cfg = getConfig();
-    const token = await tokenService.createTokenForUser({
-      type: TokenType.TOKEN_EMAIL_PASSWORD_RESET,
-      userId: user.id
-    });
+    if (user && user.isAccepted) {
+      const cfg = getConfig();
+      const token = await tokenService.createTokenForUser({
+        type: TokenType.TOKEN_EMAIL_PASSWORD_RESET,
+        userId: user.id
+      });
 
-    await smtpService.sendMail({
-      template: SmtpTemplates.ResetPassword,
-      recipients: [email],
-      subjectLine: "Infisical password reset",
-      substitutions: {
-        email,
-        token,
-        callback_url: cfg.SITE_URL ? `${cfg.SITE_URL}/password-reset` : ""
-      }
-    });
+      await smtpService.sendMail({
+        template: SmtpTemplates.ResetPassword,
+        recipients: [email],
+        subjectLine: "Infisical password reset",
+        substitutions: {
+          email,
+          token,
+          callback_url: cfg.SITE_URL ? `${cfg.SITE_URL}/password-reset` : ""
+        }
+      });
+    }
+
+    const elapsedTime = Date.now() - startTime;
path: backend/src/services/auth/auth-password-service.ts
user: login: akhilmhdh
body: What you can do here is:

Wrap the thing in async and instead of await use then and catch. This makes the process a background task.

Return straight away a message stating a reset confirmation mail has been send. This is how some of the existing platform like mongodb, sendgrid 
created_at: 2025-03-07T09:52:26Z
author_association: MEMBER
diff_hunk: @@ -114,26 +119,35 @@ export const authPaswordServiceFactory = ({
    * Email password reset flow via email. Step 1 send email
    */
   const sendPasswordResetEmail = async (email: string) => {
+    const startTime = Date.now();
+
     const user = await userDAL.findUserByUsername(email);
-    // ignore as user is not found to avoid an outside entity to identify infisical registered accounts
-    if (!user || (user && !user.isAccepted)) return;
 
-    const cfg = getConfig();
-    const token = await tokenService.createTokenForUser({
-      type: TokenType.TOKEN_EMAIL_PASSWORD_RESET,
-      userId: user.id
-    });
+    if (user && user.isAccepted) {
+      const cfg = getConfig();
+      const token = await tokenService.createTokenForUser({
+        type: TokenType.TOKEN_EMAIL_PASSWORD_RESET,
+        userId: user.id
+      });
 
-    await smtpService.sendMail({
-      template: SmtpTemplates.ResetPassword,
-      recipients: [email],
-      subjectLine: "Infisical password reset",
-      substitutions: {
-        email,
-        token,
-        callback_url: cfg.SITE_URL ? `${cfg.SITE_URL}/password-reset` : ""
-      }
-    });
+      await smtpService.sendMail({
+        template: SmtpTemplates.ResetPassword,
+        recipients: [email],
+        subjectLine: "Infisical password reset",
+        substitutions: {
+          email,
+          token,
+          callback_url: cfg.SITE_URL ? `${cfg.SITE_URL}/password-reset` : ""
+        }
+      });
+    }
+
+    const elapsedTime = Date.now() - startTime;
path: backend/src/services/auth/auth-password-service.ts
user: login: akhilmhdh
body: Waiting 8s is also kinda weird and cause user frustration
created_at: 2025-03-07T09:53:59Z
author_association: MEMBER
diff_hunk: @@ -142,6 +153,11 @@ export const authPaswordServiceFactory = ({
   const verifyPasswordResetEmail = async (email: string, code: string) => {
     const cfg = getConfig();
     const user = await userDAL.findUserByUsername(email);
+
+    const userEnc = await userDAL.findUserEncKeyByUserId(user.id);
+
+    if (!userEnc) throw new BadRequestError({ message: "Failed to find user encryption data" });
+
path: backend/src/services/auth/auth-password-service.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Add optional chaining for safer user access.**

The static analysis tool correctly flags a potential issue where `user` might be undefined, leading to a runtime error when accessing `user.id`.

```diff
-const userEnc = await userDAL.findUserEncKeyByUserId(user.id);
+const userEnc = await userDAL.findUserEncKeyByUserId(user?.id);

-if (!userEnc) throw new BadRequestError({ message: "Failed to find user encryption data" });
+if (!user || !userEnc) throw new BadRequestError({ message: "Failed to find user or user encryption data" });
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
    const userEnc = await userDAL.findUserEncKeyByUserId(user?.id);

    if (!user || !userEnc) throw new BadRequestError({ message: "Failed to find user or user encryption data" });
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-07T16:19:49Z
author_association: NONE
diff_hunk: @@ -0,0 +1,53 @@
+import { z } from "zod";
+
+import { authRateLimit } from "@app/server/config/rateLimiter";
+import { validatePasswordResetAuthorization } from "@app/services/auth/auth-fns";
+import { AuthMode } from "@app/services/auth/auth-type";
+import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
+import { ResetPasswordV2Type } from "@app/services/auth/auth-password-type";
+
+export const registerPasswordRouter = async (server: FastifyZodProvider) => {
+  server.route({
+    method: "POST",
+    url: "/password-reset",
+    config: {
+      rateLimit: authRateLimit
+    },
+    schema: {
+      body: z.object({
+        newPassword: z.string().trim()
+      })
+    },
+    handler: async (req) => {
+      const token = validatePasswordResetAuthorization(req.headers.authorization);
path: backend/src/server/routes/v2/password-router.ts
user: login: akhilmhdh
body: All good! I think you forgot to do validation on the token type.

https://github.com/Infisical/infisical/blob/main/backend/src/services/auth-token/auth-token-service.ts#L104


created_at: 2025-03-10T10:24:19Z
author_association: MEMBER

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-03-07T00:40:57Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough

The changes update various backend and frontend modules to enhance password reset and signup processes. In the backend, modifications include the addition of a new import for `UserEncryption`, updates to the response schema for the `/email/password-reset-verify` route, and the introduction of a new password router for version 2 with two POST routes handling recovery-token and logged-in resets. Enhancements to authorization and password reset functions include additional validations and encryption/decryption logic, along with new types and enumerations to support versioned password operations. On the frontend, several components and hooks related to backup key generation and PDF downloads have been removed from the signup flow. The password reset and change processes have been refactored to utilize form management and validation via `react-hook-form` and Zod. These adjustments streamline the routes and UI logic, aligning password reset functionality with the updated backend APIs.

## Suggested reviewers

- akhilmhdh
- scott-ray-wilson

<!-- walkthrough_end -->
<!-- This is an auto-generated comment: all tool run failures by coderabbit.ai -->

> [!WARNING]
> There were issues while running some tools. Please review the errors and either fix the tool’s configuration or disable the tool if it’s a critical failure.
> 
> <details>
> <summary>🔧 ESLint</summary>
> 
> > If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.
> 
> <details>
> <summary>backend/src/services/auth/auth-fns.ts</summary>
> 
> 
> Oops! Something went wrong! :(
> 
> ESLint: 8.57.1
> 
> ESLint couldn't find the plugin "@typescript-eslint/eslint-plugin".
> 
> (The package "@typescript-eslint/eslint-plugin" was not found when loaded as a Node module from the directory "/backend".)
> 
> It's likely that the plugin isn't installed correctly. Try reinstalling by running the following:
> 
>     npm install @typescript-eslint/eslint-plugin@latest --save-dev
> 
> The plugin "@typescript-eslint/eslint-plugin" was referenced from the config file in "backend/.eslintrc.js".
> 
> If you still can't figure out the problem, please stop by https://eslint.org/chat/help to chat with the team.
> 
> 
> </details>
> 
> </details>

<!-- end of auto-generated comment: all tool run failures by coderabbit.ai -->


---

<details>
<summary>📜 Recent review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 795d9e44139348e11cca7ce171752d66f00c784c and 9458c8b04f8a5a90f0a844f7d81e74c410ea87c0.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `backend/src/services/auth/auth-fns.ts` (1 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* backend/src/services/auth/auth-fns.ts

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (4)</summary>

* GitHub Check: Run integration test
* GitHub Check: Check API Changes
* GitHub Check: Check TS and Lint
* GitHub Check: Check Frontend Type and Lint check

</details>

</details>
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=Infisical/infisical&utm_content=3198):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: NONE
user: login: akhilmhdh
created_at: 2025-03-07T09:56:16Z
body: Will be doing a complete application test and checking the auth part once again!
author_association: MEMBER
user: login: akhilmhdh
created_at: 2025-03-07T19:56:51Z
body: Application testing looks good.

Will do one more code check on auth tmrw.
author_association: MEMBER