diff --git a/frontend/src/components/signup/UserInfoStep.tsx b/frontend/src/components/signup/UserInfoStep.tsx index c98998e35..705b490bb 100644 --- a/frontend/src/components/signup/UserInfoStep.tsx +++ b/frontend/src/components/signup/UserInfoStep.tsx @@ -161,6 +161,7 @@ export default function UserInfoStep({ const response = await completeAccountSignup({ email, + password, firstName: name.split(" ")[0], lastName: name.split(" ").slice(1).join(" "), protectedKey, diff --git a/frontend/src/components/utilities/attemptCliLogin.ts b/frontend/src/components/utilities/attemptCliLogin.ts index 8f7c4bb9f..6eba08d61 100644 --- a/frontend/src/components/utilities/attemptCliLogin.ts +++ b/frontend/src/components/utilities/attemptCliLogin.ts @@ -71,6 +71,7 @@ const attemptLogin = async ({ tag } = await login2({ email, + password, clientProof, providerAuthToken, captchaToken diff --git a/frontend/src/components/utilities/attemptLogin.ts b/frontend/src/components/utilities/attemptLogin.ts index b909b1ba7..120ae62ec 100644 --- a/frontend/src/components/utilities/attemptLogin.ts +++ b/frontend/src/components/utilities/attemptLogin.ts @@ -62,6 +62,7 @@ const attemptLogin = async ({ } = await login2({ captchaToken, email, + password, clientProof, providerAuthToken }); diff --git a/frontend/src/hooks/api/auth/index.tsx b/frontend/src/hooks/api/auth/index.tsx index 505f7b05f..66688cbdc 100644 --- a/frontend/src/hooks/api/auth/index.tsx +++ b/frontend/src/hooks/api/auth/index.tsx @@ -1,5 +1,6 @@ export { useGetAuthToken, + useOauthTokenExchange, useResetPassword, useSelectOrganization, useSendMfaToken, @@ -7,4 +8,5 @@ export { useSendVerificationEmail, useVerifyMfaToken, useVerifyPasswordResetCode, - useVerifySignupEmailVerificationCode} from "./queries"; + useVerifySignupEmailVerificationCode +} from "./queries"; diff --git a/frontend/src/hooks/api/auth/queries.tsx b/frontend/src/hooks/api/auth/queries.tsx index cba815fae..fcec4f2ef 100644 --- a/frontend/src/hooks/api/auth/queries.tsx +++ b/frontend/src/hooks/api/auth/queries.tsx @@ -23,6 +23,7 @@ import { SendMfaTokenDTO, SRP1DTO, SRPR1Res, + TOauthTokenExchangeDTO, VerifyMfaTokenDTO, VerifyMfaTokenRes, VerifySignupInviteDTO @@ -92,6 +93,7 @@ export const useLogin2 = () => { mutationFn: async (details: { email: string; clientProof: string; + password: string; providerAuthToken?: string; }) => { return login2(details); @@ -99,6 +101,20 @@ export const useLogin2 = () => { }); }; +export const oauthTokenExchange = async (details: TOauthTokenExchangeDTO) => { + const { data } = await apiRequest.post("/api/v1/sso/token-exchange", details); + return data; +}; + +export const useOauthTokenExchange = () => { + // note: use after srp1 + return useMutation({ + mutationFn: async (details: TOauthTokenExchangeDTO) => { + return oauthTokenExchange(details); + } + }); +}; + export const srp1 = async (details: SRP1DTO) => { const { data } = await apiRequest.post("/api/v1/password/srp1", details); return data; diff --git a/frontend/src/hooks/api/auth/types.ts b/frontend/src/hooks/api/auth/types.ts index ce1b18bc8..a6bd63342 100644 --- a/frontend/src/hooks/api/auth/types.ts +++ b/frontend/src/hooks/api/auth/types.ts @@ -23,6 +23,11 @@ export type VerifyMfaTokenRes = { tag: string; }; +export type TOauthTokenExchangeDTO = { + providerAuthToken: string; + email: string; +}; + export type Login1DTO = { email: string; clientPublicKey: string; @@ -34,6 +39,7 @@ export type Login2DTO = { email: string; clientProof: string; providerAuthToken?: string; + password: string; }; export type Login1Res = { @@ -86,6 +92,7 @@ export type CompleteAccountDTO = { encryptedPrivateKeyTag: string; salt: string; verifier: string; + password: string; }; export type CompleteAccountSignupDTO = CompleteAccountDTO & { diff --git a/frontend/src/hooks/api/users/queries.tsx b/frontend/src/hooks/api/users/queries.tsx index a443c6750..fa0b932ea 100644 --- a/frontend/src/hooks/api/users/queries.tsx +++ b/frontend/src/hooks/api/users/queries.tsx @@ -20,6 +20,7 @@ import { export const userKeys = { getUser: ["user"] as const, + getPrivateKey: ["user"] as const, userAction: ["user-action"] as const, getOrgUsers: (orgId: string) => [{ orgId }, "user"], myIp: ["ip"] as const, @@ -351,3 +352,11 @@ export const useGetMyOrganizationProjects = (orgId: string) => { enabled: true }); }; + +export const fetchMyPrivateKey = async () => { + const { + data: { privateKey } + } = await apiRequest.get<{ privateKey: string }>("/api/v1/user/private-key"); + + return privateKey; +}; diff --git a/frontend/src/pages/signupinvite.tsx b/frontend/src/pages/signupinvite.tsx index 17ac5b4ff..061e1bc70 100644 --- a/frontend/src/pages/signupinvite.tsx +++ b/frontend/src/pages/signupinvite.tsx @@ -149,6 +149,7 @@ export default function SignupInvite() { const { token: jwtToken } = await completeAccountSignupInvite({ email, + password, firstName, lastName, protectedKey, diff --git a/frontend/src/views/Login/components/MFAStep/MFAStep.tsx b/frontend/src/views/Login/components/MFAStep/MFAStep.tsx index b23af1cef..fa443fe78 100644 --- a/frontend/src/views/Login/components/MFAStep/MFAStep.tsx +++ b/frontend/src/views/Login/components/MFAStep/MFAStep.tsx @@ -9,13 +9,12 @@ import Error from "@app/components/basic/Error"; import { createNotification } from "@app/components/notifications"; import attemptCliLoginMfa from "@app/components/utilities/attemptCliLoginMfa"; import attemptLoginMfa from "@app/components/utilities/attemptLoginMfa"; +import SecurityClient from "@app/components/utilities/SecurityClient"; import { Button } from "@app/components/v2"; -import { useUpdateUserAuthMethods } from "@app/hooks/api"; import { useSendMfaToken } from "@app/hooks/api/auth"; -import { useSelectOrganization } from "@app/hooks/api/auth/queries"; +import { useSelectOrganization, verifyMfaToken } from "@app/hooks/api/auth/queries"; import { fetchOrganizations } from "@app/hooks/api/organization/queries"; -import { fetchUserDetails } from "@app/hooks/api/users/queries"; -import { AuthMethod } from "@app/hooks/api/users/types"; +import { fetchMyPrivateKey } from "@app/hooks/api/users/queries"; import { navigateUserToOrg, navigateUserToSelectOrg } from "../../Login.utils"; @@ -56,15 +55,62 @@ export const MFAStep = ({ email, password, providerAuthToken }: Props) => { const { t } = useTranslation(); const sendMfaToken = useSendMfaToken(); - const { mutateAsync: updateUserAuthMethodsMutateAsync } = useUpdateUserAuthMethods(); const { mutateAsync: selectOrganization } = useSelectOrganization(); + // They don't have password + const handleLoginMfaOauth = async (callbackPort: string, organizationId?: string) => { + setIsLoading(true); + if (callbackPort) { + // attemptCliLogin + 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(); + await instance.post(cliUrl, { + email, + privateKey, + JTWToken: newJwtToken + }); + } + 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) { + navigateUserToSelectOrg(router, 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 isLinkingRequired: undefined | boolean; let callbackPort: undefined | string; - let authMethod: undefined | AuthMethod; let organizationId: undefined | string; + let hasExchangedPrivateKey: undefined | boolean; const queryParams = new URLSearchParams(window.location.search); @@ -73,10 +119,9 @@ export const MFAStep = ({ email, password, providerAuthToken }: Props) => { if (providerAuthToken) { const decodedToken = jwt_decode(providerAuthToken) as any; - isLinkingRequired = decodedToken.isLinkingRequired; callbackPort = decodedToken.callbackPort; - authMethod = decodedToken.authMethod; organizationId = decodedToken?.organizationId; + hasExchangedPrivateKey = decodedToken?.hasExchangedPrivateKey; } if (mfaCode.length !== 6) { @@ -87,6 +132,11 @@ export const MFAStep = ({ email, password, providerAuthToken }: Props) => { return; } + if (hasExchangedPrivateKey) { + await handleLoginMfaOauth(callbackPort as string, organizationId); + return; + } + setIsLoading(true); if (callbackPort) { // attemptCliLogin @@ -145,14 +195,6 @@ export const MFAStep = ({ email, password, providerAuthToken }: Props) => { type: "success" }); - if (isLinkingRequired && authMethod) { - const user = await fetchUserDetails(); - const newAuthMethods = [...user.authMethods, authMethod]; - await updateUserAuthMethodsMutateAsync({ - authMethods: newAuthMethods - }); - } - if (organizationId) { await navigateUserToOrg(router, organizationId); } else { diff --git a/frontend/src/views/Login/components/PasswordStep/PasswordStep.tsx b/frontend/src/views/Login/components/PasswordStep/PasswordStep.tsx index 2ba90528f..06438a2f8 100644 --- a/frontend/src/views/Login/components/PasswordStep/PasswordStep.tsx +++ b/frontend/src/views/Login/components/PasswordStep/PasswordStep.tsx @@ -1,4 +1,4 @@ -import { useRef, useState } from "react"; +import { useEffect, useRef,useState } from "react"; import { useTranslation } from "react-i18next"; import Link from "next/link"; import { useRouter } from "next/router"; @@ -10,11 +10,11 @@ import { createNotification } from "@app/components/notifications"; import attemptCliLogin from "@app/components/utilities/attemptCliLogin"; import attemptLogin from "@app/components/utilities/attemptLogin"; import { CAPTCHA_SITE_KEY } from "@app/components/utilities/config"; -import { Button, Input } from "@app/components/v2"; -import { useUpdateUserAuthMethods } from "@app/hooks/api"; -import { useSelectOrganization } from "@app/hooks/api/auth/queries"; +import SecurityClient from "@app/components/utilities/SecurityClient"; +import { Button, Input, Spinner } from "@app/components/v2"; +import { useOauthTokenExchange, useSelectOrganization } from "@app/hooks/api"; import { fetchOrganizations } from "@app/hooks/api/organization/queries"; -import { fetchUserDetails } from "@app/hooks/api/users/queries"; +import { fetchMyPrivateKey } from "@app/hooks/api/users/queries"; import { navigateUserToOrg, navigateUserToSelectOrg } from "../../Login.utils"; @@ -36,12 +36,94 @@ export const PasswordStep = ({ const [isLoading, setIsLoading] = useState(false); const { t } = useTranslation(); const router = useRouter(); - const { mutateAsync } = useUpdateUserAuthMethods(); const { mutateAsync: selectOrganization } = useSelectOrganization(); + const { mutateAsync: oauthTokenExchange } = useOauthTokenExchange(); - const { callbackPort, isLinkingRequired, authMethod, organizationId } = jwt_decode( - providerAuthToken - ) as any; + const { callbackPort, organizationId, hasExchangedPrivateKey } = + jwt_decode(providerAuthToken) as any; + + const handleExchange = async () => { + try { + setIsLoading(true); + const oauthLogin = await oauthTokenExchange({ + email, + providerAuthToken + }); + + // 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 + SecurityClient.setToken(oauthLogin.token); + + 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) { + console.log("organization id was present. new JWT token to be used in CLI:", newJwtToken); + const instance = axios.create(); + await instance.post(cliUrl, { + privateKey, + email, + JTWToken: newJwtToken + }); + } + + 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) { + navigateUserToSelectOrg(router, callbackPort); + } + // case: no orgs found, so we navigate the user to create an org + else { + await navigateUserToOrg(router); + } + } + } catch (err: any) { + setIsLoading(false); + console.error(err); + + if (err.response.data.error === "User Locked") { + createNotification({ + title: err.response.data.error, + text: err.response.data.message, + type: "error" + }); + return; + } + + createNotification({ + text: "Login unsuccessful. Double-check your master password and try again.", + type: "error" + }); + } + }; + + useEffect(() => { + if (hasExchangedPrivateKey) { + handleExchange(); + } + }, []); const [captchaToken, setCaptchaToken] = useState(""); const [shouldShowCaptcha, setShouldShowCaptcha] = useState(false); @@ -128,14 +210,6 @@ export const PasswordStep = ({ type: "success" }); - if (isLinkingRequired) { - const user = await fetchUserDetails(); - const newAuthMethods = [...user.authMethods, authMethod]; - await mutateAsync({ - authMethods: newAuthMethods - }); - } - // case: organization ID is present from the provider auth token -- navigate directly to the org if (organizationId) { await navigateUserToOrg(router, organizationId); @@ -183,20 +257,21 @@ export const PasswordStep = ({ setCaptchaToken(""); }; + if (hasExchangedPrivateKey) { + return ( +
+ +

Loading, please wait

+
+ ); + } + return (

- {isLinkingRequired ? "Link your account" : "What's your Infisical password?"} + What's your Infisical password?

- {isLinkingRequired && ( -
- - An existing account without this SSO authentication method enabled was found under the - same email. Login with your password to link the account. - -
- )}
diff --git a/frontend/src/views/Signup/components/UserInfoSSOStep/UserInfoSSOStep.tsx b/frontend/src/views/Signup/components/UserInfoSSOStep/UserInfoSSOStep.tsx index 69168e0af..e718d9851 100644 --- a/frontend/src/views/Signup/components/UserInfoSSOStep/UserInfoSSOStep.tsx +++ b/frontend/src/views/Signup/components/UserInfoSSOStep/UserInfoSSOStep.tsx @@ -2,14 +2,10 @@ import crypto from "crypto"; import React, { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; -import { faInfoCircle, faXmark } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import jsrp from "jsrp"; import nacl from "tweetnacl"; import { encodeBase64 } from "tweetnacl-util"; -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 { saveTokenToLocalStorage } from "@app/components/utilities/saveTokenToLocalStorage"; @@ -32,17 +28,6 @@ type Props = { providerAuthToken?: string; }; -type Errors = { - tooShort?: string; - tooLong?: string; - noLetterChar?: string; - noNumOrSpecialChar?: string; - repeatedChar?: string; - escapeChar?: string; - lowEntropy?: string; - breached?: string; -}; - /** * This is the step of the sign up flow where people provife their name/surname and password * @param {object} obj @@ -69,12 +54,13 @@ export const UserInfoSSOStep = ({ const [organizationName, setOrganizationName] = useState(""); const [organizationNameError, setOrganizationNameError] = useState(false); const [attributionSource, setAttributionSource] = useState(""); - const [errors, setErrors] = useState({}); const [isLoading, setIsLoading] = useState(false); const { t } = useTranslation(); const { mutateAsync: selectOrganization } = useSelectOrganization(); useEffect(() => { + const randomPassword = crypto.randomBytes(32).toString("hex"); + setPassword(randomPassword); if (providerOrganizationName !== undefined) { setOrganizationName(providerOrganizationName); } @@ -98,11 +84,6 @@ export const UserInfoSSOStep = ({ setOrganizationNameError(false); } - errorCheck = await checkPassword({ - password, - setErrors - }); - if (!errorCheck) { // Generate a random pair of a public and a private key const pair = nacl.box.keyPair(); @@ -158,6 +139,7 @@ export const UserInfoSSOStep = ({ const response = await completeAccountSignup({ email: username, + password, firstName: name.split(" ")[0], lastName: name.split(" ").slice(1).join(" "), protectedKey, @@ -272,53 +254,6 @@ export const UserInfoSSOStep = ({ />
)} -
- { - setPassword(pass); - await checkPassword({ - password: pass, - setErrors - }); - }} - type="password" - value={password} - isRequired - error={Object.keys(errors).length > 0} - autoComplete="new-password" - id="new-password" - /> -
- - Infisical Password is used as part of the encryption mechanism so that even the - authentication provider is not able to access your secrets. -
- {Object.keys(errors).length > 0 && ( -
-
- {t("section.password.validate-base")} -
- {Object.keys(errors).map((key) => { - if (errors[key as keyof Errors]) { - return ( -
-
- -
-

{errors[key as keyof Errors]}

-
- ); - } - - return null; - })} -
- )} -