Add CLI support for SAML SSO

This commit is contained in:
Tuan Dang
2023-07-24 01:53:56 +07:00
parent 1419371588
commit 446edb6ed9
8 changed files with 104 additions and 50 deletions
+15 -4
View File
@@ -18,10 +18,15 @@ import {
router.get( router.get(
"/redirect/google", "/redirect/google",
authLimiter, authLimiter,
(req, res, next) => {
passport.authenticate("google", { passport.authenticate("google", {
scope: ["profile", "email"], scope: ["profile", "email"],
session: false, session: false,
}) ...(req.query.callback_port ? {
state: req.query.callback_port as string
} : {})
})(req, res, next);
}
); );
router.get( router.get(
@@ -36,9 +41,15 @@ router.get(
router.get( router.get(
"/redirect/saml2/:ssoIdentifier", "/redirect/saml2/:ssoIdentifier",
authLimiter, authLimiter,
passport.authenticate("saml", { (req, res, next) => {
failureRedirect: "/login/fail" const options = {
}) failureRedirect: "/",
additionalParams: {
RelayState: req.query.callback_port ?? ""
},
};
passport.authenticate("saml", options)(req, res, next);
}
); );
router.post("/saml2/:ssoIdentifier", router.post("/saml2/:ssoIdentifier",
+8 -3
View File
@@ -114,7 +114,10 @@ const initializePassport = async () => {
firstName, firstName,
lastName, lastName,
authProvider: user.authProvider, authProvider: user.authProvider,
isUserCompleted isUserCompleted,
...(req.query.state ? {
callbackPort: req.query.state as string
} : {})
}, },
expiresIn: await getJwtProviderAuthLifetime(), expiresIn: await getJwtProviderAuthLifetime(),
secret: await getJwtProviderAuthSecret(), secret: await getJwtProviderAuthSecret(),
@@ -153,7 +156,6 @@ const initializePassport = async () => {
}, },
}, },
async (req: any, profile: any, done: any) => { async (req: any, profile: any, done: any) => {
if (!req.ssoConfig.isActive) return done(InternalServerError()); if (!req.ssoConfig.isActive) return done(InternalServerError());
const organization = await Organization.findById(req.ssoConfig.organization); const organization = await Organization.findById(req.ssoConfig.organization);
@@ -199,7 +201,10 @@ const initializePassport = async () => {
lastName, lastName,
organizationName: organization?.name, organizationName: organization?.name,
authProvider: user.authProvider, authProvider: user.authProvider,
isUserCompleted isUserCompleted,
...(req.body.RelayState ? {
callbackPort: req.body.RelayState as string
} : {})
}, },
expiresIn: await getJwtProviderAuthLifetime(), expiresIn: await getJwtProviderAuthLifetime(),
secret: await getJwtProviderAuthSecret(), secret: await getJwtProviderAuthSecret(),
+3 -1
View File
@@ -18,6 +18,8 @@ export const Login = () => {
const [email, setEmail] = useState(""); const [email, setEmail] = useState("");
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
const queryParams = new URLSearchParams(window.location.search)
useEffect(() => { useEffect(() => {
// TODO(akhilmhdh): workspace will be controlled by a workspace context // TODO(akhilmhdh): workspace will be controlled by a workspace context
const redirectToDashboard = async () => { const redirectToDashboard = async () => {
@@ -30,7 +32,6 @@ export const Login = () => {
const userDetails = await fetchUserDetails() const userDetails = await fetchUserDetails()
// send details back to client // send details back to client
const queryParams = new URLSearchParams(window.location.search)
if (queryParams && queryParams.get("callback_port")) { if (queryParams && queryParams.get("callback_port")) {
const callbackPort = queryParams.get("callback_port") const callbackPort = queryParams.get("callback_port")
@@ -67,6 +68,7 @@ export const Login = () => {
email={email} email={email}
password={password} password={password}
providerAuthToken={undefined} providerAuthToken={undefined}
callbackPort={queryParams.get("callback_port")}
/> />
); );
case 2: case 2:
+3 -7
View File
@@ -1,5 +1,4 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useRouter } from "next/router"
import jwt_decode from "jwt-decode"; import jwt_decode from "jwt-decode";
import { import {
@@ -14,25 +13,20 @@ type Props = {
export const LoginSSO = ({ providerAuthToken }: Props) => { export const LoginSSO = ({ providerAuthToken }: Props) => {
const [step, setStep] = useState(0); const [step, setStep] = useState(0);
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
const router = useRouter();
const { const {
email, email,
isUserCompleted, isUserCompleted,
callbackPort
} = jwt_decode(providerAuthToken) as any; } = jwt_decode(providerAuthToken) as any;
useEffect(() => { useEffect(() => {
if (!isUserCompleted) {
router.push(`/signup/sso?token=${encodeURIComponent(providerAuthToken)}`);
}
if (isUserCompleted) { if (isUserCompleted) {
setStep(1); setStep(1);
} }
}, []); }, []);
const renderView = () => { const renderView = () => {
// TODO: consider adding a complete account step here that's uniquely for SSO
switch (step) { switch (step) {
case 0: case 0:
return ( return (
@@ -42,6 +36,7 @@ export const LoginSSO = ({ providerAuthToken }: Props) => {
return ( return (
<PasswordStep <PasswordStep
providerAuthToken={providerAuthToken} providerAuthToken={providerAuthToken}
callbackPort={callbackPort}
email={email} email={email}
password={password} password={password}
setPassword={setPassword} setPassword={setPassword}
@@ -52,6 +47,7 @@ export const LoginSSO = ({ providerAuthToken }: Props) => {
return ( return (
<MFAStep <MFAStep
providerAuthToken={providerAuthToken} providerAuthToken={providerAuthToken}
callbackPort={callbackPort}
email={email} email={email}
password={password} password={password}
/> />
@@ -34,6 +34,8 @@ export const InitialStep = ({
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [loginError, setLoginError] = useState(false); const [loginError, setLoginError] = useState(false);
const queryParams = new URLSearchParams(window.location.search);
const handleLogin = async (e: FormEvent<HTMLFormElement>) => { const handleLogin = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault() e.preventDefault()
try { try {
@@ -42,7 +44,6 @@ export const InitialStep = ({
} }
setIsLoading(true); setIsLoading(true);
const queryParams = new URLSearchParams(window.location.search)
if (queryParams && queryParams.get("callback_port")) { if (queryParams && queryParams.get("callback_port")) {
const callbackPort = queryParams.get("callback_port") const callbackPort = queryParams.get("callback_port")
@@ -164,7 +165,9 @@ export const InitialStep = ({
colorSchema="primary" colorSchema="primary"
variant="solid" variant="solid"
onClick={() => { 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(); window.close();
}} }}
leftIcon={<FontAwesomeIcon icon={faGoogle} className="mr-1" />} leftIcon={<FontAwesomeIcon icon={faGoogle} className="mr-1" />}
@@ -36,6 +36,7 @@ type Props = {
email: string; email: string;
password: string; password: string;
providerAuthToken?: string; providerAuthToken?: string;
callbackPort?: string | null;
} }
interface VerifyMfaTokenError { interface VerifyMfaTokenError {
@@ -53,7 +54,8 @@ interface VerifyMfaTokenError {
export const MFAStep = ({ export const MFAStep = ({
email, email,
password, password,
providerAuthToken providerAuthToken,
callbackPort
}: Props) => { }: Props) => {
const { createNotification } = useNotificationContext(); const { createNotification } = useNotificationContext();
const router = useRouter(); const router = useRouter();
@@ -77,9 +79,7 @@ export const MFAStep = ({
} }
setIsLoading(true); setIsLoading(true);
const queryParams = new URLSearchParams(window.location.search) if (callbackPort){
if (queryParams && queryParams.get("callback_port")){
const callbackPort = queryParams.get("callback_port")
// attemptCliLogin // attemptCliLogin
const isCliLoginSuccessful = await attemptCliLoginMfa({ const isCliLoginSuccessful = await attemptCliLoginMfa({
@@ -2,14 +2,17 @@ import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router" import { useRouter } from "next/router"
import axios from "axios"
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import attemptCliLogin from "@app/components/utilities/attemptCliLogin";
import attemptLogin from "@app/components/utilities/attemptLogin"; import attemptLogin from "@app/components/utilities/attemptLogin";
import { Button, Input } from "@app/components/v2"; import { Button, Input } from "@app/components/v2";
import getOrganizations from "@app/pages/api/organization/getOrgs"; import getOrganizations from "@app/pages/api/organization/getOrgs";
type Props = { type Props = {
providerAuthToken: string; providerAuthToken: string;
callbackPort?: string;
email: string; email: string;
password: string; password: string;
setPassword: (password: string) => void; setPassword: (password: string) => void;
@@ -18,6 +21,7 @@ type Props = {
export const PasswordStep = ({ export const PasswordStep = ({
providerAuthToken, providerAuthToken,
callbackPort,
email, email,
password, password,
setPassword, setPassword,
@@ -31,6 +35,36 @@ export const PasswordStep = ({
const handleLogin = async () => { const handleLogin = async () => {
try { try {
setIsLoading(true); setIsLoading(true);
if (callbackPort) {
// attemptCliLogin
const isCliLoginSuccessful = await attemptCliLogin({
email,
password,
providerAuthToken
})
if (isCliLoginSuccessful && isCliLoginSuccessful.success) {
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
}
} else {
const loginAttempt = await attemptLogin({ const loginAttempt = await attemptLogin({
email, email,
password, password,
@@ -58,7 +92,7 @@ export const PasswordStep = ({
}); });
router.push(`/org/${userOrg?._id}/overview`); router.push(`/org/${userOrg?._id}/overview`);
} }
}
} catch (err) { } catch (err) {
setIsLoading(false); setIsLoading(false);
createNotification({ createNotification({
@@ -13,6 +13,8 @@ export const SAMLSSOStep = ({
const [ssoIdentifier, setSSOIdentifier] = useState(""); const [ssoIdentifier, setSSOIdentifier] = useState("");
const { t } = useTranslation(); const { t } = useTranslation();
const queryParams = new URLSearchParams(window.location.search);
return ( return (
<div className="mx-auto w-full max-w-md md:px-6"> <div className="mx-auto w-full max-w-md md:px-6">
<p className="mx-auto mb-6 flex w-max justify-center text-xl font-medium text-transparent bg-clip-text bg-gradient-to-b from-white to-bunker-200 text-center mb-8"> <p className="mx-auto mb-6 flex w-max justify-center text-xl font-medium text-transparent bg-clip-text bg-gradient-to-b from-white to-bunker-200 text-center mb-8">
@@ -37,7 +39,8 @@ export const SAMLSSOStep = ({
colorSchema="primary" colorSchema="primary"
variant="outline_bg" variant="outline_bg"
onClick={() => { 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(); window.close();
}} }}
isFullWidth isFullWidth