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 && ( - -
- Infisical logo -
Welcome to Infisical
-
Create your first Super Admin Account
-
-
-
-
- ( - - - - )} - /> - ( - - - - )} - /> -
+ +
+ Infisical logo +
Welcome to Infisical
+
Create your first Super Admin Account
+
+ +
+
( @@ -186,56 +126,66 @@ export const SignUpPage = () => { /> ( - - - )} - /> - ( - - + )} />
- - - - )} - {step === SignupSteps.BackupKey && ( - - - - )} + ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> +
+ + +
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 -

- verify email -
- -
-
- ); - - // Input backup key - const stepInputBackupKey = ( -
-

- Enter your backup key -

-
-

- You can find it in your emergency kit. You had to download the emergency kit during - signup. -

-
-
- -
-
-
- -
-
-
- ); - - // Enter new password - const stepEnterNewPassword = ( -
-

- Enter new password -

-
-

- Make sure you save it somewhere safe. -

-
-
- { - setNewPassword(password); - passwordCheck({ - password, - setPasswordErrorTooShort, - setPasswordErrorTooLong, - setPasswordErrorNoLetterChar, - setPasswordErrorNoNumOrSpecialChar, - setPasswordErrorRepeatedChar, - setPasswordErrorEscapeChar, - setPasswordErrorLowEntropy, - setPasswordErrorBreached - }); - }} - type="password" - value={newPassword} - isRequired - error={ - passwordErrorTooShort && - passwordErrorTooLong && - passwordErrorNoLetterChar && - passwordErrorNoNumOrSpecialChar && - passwordErrorRepeatedChar && - passwordErrorEscapeChar && - passwordErrorLowEntropy && - passwordErrorBreached - } - autoComplete="new-password" - id="new-password" - /> -
- {passwordErrorTooShort || - passwordErrorTooLong || - passwordErrorNoLetterChar || - passwordErrorNoNumOrSpecialChar || - passwordErrorRepeatedChar || - passwordErrorEscapeChar || - passwordErrorLowEntropy || - passwordErrorBreached ? ( -
-
Password should contain:
-
- {passwordErrorTooShort ? ( - - ) : ( - - )} -
- at least 14 characters -
-
-
- {passwordErrorTooLong ? ( - - ) : ( - - )} -
- at most 100 characters -
-
-
- {passwordErrorNoLetterChar ? ( - - ) : ( - - )} -
- at least 1 letter character -
-
-
- {passwordErrorNoNumOrSpecialChar ? ( - - ) : ( - - )} -
- at least 1 number or special character -
-
-
- {passwordErrorRepeatedChar ? ( - - ) : ( - - )} -
- at most 3 repeated, consecutive characters -
-
-
- {passwordErrorEscapeChar ? ( - - ) : ( - - )} -
- No escape characters allowed. -
-
-
- {passwordErrorLowEntropy ? ( - - ) : ( - - )} -
- Password contains personal info. -
-
-
- {passwordErrorBreached ? ( - - ) : ( - - )} -
- Password was found in a data breach. -
-
-
- ) : ( -
- )} -
-
- -
-
- - ); 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 +

+ verify 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 ( +
+

+ Enter new password +

+
+

+ Make sure you save it somewhere safe. +

+
+
+ ( + + { + field.onChange(e); + handlePasswordCheck(e.target.value); + }} + type="password" + /> + + )} + /> +
+ {passwordErrorTooShort || + passwordErrorTooLong || + passwordErrorNoLetterChar || + passwordErrorNoNumOrSpecialChar || + passwordErrorRepeatedChar || + passwordErrorEscapeChar || + passwordErrorLowEntropy || + passwordErrorBreached ? ( +
+
Password should contain:
+
+ {passwordErrorTooShort ? ( + + ) : ( + + )} +
+ at least 14 characters +
+
+
+ {passwordErrorTooLong ? ( + + ) : ( + + )} +
+ at most 100 characters +
+
+
+ {passwordErrorNoLetterChar ? ( + + ) : ( + + )} +
+ at least 1 letter character +
+
+
+ {passwordErrorNoNumOrSpecialChar ? ( + + ) : ( + + )} +
+ at least 1 number or special character +
+
+
+ {passwordErrorRepeatedChar ? ( + + ) : ( + + )} +
+ at most 3 repeated, consecutive characters +
+
+
+ {passwordErrorEscapeChar ? ( + + ) : ( + + )} +
+ No escape characters allowed. +
+
+
+ {passwordErrorLowEntropy ? ( + + ) : ( + + )} +
+ Password contains personal info. +
+
+
+ {passwordErrorBreached ? ( + + ) : ( + + )} +
+ Password was found in a data breach. +
+
+
+ ) : ( +
+ )} +
+
+ +
+
+ + ); +}; 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 ( +
+

+ Enter your backup key +

+
+

+ You can find it in your emergency kit. You had to download the emergency kit during + signup. +

+
+
+ ( + + + + )} + /> +
+
+
+ +
+
+
+ ); +}; 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 = () => { Infisical Logo
- {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 && }
);