mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 11:27:32 +00:00
Add CLI support for SAML SSO
This commit is contained in:
@@ -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",
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user