diff --git a/backend/src/server/routes/v1/password-router.ts b/backend/src/server/routes/v1/password-router.ts
index e96a577d9..724468e02 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 3d7581a70..cece502da 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 000000000..165130dec
--- /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 5f7aca812..ec6e0a303 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 9f004eafc..f5f42a236 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 sendEmail = async () => {
+ const user = await userDAL.findUserByUsername(email);
- 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` : ""
+ }
+ });
}
- });
+ };
+
+ // 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 7c67c0934..b3b14c3b4 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 b11ca8bd2..000000000
--- 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 (
-
-
-
- {t("signup.step4-message")}
-
-
-
-
- {t("signup.step4-description1")} {t("signup.step4-description3")}
-
-
-
-
-
- );
-}
diff --git a/frontend/src/components/utilities/checks/password/PasswordCheck.ts b/frontend/src/components/utilities/checks/password/PasswordCheck.ts
index e37abd475..fb5186220 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 66688cbdc..392f7b1f5 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 9b8afbac7..796fd3152 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 036897fed..32610c28d 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 f1c70d028..97b363558 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;
-enum SignupSteps {
- DetailsForm = "details-form",
- BackupKey = "backup-key"
-}
-
export const SignUpPage = () => {
const { t } = useTranslation();
const navigate = useNavigate();
const {
control,
handleSubmit,
- getValues,
formState: { isSubmitting }
} = useForm({
resolver: zodResolver(formSchema)
});
- const [step, setStep] = useState(SignupSteps.DetailsForm);
-
const { config } = useServerConfig();
const { mutateAsync: createAdminUser } = useCreateAdminUser();
const { mutateAsync: selectOrganization } = useSelectOrganization();
@@ -84,7 +72,7 @@ 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);
+ navigate({ to: "/admin" });
} catch (err) {
console.log(err);
createNotification({
@@ -94,27 +82,7 @@ export const SignUpPage = () => {
}
};
- 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"
- });
- }
- };
-
- if (config?.initialized && step === SignupSteps.DetailsForm)
- return ;
+ if (config?.initialized) return ;
return (
@@ -127,56 +95,28 @@ export const SignUpPage = () => {
- {step === SignupSteps.DetailsForm && (
-
-
-

-
Welcome to Infisical
-
Create your first Super Admin Account
-
-
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 253031a91..000000000
--- 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
;
-};
-
-export const DownloadBackupKeys = ({ onGenerate }: Props): JSX.Element => {
- const { t } = useTranslation();
- const [isLoading, setIsLoading] = useToggle();
-
- return (
-
-
-
- {t("signup.step4-message")}
-
-
-
-
- {t("signup.step4-description1")} {t("signup.step4-description3")}
-
-
-
-
-
-
-
-
-
- );
-};
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 bbbd9aad9..000000000
--- 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 3361dd961..8b6be6b9a 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 { 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 { 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 { UserEncryptionVersion } from "@app/hooks/api/auth/types";
-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";
+import { ConfirmEmailStep } from "./components/ConfirmEmailStep";
+import { EnterPasswordStep } from "./components/EnterPasswordStep";
+import { InputBackupKeyStep } from "./components/InputBackupKeyStep";
-// eslint-disable-next-line new-cap
-const client = new jsrp.client();
+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;
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 { watch, setValue } = useForm({
+ resolver: zodResolver(formData)
+ });
+ const verificationToken = watch("verificationToken");
+ const encryptionVersion = watch("userEncryptionVersion");
+ const privateKey = watch("privateKey");
+
+ const [step, setStep] = useState(Steps.ConfirmEmail);
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) => {
- 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) => {
- 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" });
-
- setLoading(false);
- });
- }
- );
- }
- };
-
- // Click a button to confirm email
- const stepConfirmEmail = (
-
-
- Confirm your email
-
-

-
-
-
-
- );
-
- // Input backup key
- const stepInputBackupKey = (
-
- );
-
- // Enter new password
- const stepEnterNewPassword = (
-
- );
return (
- {step === 1 && stepConfirmEmail}
- {step === 2 && stepInputBackupKey}
- {step === 3 && stepEnterNewPassword}
+ {step === Steps.ConfirmEmail && (
+ {
+ setValue("verificationToken", verifyToken);
+ setValue("userEncryptionVersion", userEncryptionVersion);
+
+ if (userEncryptionVersion === UserEncryptionVersion.V2) {
+ setStep(Steps.EnterNewPassword);
+ } else {
+ setStep(Steps.InputBackupKey);
+ }
+ }}
+ />
+ )}
+ {step === Steps.InputBackupKey && (
+ {
+ setValue("privateKey", key);
+ setStep(Steps.EnterNewPassword);
+ }}
+ />
+ )}
+ {step === Steps.EnterNewPassword && (
+ {
+ navigate({ to: "/login" });
+ }}
+ />
+ )}
);
};
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 000000000..1f1a79490
--- /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 (
+
+
+ Confirm your email
+
+

+
+
+
+
+ );
+};
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 000000000..de7bcd3f3
--- /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;
+
+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({
+ 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 (
+
+ );
+};
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 000000000..54a5d5e9d
--- /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;
+
+export const InputBackupKeyStep = ({ verificationToken, onComplete }: Props) => {
+ const { control, handleSubmit, setError } = useForm({
+ resolver: zodResolver(formData)
+ });
+
+ const getEncryptedKeyHandler = async (data: z.infer) => {
+ 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 (
+
+ );
+};
diff --git a/frontend/src/pages/auth/SignUpInvitePage/SignUpInvitePage.tsx b/frontend/src/pages/auth/SignUpInvitePage/SignUpInvitePage.tsx
index d3106e341..7cf4ad572 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({});
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 = () => {
);
- // Step 4 of the sign up process (download the emergency kit pdf)
- const step4 = (
-
-
- Save your Emergency Kit
-
-
-
- If you get locked out of your account, your Emergency Kit is the only way to sign in.
-
-
We recommend you download it and keep it somewhere safe.
-
-
-
- It contains your Secret Key which we cannot access or recover for you if you lose it.
-
-
-
-
-
- );
-
return (
@@ -425,7 +386,8 @@ export const SignupInvitePage = () => {
- {step === 1 ? stepConfirmEmail : step === 2 ? main : step4}
+ {step === 1 && stepConfirmEmail}
+ {step === 2 && main}
>
)}
diff --git a/frontend/src/pages/auth/SignUpPage/SignUpPage.tsx b/frontend/src/pages/auth/SignUpPage/SignUpPage.tsx
index c1fcc30c4..75af4ff48 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 (
-
- );
- }
-
if (serverDetails?.emailConfigured) {
return ;
}
diff --git a/frontend/src/pages/auth/SignUpSsoPage/SignUpSsoPage.tsx b/frontend/src/pages/auth/SignUpSsoPage/SignUpSsoPage.tsx
index a13cfcfc6..40075ae54 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 (
-
- );
default:
return ;
}
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 9a2369170..000000000
--- 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 (
-
-
-
- {t("signup.step4-message")}
-
-
-
-
- {t("signup.step4-description1")} {t("signup.step4-description3")}
-
-
-
-
-
-
-
-
-
- );
-};
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 01f7745cd..000000000
--- 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 c3d131708..a6ab0623b 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 418529bdc..47012e2cb 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?
-
- You will need your emergency kit. Enter your email to start account recovery.
+
+ Enter your email to start the password reset process. You will receive an email with
+ instructions.
@@ -102,8 +103,10 @@ export const VerifyEmailPage = () => {
Look for an email in your inbox.
-
- An email with instructions has been sent to {email}.
+
+ If the email is in our system, you will receive an email at{" "}
+ {email} with instructions on how to reset your
+ password.
diff --git a/frontend/src/pages/user/PersonalSettingsPage/components/ChangePasswordSection/ChangePasswordSection.tsx b/frontend/src/pages/user/PersonalSettingsPage/components/ChangePasswordSection/ChangePasswordSection.tsx
index 9c34693df..d6842c755 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;
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({});
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 ac4c5bea2..dcf35e672 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 (
-
+ {encryptionVersion === UserEncryptionVersion.V1 && }
);