diff --git a/backend/src/ee/routes/v1/sso.ts b/backend/src/ee/routes/v1/sso.ts index fb26e1064..4771b5144 100644 --- a/backend/src/ee/routes/v1/sso.ts +++ b/backend/src/ee/routes/v1/sso.ts @@ -18,10 +18,15 @@ import { router.get( "/redirect/google", authLimiter, - passport.authenticate("google", { - scope: ["profile", "email"], - session: false, - }) + (req, res, next) => { + passport.authenticate("google", { + scope: ["profile", "email"], + session: false, + ...(req.query.callback_port ? { + state: req.query.callback_port as string + } : {}) + })(req, res, next); + } ); router.get( @@ -36,16 +41,22 @@ router.get( router.get( "/redirect/saml2/:ssoIdentifier", authLimiter, - passport.authenticate("saml", { - failureRedirect: "/login/fail" - }) + (req, res, next) => { + const options = { + failureRedirect: "/", + additionalParams: { + RelayState: req.query.callback_port ?? "" + }, + }; + passport.authenticate("saml", options)(req, res, next); + } ); router.post("/saml2/:ssoIdentifier", passport.authenticate("saml", { failureRedirect: "/login/provider/error", failureFlash: true, - session: false + session: false }), ssoController.redirectSSO ); diff --git a/backend/src/utils/auth.ts b/backend/src/utils/auth.ts index dccd5b5e4..ded2f3eb3 100644 --- a/backend/src/utils/auth.ts +++ b/backend/src/utils/auth.ts @@ -114,7 +114,10 @@ const initializePassport = async () => { firstName, lastName, authProvider: user.authProvider, - isUserCompleted + isUserCompleted, + ...(req.query.state ? { + callbackPort: req.query.state as string + } : {}) }, expiresIn: await getJwtProviderAuthLifetime(), secret: await getJwtProviderAuthSecret(), @@ -153,7 +156,6 @@ const initializePassport = async () => { }, }, async (req: any, profile: any, done: any) => { - if (!req.ssoConfig.isActive) return done(InternalServerError()); const organization = await Organization.findById(req.ssoConfig.organization); @@ -199,7 +201,10 @@ const initializePassport = async () => { lastName, organizationName: organization?.name, authProvider: user.authProvider, - isUserCompleted + isUserCompleted, + ...(req.body.RelayState ? { + callbackPort: req.body.RelayState as string + } : {}) }, expiresIn: await getJwtProviderAuthLifetime(), secret: await getJwtProviderAuthSecret(), diff --git a/frontend/src/views/Login/Login.tsx b/frontend/src/views/Login/Login.tsx index 6a54268e9..d7ddf6078 100644 --- a/frontend/src/views/Login/Login.tsx +++ b/frontend/src/views/Login/Login.tsx @@ -18,6 +18,8 @@ export const Login = () => { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); + const queryParams = new URLSearchParams(window.location.search) + useEffect(() => { // TODO(akhilmhdh): workspace will be controlled by a workspace context const redirectToDashboard = async () => { @@ -30,7 +32,6 @@ export const Login = () => { const userDetails = await fetchUserDetails() // send details back to client - const queryParams = new URLSearchParams(window.location.search) if (queryParams && queryParams.get("callback_port")) { const callbackPort = queryParams.get("callback_port") @@ -67,6 +68,7 @@ export const Login = () => { email={email} password={password} providerAuthToken={undefined} + callbackPort={queryParams.get("callback_port")} /> ); case 2: diff --git a/frontend/src/views/Login/LoginSSO.tsx b/frontend/src/views/Login/LoginSSO.tsx index 170b591d4..548bee3ae 100644 --- a/frontend/src/views/Login/LoginSSO.tsx +++ b/frontend/src/views/Login/LoginSSO.tsx @@ -1,5 +1,4 @@ import { useEffect, useState } from "react"; -import { useRouter } from "next/router" import jwt_decode from "jwt-decode"; import { @@ -14,25 +13,20 @@ type Props = { export const LoginSSO = ({ providerAuthToken }: Props) => { const [step, setStep] = useState(0); const [password, setPassword] = useState(""); - const router = useRouter(); const { email, isUserCompleted, + callbackPort } = jwt_decode(providerAuthToken) as any; - + useEffect(() => { - if (!isUserCompleted) { - router.push(`/signup/sso?token=${encodeURIComponent(providerAuthToken)}`); - } - if (isUserCompleted) { setStep(1); } }, []); const renderView = () => { - // TODO: consider adding a complete account step here that's uniquely for SSO switch (step) { case 0: return ( @@ -42,6 +36,7 @@ export const LoginSSO = ({ providerAuthToken }: Props) => { return ( { return ( diff --git a/frontend/src/views/Login/components/InitialStep/InitialStep.tsx b/frontend/src/views/Login/components/InitialStep/InitialStep.tsx index abc6adf56..7fb77de48 100644 --- a/frontend/src/views/Login/components/InitialStep/InitialStep.tsx +++ b/frontend/src/views/Login/components/InitialStep/InitialStep.tsx @@ -34,6 +34,8 @@ export const InitialStep = ({ const [isLoading, setIsLoading] = useState(false); const [loginError, setLoginError] = useState(false); + const queryParams = new URLSearchParams(window.location.search); + const handleLogin = async (e: FormEvent) => { e.preventDefault() try { @@ -42,7 +44,6 @@ export const InitialStep = ({ } setIsLoading(true); - const queryParams = new URLSearchParams(window.location.search) if (queryParams && queryParams.get("callback_port")) { const callbackPort = queryParams.get("callback_port") @@ -164,7 +165,9 @@ export const InitialStep = ({ colorSchema="primary" variant="solid" onClick={() => { - window.open("/api/v1/sso/redirect/google"); + const callbackPort = queryParams.get("callback_port"); + + window.open(`/api/v1/sso/redirect/google${callbackPort ? `?callback_port=${callbackPort}` : ""}`); window.close(); }} leftIcon={} diff --git a/frontend/src/views/Login/components/MFAStep/MFAStep.tsx b/frontend/src/views/Login/components/MFAStep/MFAStep.tsx index 3eebba058..fe4a0fd01 100644 --- a/frontend/src/views/Login/components/MFAStep/MFAStep.tsx +++ b/frontend/src/views/Login/components/MFAStep/MFAStep.tsx @@ -36,6 +36,7 @@ type Props = { email: string; password: string; providerAuthToken?: string; + callbackPort?: string | null; } interface VerifyMfaTokenError { @@ -53,7 +54,8 @@ interface VerifyMfaTokenError { export const MFAStep = ({ email, password, - providerAuthToken + providerAuthToken, + callbackPort }: Props) => { const { createNotification } = useNotificationContext(); const router = useRouter(); @@ -77,9 +79,7 @@ export const MFAStep = ({ } setIsLoading(true); - const queryParams = new URLSearchParams(window.location.search) - if (queryParams && queryParams.get("callback_port")){ - const callbackPort = queryParams.get("callback_port") + if (callbackPort){ // attemptCliLogin const isCliLoginSuccessful = await attemptCliLoginMfa({ diff --git a/frontend/src/views/Login/components/PasswordStep/PasswordStep.tsx b/frontend/src/views/Login/components/PasswordStep/PasswordStep.tsx index 07a0b1ba3..6076e76bb 100644 --- a/frontend/src/views/Login/components/PasswordStep/PasswordStep.tsx +++ b/frontend/src/views/Login/components/PasswordStep/PasswordStep.tsx @@ -2,14 +2,17 @@ import { useState } from "react"; import { useTranslation } from "react-i18next"; import Link from "next/link"; import { useRouter } from "next/router" +import axios from "axios" 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 getOrganizations from "@app/pages/api/organization/getOrgs"; type Props = { providerAuthToken: string; + callbackPort?: string; email: string; password: string; setPassword: (password: string) => void; @@ -18,6 +21,7 @@ type Props = { export const PasswordStep = ({ providerAuthToken, + callbackPort, email, password, setPassword, @@ -31,34 +35,64 @@ export const PasswordStep = ({ const handleLogin = async () => { try { setIsLoading(true); - const loginAttempt = await attemptLogin({ - email, - password, - providerAuthToken, - }); + + if (callbackPort) { + // attemptCliLogin + const isCliLoginSuccessful = await attemptCliLogin({ + email, + password, + providerAuthToken + }) - if (loginAttempt && loginAttempt.success) { - // case: login was successful + if (isCliLoginSuccessful && isCliLoginSuccessful.success) { - if (loginAttempt.mfaEnabled) { - // TODO: deal with MFA - // case: login requires MFA step - setIsLoading(false); - setStep(2); - return; + if (isCliLoginSuccessful.mfaEnabled) { + // case: login requires MFA step + setStep(2); + setIsLoading(false); + return; + } + // case: login was successful + const cliUrl = `http://localhost:${callbackPort}` + + // send request to server endpoint + const instance = axios.create() + await instance.post(cliUrl, { ...isCliLoginSuccessful.loginResponse }) + + // cli page + router.push("/cli-redirect"); + + // on success, router.push to cli Login Successful page } - - // case: login does not require MFA step - const userOrgs = await getOrganizations(); - const userOrg = userOrgs[0]._id; - setIsLoading(false); - createNotification({ - text: "Successfully logged in", - type: "success" + } else { + const loginAttempt = await attemptLogin({ + email, + password, + providerAuthToken, }); - router.push(`/org/${userOrg?._id}/overview`); - } + if (loginAttempt && loginAttempt.success) { + // case: login was successful + + if (loginAttempt.mfaEnabled) { + // TODO: deal with MFA + // case: login requires MFA step + setIsLoading(false); + setStep(2); + return; + } + + // case: login does not require MFA step + const userOrgs = await getOrganizations(); + const userOrg = userOrgs[0]._id; + setIsLoading(false); + createNotification({ + text: "Successfully logged in", + type: "success" + }); + router.push(`/org/${userOrg?._id}/overview`); + } + } } catch (err) { setIsLoading(false); createNotification({ diff --git a/frontend/src/views/Login/components/SAMLSSOStep/SAMLSSOStep.tsx b/frontend/src/views/Login/components/SAMLSSOStep/SAMLSSOStep.tsx index 77e7e6cb4..8ff363d36 100644 --- a/frontend/src/views/Login/components/SAMLSSOStep/SAMLSSOStep.tsx +++ b/frontend/src/views/Login/components/SAMLSSOStep/SAMLSSOStep.tsx @@ -13,6 +13,8 @@ export const SAMLSSOStep = ({ const [ssoIdentifier, setSSOIdentifier] = useState(""); const { t } = useTranslation(); + const queryParams = new URLSearchParams(window.location.search); + return (

@@ -37,7 +39,8 @@ export const SAMLSSOStep = ({ colorSchema="primary" variant="outline_bg" onClick={() => { - window.open(`/api/v1/sso/redirect/saml2/${ssoIdentifier}`); + const callbackPort = queryParams.get("callback_port"); + window.open(`/api/v1/sso/redirect/saml2/${ssoIdentifier}${callbackPort ? `?callback_port=${callbackPort}` : ""}`); window.close(); }} isFullWidth