From bc108a82b6977183daf007e81f18013e9f05d63b Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Sun, 13 Aug 2023 22:47:29 +0700 Subject: [PATCH] Add SSO linking feature for existing users --- backend/src/utils/auth.ts | 8 +++- frontend/src/hooks/api/users/queries.tsx | 3 +- frontend/src/views/Login/LoginSSO.tsx | 5 +-- .../Login/components/MFAStep/MFAStep.tsx | 32 ++++++++++++++-- .../components/PasswordStep/PasswordStep.tsx | 38 ++++++++++++++++--- 5 files changed, 70 insertions(+), 16 deletions(-) diff --git a/backend/src/utils/auth.ts b/backend/src/utils/auth.ts index 2516337af..f7a1a504c 100644 --- a/backend/src/utils/auth.ts +++ b/backend/src/utils/auth.ts @@ -106,8 +106,9 @@ const initializePassport = async () => { }).save(); } + let isLinkingRequired = false; if (!user.authMethods.includes(AuthMethod.GOOGLE)) { - done(InternalServerError()); + isLinkingRequired = true; } const isUserCompleted = !!user.publicKey; @@ -119,6 +120,7 @@ const initializePassport = async () => { lastName: user.lastName, authMethod: AuthMethod.GOOGLE, isUserCompleted, + isLinkingRequired, ...(req.query.state ? { callbackPort: req.query.state as string } : {}) @@ -159,8 +161,9 @@ const initializePassport = async () => { }).save(); } + let isLinkingRequired = false; if (!user.authMethods.includes(AuthMethod.GITHUB)) { - done(InternalServerError()); + isLinkingRequired = true; } const isUserCompleted = !!user.publicKey; @@ -172,6 +175,7 @@ const initializePassport = async () => { lastName: user.lastName, authMethod: AuthMethod.GITHUB, isUserCompleted, + isLinkingRequired, ...(req.query.state ? { callbackPort: req.query.state as string } : {}) diff --git a/frontend/src/hooks/api/users/queries.tsx b/frontend/src/hooks/api/users/queries.tsx index e8a7a45f6..90314a1e4 100644 --- a/frontend/src/hooks/api/users/queries.tsx +++ b/frontend/src/hooks/api/users/queries.tsx @@ -20,7 +20,8 @@ import { RenameUserDTO, TokenVersion, UpdateOrgUserRoleDTO, - User} from "./types"; + User +} from "./types"; const userKeys = { getUser: ["user"] as const, diff --git a/frontend/src/views/Login/LoginSSO.tsx b/frontend/src/views/Login/LoginSSO.tsx index 548bee3ae..8140b872f 100644 --- a/frontend/src/views/Login/LoginSSO.tsx +++ b/frontend/src/views/Login/LoginSSO.tsx @@ -16,8 +16,7 @@ export const LoginSSO = ({ providerAuthToken }: Props) => { const { email, - isUserCompleted, - callbackPort + isUserCompleted } = jwt_decode(providerAuthToken) as any; useEffect(() => { @@ -36,7 +35,6 @@ export const LoginSSO = ({ providerAuthToken }: Props) => { return ( { return ( diff --git a/frontend/src/views/Login/components/MFAStep/MFAStep.tsx b/frontend/src/views/Login/components/MFAStep/MFAStep.tsx index c607502c9..571d7ee56 100644 --- a/frontend/src/views/Login/components/MFAStep/MFAStep.tsx +++ b/frontend/src/views/Login/components/MFAStep/MFAStep.tsx @@ -3,14 +3,19 @@ import ReactCodeInput from "react-code-input"; import { useTranslation } from "react-i18next"; import { useRouter } from "next/router"; import axios from "axios" +import jwt_decode from "jwt-decode"; import Error from "@app/components/basic/Error"; // which to notification import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import attemptCliLoginMfa from "@app/components/utilities/attemptCliLoginMfa" import attemptLoginMfa from "@app/components/utilities/attemptLoginMfa"; import { Button } from "@app/components/v2"; +import { useUpdateUserAuthMethods } from "@app/hooks/api"; import { useSendMfaToken } from "@app/hooks/api/auth"; import { fetchOrganizations } from "@app/hooks/api/organization/queries"; +import { fetchUserDetails } from "@app/hooks/api/users/queries"; +import { AuthMethod } from "@app/hooks/api/users/types"; + // The style for the verification code input const props = { @@ -54,8 +59,7 @@ interface VerifyMfaTokenError { export const MFAStep = ({ email, password, - providerAuthToken, - callbackPort + providerAuthToken }: Props) => { const { createNotification } = useNotificationContext(); const router = useRouter(); @@ -67,9 +71,22 @@ export const MFAStep = ({ const { t } = useTranslation(); const sendMfaToken = useSendMfaToken(); + const { mutateAsync: updateUserAuthMethodsMutateAsync } = useUpdateUserAuthMethods(); const handleLoginMfa = async () => { try { + let isLinkingRequired: undefined | boolean; + let callbackPort: undefined | string; + let authMethod: undefined | AuthMethod; + + if (providerAuthToken) { + const decodedToken = jwt_decode(providerAuthToken) as any; + + isLinkingRequired = decodedToken.isLinkingRequired; + callbackPort = decodedToken.callbackPort; + authMethod = decodedToken.authMethod; + } + if (mfaCode.length !== 6) { createNotification({ text: "Please enter a 6-digit MFA code and try again", @@ -79,7 +96,7 @@ export const MFAStep = ({ } setIsLoading(true); - if (callbackPort){ + if (callbackPort) { // attemptCliLogin const isCliLoginSuccessful = await attemptCliLoginMfa({ @@ -118,6 +135,15 @@ export const MFAStep = ({ text: "Successfully logged in", type: "success" }); + + if (isLinkingRequired && authMethod) { + const user = await fetchUserDetails(); + const newAuthMethods = [...user.authMethods, authMethod] + await updateUserAuthMethodsMutateAsync({ + authMethods: newAuthMethods + }); + } + router.push(`/org/${userOrg}/overview`); } else { createNotification({ diff --git a/frontend/src/views/Login/components/PasswordStep/PasswordStep.tsx b/frontend/src/views/Login/components/PasswordStep/PasswordStep.tsx index dae175eb1..5a5e7c99d 100644 --- a/frontend/src/views/Login/components/PasswordStep/PasswordStep.tsx +++ b/frontend/src/views/Login/components/PasswordStep/PasswordStep.tsx @@ -3,16 +3,18 @@ import { useTranslation } from "react-i18next"; import Link from "next/link"; import { useRouter } from "next/router" import axios from "axios" +import jwt_decode from "jwt-decode"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import attemptCliLogin from "@app/components/utilities/attemptCliLogin"; import attemptLogin from "@app/components/utilities/attemptLogin"; import { Button, Input } from "@app/components/v2"; +import { useUpdateUserAuthMethods } from "@app/hooks/api"; import { fetchOrganizations } from "@app/hooks/api/organization/queries"; +import { fetchUserDetails } from "@app/hooks/api/users/queries"; type Props = { providerAuthToken: string; - callbackPort?: string; email: string; password: string; setPassword: (password: string) => void; @@ -21,16 +23,22 @@ type Props = { export const PasswordStep = ({ providerAuthToken, - callbackPort, email, password, setPassword, - setStep + setStep, }: Props) => { const { createNotification } = useNotificationContext(); const [isLoading, setIsLoading] = useState(false); const { t } = useTranslation(); const router = useRouter(); + const { mutateAsync } = useUpdateUserAuthMethods(); + + const { + callbackPort, + isLinkingRequired, + authMethod + } = jwt_decode(providerAuthToken) as any; const handleLogin = async () => { try { @@ -90,6 +98,15 @@ export const PasswordStep = ({ text: "Successfully logged in", type: "success" }); + + if (isLinkingRequired) { + const user = await fetchUserDetails(); + const newAuthMethods = [...user.authMethods, authMethod] + await mutateAsync({ + authMethods: newAuthMethods + }); + } + router.push(`/org/${userOrg}/overview`); } } @@ -108,9 +125,18 @@ export const PasswordStep = ({ onSubmit={(e) => e.preventDefault()} className="h-full mx-auto w-full max-w-md px-6 pt-8" > -

- What’s your Infisical Password? -

+
+

+ {isLinkingRequired ? "Link your account" : "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. + +
+ )} +