diff --git a/backend/src/services/auth/auth-login-service.ts b/backend/src/services/auth/auth-login-service.ts index 39f3162d1..83da4724e 100644 --- a/backend/src/services/auth/auth-login-service.ts +++ b/backend/src/services/auth/auth-login-service.ts @@ -354,7 +354,7 @@ export const authLoginServiceFactory = ({ } // send multi factor auth token if they it enabled - if (user.isMfaEnabled && user.email && !decodedToken.isMfaVerified) { + if ((selectedOrg.enforceMfa || user.isMfaEnabled) && user.email && !decodedToken.isMfaVerified) { enforceUserLockStatus(Boolean(user.isLocked), user.temporaryLockDateEnd); const mfaToken = jwt.sign( diff --git a/frontend/src/hooks/api/auth/queries.tsx b/frontend/src/hooks/api/auth/queries.tsx index 91c2e7be1..dce930418 100644 --- a/frontend/src/hooks/api/auth/queries.tsx +++ b/frontend/src/hooks/api/auth/queries.tsx @@ -79,7 +79,7 @@ export const useSelectOrganization = () => { const data = await selectOrganization(details); // If a custom user agent is set, then this session is meant for another consuming application, not the web application. - if (!details.userAgent) { + if (!details.userAgent && !data.isMfaEnabled) { SecurityClient.setToken(data.token); SecurityClient.setProviderAuthToken(""); } diff --git a/frontend/src/layouts/AppLayout/AppLayout.tsx b/frontend/src/layouts/AppLayout/AppLayout.tsx index 6e5c18ea0..f0737c32d 100644 --- a/frontend/src/layouts/AppLayout/AppLayout.tsx +++ b/frontend/src/layouts/AppLayout/AppLayout.tsx @@ -5,7 +5,7 @@ /* eslint-disable no-var */ /* eslint-disable func-names */ -import { useEffect, useMemo } from "react"; +import { useEffect, useMemo, useState } from "react"; import { Controller, useForm } from "react-hook-form"; import { useTranslation } from "react-i18next"; import Link from "next/link"; @@ -35,6 +35,7 @@ import * as yup from "yup"; import { createNotification } from "@app/components/notifications"; import { OrgPermissionCan } from "@app/components/permissions"; import { tempLocalStorage } from "@app/components/utilities/checks/tempLocalStorage"; +import SecurityClient from "@app/components/utilities/SecurityClient"; import { Accordion, AccordionContent, @@ -64,7 +65,7 @@ import { useUser, useWorkspace } from "@app/context"; -import { usePopUp } from "@app/hooks"; +import { usePopUp, useToggle } from "@app/hooks"; import { fetchOrgUsers, useAddUserToWsNonE2EE, @@ -82,6 +83,7 @@ import { useUpdateUserProjectFavorites } from "@app/hooks/api/users/mutation"; import { useGetUserProjectFavorites } from "@app/hooks/api/users/queries"; import { AuthMethod } from "@app/hooks/api/users/types"; import { navigateUserToOrg } from "@app/views/Login/Login.utils"; +import { Mfa } from "@app/views/Login/Mfa"; import { CreateOrgModal } from "@app/views/Org/components"; import { WishForm } from "./components/WishForm/WishForm"; @@ -136,6 +138,8 @@ export const AppLayout = ({ children }: LayoutProps) => { const { data: projectFavorites } = useGetUserProjectFavorites(currentOrg?.id!); const { mutateAsync: updateUserProjectFavorites } = useUpdateUserProjectFavorites(); + const [shouldShowMfa, toggleShowMfa] = useToggle(false); + const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {}); const workspacesWithFaveProp = useMemo( () => @@ -206,10 +210,17 @@ export const AppLayout = ({ children }: LayoutProps) => { }; const changeOrg = async (orgId: string) => { - await selectOrganization({ + const { token, isMfaEnabled } = await selectOrganization({ organizationId: orgId }); + if (isMfaEnabled) { + SecurityClient.setMfaToken(token); + toggleShowMfa.on(); + setMfaSuccessCallback(() => () => changeOrg(orgId)); + return; + } + await navigateUserToOrg(router, orgId); }; @@ -334,6 +345,18 @@ export const AppLayout = ({ children }: LayoutProps) => { } }; + if (shouldShowMfa) { + return ( +
+ toggleShowMfa.off()} + /> +
+ ); + } + return ( <>
@@ -749,7 +772,8 @@ export const AppLayout = ({ children }: LayoutProps) => { - {(window.location.origin.includes("https://app.infisical.com") || window.location.origin.includes("https://eu.infisical.com") || + {(window.location.origin.includes("https://app.infisical.com") || + window.location.origin.includes("https://eu.infisical.com") || window.location.origin.includes("https://gamma.infisical.com")) && ( diff --git a/frontend/src/pages/login/select-organization.tsx b/frontend/src/pages/login/select-organization.tsx index 319a6d6df..d4b3236e2 100644 --- a/frontend/src/pages/login/select-organization.tsx +++ b/frontend/src/pages/login/select-organization.tsx @@ -46,7 +46,9 @@ export default function LoginPage() { const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {}); const queryParams = new URLSearchParams(window.location.search); + const orgId = queryParams.get("org_id"); const callbackPort = queryParams.get("callback_port"); + const defaultSelectedOrg = organizations.data?.find((org) => org.id === orgId); const logout = useLogoutUser(true); const handleLogout = useCallback(async () => { @@ -179,6 +181,12 @@ export default function LoginPage() { } }, [organizations.isLoading, organizations.data]); + useEffect(() => { + if (defaultSelectedOrg) { + handleSelectOrganization(defaultSelectedOrg); + } + }, [defaultSelectedOrg]); + if (userLoading || !user) { return ; } @@ -193,7 +201,11 @@ export default function LoginPage() { {shouldShowMfa ? ( - toggleShowMfa.off()} /> + toggleShowMfa.off()} + /> ) : (
diff --git a/frontend/src/pages/signupinvite.tsx b/frontend/src/pages/signupinvite.tsx index c05a2ef18..7ab132a99 100644 --- a/frontend/src/pages/signupinvite.tsx +++ b/frontend/src/pages/signupinvite.tsx @@ -23,6 +23,7 @@ import issueBackupKey from "@app/components/utilities/cryptography/issueBackupKe import { saveTokenToLocalStorage } from "@app/components/utilities/saveTokenToLocalStorage"; import SecurityClient from "@app/components/utilities/SecurityClient"; import { useServerConfig } from "@app/context"; +import { useToggle } from "@app/hooks"; import { completeAccountSignupInvite, useSelectOrganization, @@ -57,6 +58,8 @@ export default function SignupInvite() { const [backupKeyIssued, setBackupKeyIssued] = useState(false); const [errors, setErrors] = useState({}); + const [shouldShowMfa, toggleShowMfa] = useToggle(false); + const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {}); const router = useRouter(); const parsedUrl = queryString.parse(router.asPath.split("?")[1]); const token = parsedUrl.token as string; @@ -180,11 +183,24 @@ export default function SignupInvite() { if (!orgId) throw new Error("You are not part of any organization"); - await selectOrganization({ organizationId: orgId }); + const completeSignupFlow = async () => { + const { token: mfaToken, isMfaEnabled } = await selectOrganization({ + organizationId: orgId + }); - localStorage.setItem("orgData.id", orgId); + if (isMfaEnabled) { + SecurityClient.setMfaToken(mfaToken); + toggleShowMfa.on(); + setMfaSuccessCallback(() => completeSignupFlow); + return; + } - setStep(3); + localStorage.setItem("orgData.id", orgId); + + setStep(3); + }; + + await completeSignupFlow(); } catch (error) { setIsLoading(false); console.error(error); @@ -222,11 +238,24 @@ export default function SignupInvite() { SecurityClient.setSignupToken(response.token); setStep(2); } else { - await selectOrganization({ organizationId }); + const redirectExistingUser = async () => { + const { token: mfaToken, isMfaEnabled } = await selectOrganization({ + organizationId + }); - // user will be redirected to dashboard - // if not logged in gets kicked out to login - await navigateUserToOrg(router, organizationId); + if (isMfaEnabled) { + SecurityClient.setMfaToken(mfaToken); + toggleShowMfa.on(); + setMfaSuccessCallback(() => redirectExistingUser); + return; + } + + // user will be redirected to dashboard + // if not logged in gets kicked out to login + await navigateUserToOrg(router, organizationId); + }; + + await redirectExistingUser(); } } } catch (err) { diff --git a/frontend/src/views/Login/Login.tsx b/frontend/src/views/Login/Login.tsx index 36cd355f5..05b81b4f2 100644 --- a/frontend/src/views/Login/Login.tsx +++ b/frontend/src/views/Login/Login.tsx @@ -2,7 +2,7 @@ import { useEffect, useState } from "react"; import { isLoggedIn } from "@app/reactQuery"; -import { InitialStep, MFAStep, SSOStep } from "./components"; +import { InitialStep, SSOStep } from "./components"; import { useNavigateToSelectOrganization } from "./Login.utils"; export const Login = () => { @@ -46,15 +46,6 @@ export const Login = () => { setPassword={setPassword} /> ); - case 1: - return ( - - ); case 2: return ; case 3: diff --git a/frontend/src/views/Login/Login.utils.tsx b/frontend/src/views/Login/Login.utils.tsx index 9aa726d6b..b0cd3d031 100644 --- a/frontend/src/views/Login/Login.utils.tsx +++ b/frontend/src/views/Login/Login.utils.tsx @@ -1,7 +1,6 @@ import { NextRouter, useRouter } from "next/router"; import { useServerConfig } from "@app/context"; -import { useSelectOrganization } from "@app/hooks/api"; import { fetchOrganizations } from "@app/hooks/api/organization/queries"; import { userKeys } from "@app/hooks/api/users"; import { queryClient } from "@app/reactQuery"; @@ -31,23 +30,18 @@ export const navigateUserToOrg = async (router: NextRouter, organizationId?: str export const useNavigateToSelectOrganization = () => { const { config } = useServerConfig(); - const selectOrganization = useSelectOrganization(); const router = useRouter(); const navigate = async (cliCallbackPort?: string) => { + let redirectTo = "/login/select-organization?"; if (config.defaultAuthOrgId) { - await selectOrganization.mutateAsync({ - organizationId: config.defaultAuthOrgId - }); - - await navigateUserToOrg(router, config.defaultAuthOrgId); + redirectTo += `org_id=${config.defaultAuthOrgId}&`; + } else { + queryClient.invalidateQueries(userKeys.getUser); } - queryClient.invalidateQueries(userKeys.getUser); - let redirectTo = "/login/select-organization"; - if (cliCallbackPort) { - redirectTo += `?callback_port=${cliCallbackPort}`; + redirectTo += `callback_port=${cliCallbackPort}`; } router.push(redirectTo, undefined, { shallow: true }); diff --git a/frontend/src/views/Login/LoginSSO.tsx b/frontend/src/views/Login/LoginSSO.tsx index a182a2713..5d81fdaec 100644 --- a/frontend/src/views/Login/LoginSSO.tsx +++ b/frontend/src/views/Login/LoginSSO.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from "react"; import jwt_decode from "jwt-decode"; -import { MFAStep, PasswordStep } from "./components"; +import { PasswordStep } from "./components"; type Props = { providerAuthToken: string; @@ -33,10 +33,6 @@ export const LoginSSO = ({ providerAuthToken }: Props) => { setStep={setStep} /> ); - case 2: - return ( - - ); default: return
; } diff --git a/frontend/src/views/Login/Mfa.tsx b/frontend/src/views/Login/Mfa.tsx index 677f0d22a..cd8d64059 100644 --- a/frontend/src/views/Login/Mfa.tsx +++ b/frontend/src/views/Login/Mfa.tsx @@ -8,7 +8,6 @@ import { t } from "i18next"; import Error from "@app/components/basic/Error"; import SecurityClient from "@app/components/utilities/SecurityClient"; import { Button } from "@app/components/v2"; -import { useUser } from "@app/context"; import { useSendMfaToken } from "@app/hooks/api"; import { verifyMfaToken } from "@app/hooks/api/auth/queries"; @@ -35,27 +34,24 @@ const codeInputProps = { type Props = { successCallback: () => void; closeMfa: () => void; + hideLogo?: boolean; + email: string; }; -export const Mfa = ({ successCallback, closeMfa }: Props) => { +export const Mfa = ({ successCallback, closeMfa, hideLogo, email }: Props) => { const [mfaCode, setMfaCode] = useState(""); const router = useRouter(); const [isLoading, setIsLoading] = useState(false); const [isLoadingResend, setIsLoadingResend] = useState(false); const [triesLeft, setTriesLeft] = useState(undefined); - const { user } = useUser(); const sendMfaToken = useSendMfaToken(); const verifyMfa = async () => { - if (!user.email) { - return; - } - setIsLoading(true); try { const { token } = await verifyMfaToken({ - email: user.email, + email, mfaCode }); @@ -84,13 +80,9 @@ export const Mfa = ({ successCallback, closeMfa }: Props) => { }; const handleResendMfaCode = async () => { - if (!user?.email) { - return; - } - try { setIsLoadingResend(true); - await sendMfaToken.mutateAsync({ email: user.email }); + await sendMfaToken.mutateAsync({ email }); setIsLoadingResend(false); } catch (err) { console.error(err); @@ -100,13 +92,15 @@ export const Mfa = ({ successCallback, closeMfa }: Props) => { return (
- -
- Infisical logo -
- + {!hideLogo && ( + +
+ Infisical logo +
+ + )}

{t("mfa.step2-message")}

-

{user.email}

+

{email}

{ - const router = useRouter(); - const [isLoading, setIsLoading] = useState(false); - const [isLoadingResend, setIsLoadingResend] = useState(false); - const [mfaCode, setMfaCode] = useState(""); - const { navigateToSelectOrganization } = useNavigateToSelectOrganization(); - const [triesLeft, setTriesLeft] = useState(undefined); - - const { t } = useTranslation(); - - const sendMfaToken = useSendMfaToken(); - const { mutateAsync: selectOrganization } = useSelectOrganization(); - - // They don't have password - const handleLoginMfaOauth = async (callbackPort: string, organizationId?: string) => { - setIsLoading(true); - const { token } = await verifyMfaToken({ - email, - mfaCode - }); - // - // unset temporary (MFA) JWT token and set JWT token - SecurityClient.setMfaToken(""); - SecurityClient.setToken(token); - SecurityClient.setProviderAuthToken(""); - const privateKey = await fetchMyPrivateKey(); - localStorage.setItem("PRIVATE_KEY", privateKey); - - // case: organization ID is present from the provider auth token -- select the org and use the new jwt token in the CLI, then navigate to the org - if (organizationId) { - const { token: newJwtToken } = await selectOrganization({ organizationId }); - if (callbackPort) { - const cliUrl = `http://127.0.0.1:${callbackPort}/`; - const instance = axios.create(); - const payload = { - email, - privateKey, - JTWToken: newJwtToken - }; - await instance.post(cliUrl, payload).catch(() => { - // if error happens to communicate we set the token with an expiry in sessino storage - // the cli-redirect page has logic to show this to user and ask them to paste it in terminal - sessionStorage.setItem( - SessionStorageKeys.CLI_TERMINAL_TOKEN, - JSON.stringify({ - expiry: formatISO(addSeconds(new Date(), 30)), - data: window.btoa(JSON.stringify(payload)) - }) - ); - }); - router.push("/cli-redirect"); - return; - } - await navigateUserToOrg(router, organizationId); - } - // case: no organization ID is present -- navigate to the select org page IF the user has any orgs - // if the user has no orgs, navigate to the create org page - else { - const userOrgs = await fetchOrganizations(); - - // case: user has orgs, so we navigate the user to select an org - if (userOrgs.length > 0) { - navigateToSelectOrganization(callbackPort); - } - // case: no orgs found, so we navigate the user to create an org - // cli login will fail in this case - else { - await navigateUserToOrg(router); - } - } - }; - - const handleLoginMfa = async () => { - try { - let callbackPort: undefined | string; - let organizationId: undefined | string; - let hasExchangedPrivateKey: undefined | boolean; - - const queryParams = new URLSearchParams(window.location.search); - - callbackPort = queryParams.get("callback_port") || undefined; - - if (providerAuthToken) { - const decodedToken = jwt_decode(providerAuthToken) as any; - - callbackPort = decodedToken.callbackPort; - organizationId = decodedToken?.organizationId; - hasExchangedPrivateKey = decodedToken?.hasExchangedPrivateKey; - } - - if (mfaCode.length !== 6) { - createNotification({ - text: "Please enter a 6-digit MFA code and try again", - type: "error" - }); - return; - } - - if (hasExchangedPrivateKey) { - await handleLoginMfaOauth(callbackPort as string, organizationId); - return; - } - - setIsLoading(true); - if (callbackPort) { - // attemptCliLogin - const isCliLoginSuccessful = await attemptCliLoginMfa({ - email, - password, - providerAuthToken, - mfaToken: mfaCode - }); - - if (isCliLoginSuccessful && isCliLoginSuccessful.success) { - const cliUrl = `http://127.0.0.1:${callbackPort}/`; - - // case: organization ID is present from the provider auth token -- select the org and use the new jwt token in the CLI, then navigate to the org - if (organizationId) { - const { token: newJwtToken } = await selectOrganization({ organizationId }); - - const instance = axios.create(); - const payload = { - ...isCliLoginSuccessful.loginResponse, - JTWToken: newJwtToken - }; - await instance.post(cliUrl, payload).catch(() => { - // if error happens to communicate we set the token with an expiry in sessino storage - // the cli-redirect page has logic to show this to user and ask them to paste it in terminal - sessionStorage.setItem( - SessionStorageKeys.CLI_TERMINAL_TOKEN, - JSON.stringify({ - expiry: formatISO(addSeconds(new Date(), 30)), - data: window.btoa(JSON.stringify(payload)) - }) - ); - }); - router.push("/cli-redirect"); - return; - } - // case: no organization ID is present -- navigate to the select org page IF the user has any orgs - // if the user has no orgs, navigate to the create org page - - const userOrgs = await fetchOrganizations(); - - // case: user has orgs, so we navigate the user to select an org - if (userOrgs.length > 0) { - navigateToSelectOrganization(callbackPort); - } - // case: no orgs found, so we navigate the user to create an org - // cli login will fail in this case - else { - await navigateUserToOrg(router); - } - - } - } else { - const isLoginSuccessful = await attemptLoginMfa({ - email, - password, - providerAuthToken, - mfaToken: mfaCode - }); - - if (isLoginSuccessful) { - setIsLoading(false); - - // case: login does not require MFA step - createNotification({ - text: "Successfully logged in", - type: "success" - }); - - if (organizationId) { - await navigateUserToOrg(router, organizationId); - } else { - navigateToSelectOrganization(); - } - } else { - createNotification({ - text: "Failed to log in", - type: "error" - }); - } - } - } catch (err: any) { - if (err.response.data.error === "User Locked") { - createNotification({ - title: err.response.data.error, - text: err.response.data.message, - type: "error" - }); - setIsLoading(false); - return; - } - - createNotification({ - text: "Failed to log in", - type: "error" - }); - - if (triesLeft) { - setTriesLeft((left) => { - if (triesLeft === 1) { - router.push("/"); - } - return (left as number) - 1; - }); - } else { - setTriesLeft(2); - } - - setIsLoading(false); - } - }; - - const handleResendMfaCode = async () => { - try { - setIsLoadingResend(true); - await sendMfaToken.mutateAsync({ email }); - setIsLoadingResend(false); - } catch (err) { - console.error(err); - setIsLoadingResend(false); - } - }; - - return ( -
-

{t("mfa.step2-message")}

-

{email}

-
- -
-
- -
- {typeof triesLeft === "number" && ( - - )} -
-
- -
-
-
-
- {t("signup.step2-resend-alert")} -
- -
-
-

{t("signup.step2-spam-alert")}

-
- - ); -}; diff --git a/frontend/src/views/Login/components/MFAStep/index.tsx b/frontend/src/views/Login/components/MFAStep/index.tsx deleted file mode 100644 index 4062f25d1..000000000 --- a/frontend/src/views/Login/components/MFAStep/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { MFAStep } from "./MFAStep"; diff --git a/frontend/src/views/Login/components/PasswordStep/PasswordStep.tsx b/frontend/src/views/Login/components/PasswordStep/PasswordStep.tsx index 16a06f9b7..e1dc88312 100644 --- a/frontend/src/views/Login/components/PasswordStep/PasswordStep.tsx +++ b/frontend/src/views/Login/components/PasswordStep/PasswordStep.tsx @@ -14,11 +14,13 @@ import { CAPTCHA_SITE_KEY } from "@app/components/utilities/config"; import SecurityClient from "@app/components/utilities/SecurityClient"; import { Button, Input, Spinner } from "@app/components/v2"; import { SessionStorageKeys } from "@app/const"; +import { useToggle } from "@app/hooks"; import { useOauthTokenExchange, useSelectOrganization } from "@app/hooks/api"; import { fetchOrganizations } from "@app/hooks/api/organization/queries"; import { fetchMyPrivateKey } from "@app/hooks/api/users/queries"; import { navigateUserToOrg, useNavigateToSelectOrganization } from "../../Login.utils"; +import { Mfa } from "../../Mfa"; type Props = { providerAuthToken: string; @@ -40,6 +42,8 @@ export const PasswordStep = ({ const router = useRouter(); const { mutateAsync: selectOrganization } = useSelectOrganization(); const { mutateAsync: oauthTokenExchange } = useOauthTokenExchange(); + const [shouldShowMfa, toggleShowMfa] = useToggle(false); + const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {}); const { navigateToSelectOrganization } = useNavigateToSelectOrganization(); @@ -56,17 +60,8 @@ export const PasswordStep = ({ }); // attemptCliLogin - if (oauthLogin.mfaEnabled) { - SecurityClient.setMfaToken(oauthLogin.token); - // case: login requires MFA step - setStep(2); - setIsLoading(false); - return; - } const cliUrl = `http://127.0.0.1:${callbackPort}/`; - // case: MFA is not enabled - // unset provider auth token in case it was used SecurityClient.setProviderAuthToken(""); // set JWT token @@ -77,31 +72,43 @@ export const PasswordStep = ({ // case: organization ID is present from the provider auth token -- select the org and use the new jwt token in the CLI, then navigate to the org if (organizationId) { - const { token: newJwtToken } = await selectOrganization({ organizationId }); - if (callbackPort) { - console.log("organization id was present. new JWT token to be used in CLI:", newJwtToken); - const instance = axios.create(); - const payload = { - privateKey, - email, - JTWToken: newJwtToken - }; - await instance.post(cliUrl, payload).catch(() => { - // if error happens to communicate we set the token with an expiry in sessino storage - // the cli-redirect page has logic to show this to user and ask them to paste it in terminal - sessionStorage.setItem( - SessionStorageKeys.CLI_TERMINAL_TOKEN, - JSON.stringify({ - expiry: formatISO(addSeconds(new Date(), 30)), - data: window.btoa(JSON.stringify(payload)) - }) - ); - }); - router.push("/cli-redirect"); - return; - } + const finishWithOrgWorkflow = async () => { + const { token, isMfaEnabled } = await selectOrganization({ organizationId }); - await navigateUserToOrg(router, organizationId); + if (isMfaEnabled) { + SecurityClient.setMfaToken(token); + toggleShowMfa.on(); + setMfaSuccessCallback(() => finishWithOrgWorkflow); + return; + } + + if (callbackPort) { + console.log("organization id was present. new JWT token to be used in CLI:", token); + const instance = axios.create(); + const payload = { + privateKey, + email, + JTWToken: token + }; + await instance.post(cliUrl, payload).catch(() => { + // if error happens to communicate we set the token with an expiry in sessino storage + // the cli-redirect page has logic to show this to user and ask them to paste it in terminal + sessionStorage.setItem( + SessionStorageKeys.CLI_TERMINAL_TOKEN, + JSON.stringify({ + expiry: formatISO(addSeconds(new Date(), 30)), + data: window.btoa(JSON.stringify(payload)) + }) + ); + }); + router.push("/cli-redirect"); + return; + } + + await navigateUserToOrg(router, organizationId); + }; + + await finishWithOrgWorkflow(); } // case: no organization ID is present -- navigate to the select org page IF the user has any orgs // if the user has no orgs, navigate to the create org page @@ -172,32 +179,41 @@ export const PasswordStep = ({ // case: organization ID is present from the provider auth token -- select the org and use the new jwt token in the CLI, then navigate to the org if (organizationId) { - const { token: newJwtToken } = await selectOrganization({ organizationId }); + const finishWithOrgWorkflow = async () => { + const { token, isMfaEnabled } = await selectOrganization({ organizationId }); - console.log( - "organization id was present. new JWT token to be used in CLI:", - newJwtToken - ); + if (isMfaEnabled) { + SecurityClient.setMfaToken(token); + toggleShowMfa.on(); + setMfaSuccessCallback(() => finishWithOrgWorkflow); + return; + } - const instance = axios.create(); - const payload = { - ...isCliLoginSuccessful.loginResponse, - JTWToken: newJwtToken + console.log("organization id was present. new JWT token to be used in CLI:", token); + + const instance = axios.create(); + const payload = { + ...isCliLoginSuccessful.loginResponse, + JTWToken: token + }; + await instance.post(cliUrl, payload).catch(() => { + // if error happens to communicate we set the token with an expiry in sessino storage + // the cli-redirect page has logic to show this to user and ask them to paste it in terminal + sessionStorage.setItem( + SessionStorageKeys.CLI_TERMINAL_TOKEN, + JSON.stringify({ + expiry: formatISO(addSeconds(new Date(), 30)), + data: window.btoa(JSON.stringify(payload)) + }) + ); + }); + router.push("/cli-redirect"); }; - await instance.post(cliUrl, payload).catch(() => { - // if error happens to communicate we set the token with an expiry in sessino storage - // the cli-redirect page has logic to show this to user and ask them to paste it in terminal - sessionStorage.setItem( - SessionStorageKeys.CLI_TERMINAL_TOKEN, - JSON.stringify({ - expiry: formatISO(addSeconds(new Date(), 30)), - data: window.btoa(JSON.stringify(payload)) - }) - ); - }); - router.push("/cli-redirect"); + + await finishWithOrgWorkflow(); return; } + // case: no organization ID is present -- navigate to the select org page IF the user has any orgs // if the user has no orgs, navigate to the create org page const userOrgs = await fetchOrganizations(); @@ -284,6 +300,18 @@ export const PasswordStep = ({ setCaptchaToken(""); }; + if (shouldShowMfa) { + return ( +
+ toggleShowMfa.off()} + /> +
+ ); + } + if (hasExchangedPrivateKey) { return (
diff --git a/frontend/src/views/Login/components/index.tsx b/frontend/src/views/Login/components/index.tsx index 296b0503e..84ad4f73d 100644 --- a/frontend/src/views/Login/components/index.tsx +++ b/frontend/src/views/Login/components/index.tsx @@ -1,5 +1,4 @@ export { InitialStep } from "./InitialStep"; -export { MFAStep } from "./MFAStep"; export { SSOStep } from "./SSOStep"; // SSO-specific step diff --git a/frontend/src/views/Signup/components/UserInfoSSOStep/UserInfoSSOStep.tsx b/frontend/src/views/Signup/components/UserInfoSSOStep/UserInfoSSOStep.tsx index f502d3c6b..5f63c4952 100644 --- a/frontend/src/views/Signup/components/UserInfoSSOStep/UserInfoSSOStep.tsx +++ b/frontend/src/views/Signup/components/UserInfoSSOStep/UserInfoSSOStep.tsx @@ -11,9 +11,11 @@ import { deriveArgonKey } from "@app/components/utilities/cryptography/crypto"; import { saveTokenToLocalStorage } from "@app/components/utilities/saveTokenToLocalStorage"; import SecurityClient from "@app/components/utilities/SecurityClient"; import { Button, Input } from "@app/components/v2"; +import { useToggle } from "@app/hooks"; import { completeAccountSignup, useSelectOrganization } from "@app/hooks/api/auth/queries"; import { fetchOrganizations } from "@app/hooks/api/organization/queries"; import ProjectService from "@app/services/ProjectService"; +import { Mfa } from "@app/views/Login/Mfa"; // eslint-disable-next-line new-cap const client = new jsrp.client(); @@ -54,9 +56,11 @@ export const UserInfoSSOStep = ({ const [organizationName, setOrganizationName] = useState(""); const [organizationNameError, setOrganizationNameError] = useState(false); const [attributionSource, setAttributionSource] = useState(""); + const [shouldShowMfa, toggleShowMfa] = useToggle(false); const [isLoading, setIsLoading] = useState(false); const { t } = useTranslation(); const { mutateAsync: selectOrganization } = useSelectOrganization(); + const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {}); useEffect(() => { const randomPassword = crypto.randomBytes(32).toString("hex"); @@ -172,22 +176,37 @@ export const UserInfoSSOStep = ({ const userOrgs = await fetchOrganizations(); const orgId = userOrgs[0]?.id; - await selectOrganization({ - organizationId: orgId - }); + const completeSignupFlow = async () => { + try { + const { isMfaEnabled, token } = await selectOrganization({ + organizationId: orgId + }); - // only create example project if not joining existing org - if (!providerOrganizationName) { - const project = await ProjectService.initProject({ - projectName: "Example Project" - }); + if (isMfaEnabled) { + SecurityClient.setMfaToken(token); + toggleShowMfa.on(); + setMfaSuccessCallback(() => completeSignupFlow); + return; + } - localStorage.setItem("projectData.id", project.id); - } + // only create example project if not joining existing org + if (!providerOrganizationName) { + const project = await ProjectService.initProject({ + projectName: "Example Project" + }); - localStorage.setItem("orgData.id", orgId); + localStorage.setItem("projectData.id", project.id); + } - setStep(2); + localStorage.setItem("orgData.id", orgId); + setStep(2); + } catch (error) { + setIsLoading(false); + console.error(error); + } + }; + + await completeSignupFlow(); } catch (error) { setIsLoading(false); console.error(error); @@ -206,6 +225,17 @@ export const UserInfoSSOStep = ({ } }, [providerOrganizationName, password]); + if (shouldShowMfa) { + return ( + toggleShowMfa.off()} + /> + ); + } + return (