Integrated login with Gitlab

This commit is contained in:
Andrew Atimapre
2023-09-24 15:21:23 +01:00
parent f342c345b7
commit 255705501f
10 changed files with 383 additions and 304 deletions
+20
View File
@@ -46,6 +46,7 @@
"nodemailer": "^6.8.0", "nodemailer": "^6.8.0",
"passport": "^0.6.0", "passport": "^0.6.0",
"passport-github": "^1.1.0", "passport-github": "^1.1.0",
"passport-gitlab2": "^5.0.0",
"passport-google-oauth20": "^2.0.0", "passport-google-oauth20": "^2.0.0",
"posthog-node": "^2.6.0", "posthog-node": "^2.6.0",
"probot": "^12.3.1", "probot": "^12.3.1",
@@ -13698,6 +13699,17 @@
"node": ">= 0.4.0" "node": ">= 0.4.0"
} }
}, },
"node_modules/passport-gitlab2": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/passport-gitlab2/-/passport-gitlab2-5.0.0.tgz",
"integrity": "sha512-cXQMgM6JQx9wHVh7JLH30D8fplfwjsDwRz+zS0pqC8JS+4bNmc1J04NGp5g2M4yfwylH9kQRrMN98GxMw7q7cg==",
"dependencies": {
"passport-oauth2": "^1.4.0"
},
"engines": {
"node": ">= 6.0.0"
}
},
"node_modules/passport-google-oauth20": { "node_modules/passport-google-oauth20": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/passport-google-oauth20/-/passport-google-oauth20-2.0.0.tgz", "resolved": "https://registry.npmjs.org/passport-google-oauth20/-/passport-google-oauth20-2.0.0.tgz",
@@ -27074,6 +27086,14 @@
"passport-oauth2": "1.x.x" "passport-oauth2": "1.x.x"
} }
}, },
"passport-gitlab2": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/passport-gitlab2/-/passport-gitlab2-5.0.0.tgz",
"integrity": "sha512-cXQMgM6JQx9wHVh7JLH30D8fplfwjsDwRz+zS0pqC8JS+4bNmc1J04NGp5g2M4yfwylH9kQRrMN98GxMw7q7cg==",
"requires": {
"passport-oauth2": "^1.4.0"
}
},
"passport-google-oauth20": { "passport-google-oauth20": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/passport-google-oauth20/-/passport-google-oauth20-2.0.0.tgz", "resolved": "https://registry.npmjs.org/passport-google-oauth20/-/passport-google-oauth20-2.0.0.tgz",
+1
View File
@@ -37,6 +37,7 @@
"nodemailer": "^6.8.0", "nodemailer": "^6.8.0",
"passport": "^0.6.0", "passport": "^0.6.0",
"passport-github": "^1.1.0", "passport-github": "^1.1.0",
"passport-gitlab2": "^5.0.0",
"passport-google-oauth20": "^2.0.0", "passport-google-oauth20": "^2.0.0",
"posthog-node": "^2.6.0", "posthog-node": "^2.6.0",
"probot": "^12.3.1", "probot": "^12.3.1",
+2
View File
@@ -52,6 +52,8 @@ export const getClientIdGoogleLogin = async () => (await client.getSecret("CLIEN
export const getClientSecretGoogleLogin = async () => (await client.getSecret("CLIENT_SECRET_GOOGLE_LOGIN")).secretValue; export const getClientSecretGoogleLogin = async () => (await client.getSecret("CLIENT_SECRET_GOOGLE_LOGIN")).secretValue;
export const getClientIdGitHubLogin = async () => (await client.getSecret("CLIENT_ID_GITHUB_LOGIN")).secretValue; export const getClientIdGitHubLogin = async () => (await client.getSecret("CLIENT_ID_GITHUB_LOGIN")).secretValue;
export const getClientSecretGitHubLogin = async () => (await client.getSecret("CLIENT_SECRET_GITHUB_LOGIN")).secretValue; export const getClientSecretGitHubLogin = async () => (await client.getSecret("CLIENT_SECRET_GITHUB_LOGIN")).secretValue;
export const getClientIdGitLabLogin = async () => (await client.getSecret("CLIENT_ID_GITLAB_LOGIN")).secretValue;
export const getClientSecretGitLabLogin = async () => (await client.getSecret("CLIENT_SECRET_GITLAB_LOGIN")).secretValue;
export const getPostHogHost = async () => (await client.getSecret("POSTHOG_HOST")).secretValue || "https://app.posthog.com"; export const getPostHogHost = async () => (await client.getSecret("POSTHOG_HOST")).secretValue || "https://app.posthog.com";
export const getPostHogProjectApiKey = async () => (await client.getSecret("POSTHOG_PROJECT_API_KEY")).secretValue || "phc_nSin8j5q2zdhpFDI1ETmFNUIuTG4DwKVyIigrY10XiE"; export const getPostHogProjectApiKey = async () => (await client.getSecret("POSTHOG_PROJECT_API_KEY")).secretValue || "phc_nSin8j5q2zdhpFDI1ETmFNUIuTG4DwKVyIigrY10XiE";
+23
View File
@@ -65,6 +65,29 @@ router.get(
ssoController.redirectSSO ssoController.redirectSSO
); );
router.get(
"/redirect/gitlab",
authLimiter,
(req, res, next) => {
passport.authenticate("gitlab", {
session: false,
...(req.query.callback_port ? {
state: req.query.callback_port as string
} : {})
})(req, res, next);
}
)
router.get(
"/gitlab",
authLimiter,
passport.authenticate("gitlab", {
failureRedirect: "/login/provider/error",
session: false
}),
ssoController.redirectSSO
)
router.get( router.get(
"/redirect/saml2/:ssoIdentifier", "/redirect/saml2/:ssoIdentifier",
authLimiter, authLimiter,
+1
View File
@@ -4,6 +4,7 @@ export enum AuthMethod {
EMAIL = "email", EMAIL = "email",
GOOGLE = "google", GOOGLE = "google",
GITHUB = "github", GITHUB = "github",
GITLAB = "gitlab",
OKTA_SAML = "okta-saml", OKTA_SAML = "okta-saml",
AZURE_SAML = "azure-saml", AZURE_SAML = "azure-saml",
JUMPCLOUD_SAML = "jumpcloud-saml", JUMPCLOUD_SAML = "jumpcloud-saml",
+2 -1
View File
@@ -51,7 +51,8 @@ router.put(
return authMethods.every(provider => [ return authMethods.every(provider => [
AuthMethod.EMAIL, AuthMethod.EMAIL,
AuthMethod.GOOGLE, AuthMethod.GOOGLE,
AuthMethod.GITHUB AuthMethod.GITHUB,
AuthMethod.GITLAB
].includes(provider)) ].includes(provider))
}), }),
validateRequest, validateRequest,
@@ -155,6 +155,12 @@ Other environment variables are listed below to increase the functionality of yo
<ParamField query="CLIENT_SECRET_GITHUB_LOGIN" type="string" default="none" optional> <ParamField query="CLIENT_SECRET_GITHUB_LOGIN" type="string" default="none" optional>
OAuth2 client secret for GitHub login OAuth2 client secret for GitHub login
</ParamField> </ParamField>
<ParamField query="CLIENT_ID_GITLAB_LOGIN" type="string" default="none" optional>
OAuth2 client ID for GitLab login
</ParamField>
<ParamField query="CLIENT_SECRET_GITLAB_LOGIN" type="string" default="none" optional>
OAuth2 client secret for GitLab login
</ParamField>
</Tab> </Tab>
<Tab title="Others"> <Tab title="Others">
#### JWT #### JWT
+1
View File
@@ -4,6 +4,7 @@ export enum AuthMethod {
EMAIL = "email", EMAIL = "email",
GOOGLE = "google", GOOGLE = "google",
GITHUB = "github", GITHUB = "github",
GITLAB = "gitlab",
OKTA_SAML = "okta-saml", OKTA_SAML = "okta-saml",
AZURE_SAML = "azure-saml", AZURE_SAML = "azure-saml",
JUMPCLOUD_SAML = "jumpcloud-saml" JUMPCLOUD_SAML = "jumpcloud-saml"
@@ -2,10 +2,10 @@ import { FormEvent, 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 { faGithub,faGoogle } from "@fortawesome/free-brands-svg-icons"; import { faGithub, faGitlab, faGoogle } from "@fortawesome/free-brands-svg-icons";
import { faLock } from "@fortawesome/free-solid-svg-icons"; import { faLock } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import axios from "axios" import axios from "axios";
import Error from "@app/components/basic/Error"; import Error from "@app/components/basic/Error";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
@@ -16,208 +16,234 @@ import { fetchOrganizations } from "@app/hooks/api/organization/queries";
import { useFetchServerStatus } from "@app/hooks/api/serverDetails"; import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
type Props = { type Props = {
setStep: (step: number) => void; setStep: (step: number) => void;
email: string; email: string;
setEmail: (email: string) => void; setEmail: (email: string) => void;
password: string; password: string;
setPassword: (email: string) => void; setPassword: (email: string) => void;
} };
export const InitialStep = ({ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }: Props) => {
setStep, const router = useRouter();
email, const { createNotification } = useNotificationContext();
setEmail, const { t } = useTranslation();
password, const [isLoading, setIsLoading] = useState(false);
setPassword const [loginError, setLoginError] = useState(false);
}: Props) => { const { data: serverDetails } = useFetchServerStatus();
const router = useRouter(); const queryParams = new URLSearchParams(window.location.search);
const { createNotification } = useNotificationContext();
const { t } = useTranslation();
const [isLoading, setIsLoading] = useState(false);
const [loginError, setLoginError] = useState(false);
const { data: serverDetails } = useFetchServerStatus();
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 {
if (!email || !password) { if (!email || !password) {
return; return;
} }
setIsLoading(true); setIsLoading(true);
if (queryParams && queryParams.get("callback_port")) { if (queryParams && queryParams.get("callback_port")) {
const callbackPort = queryParams.get("callback_port") const callbackPort = queryParams.get("callback_port");
// attemptCliLogin // attemptCliLogin
const isCliLoginSuccessful = await attemptCliLogin({ const isCliLoginSuccessful = await attemptCliLogin({
email: email.toLowerCase(), email: email.toLowerCase(),
password, password
}) });
if (isCliLoginSuccessful && isCliLoginSuccessful.success) { if (isCliLoginSuccessful && isCliLoginSuccessful.success) {
if (isCliLoginSuccessful.mfaEnabled) {
// case: login requires MFA step
setStep(1);
setIsLoading(false);
return;
}
// case: login was successful
const cliUrl = `http://localhost:${callbackPort}`;
if (isCliLoginSuccessful.mfaEnabled) { // send request to server endpoint
// case: login requires MFA step const instance = axios.create();
setStep(1); await instance.post(cliUrl, { ...isCliLoginSuccessful.loginResponse });
setIsLoading(false);
return;
}
// case: login was successful
const cliUrl = `http://localhost:${callbackPort}`
// send request to server endpoint // cli page
const instance = axios.create() router.push("/cli-redirect");
await instance.post(cliUrl, { ...isCliLoginSuccessful.loginResponse })
// cli page // on success, router.push to cli Login Successful page
router.push("/cli-redirect");
// on success, router.push to cli Login Successful page
}
} else {
const isLoginSuccessful = await attemptLogin({
email: email.toLowerCase(),
password,
});
if (isLoginSuccessful && isLoginSuccessful.success) {
// case: login was successful
if (isLoginSuccessful.mfaEnabled) {
// case: login requires MFA step
setStep(1);
setIsLoading(false);
return;
}
const userOrgs = await fetchOrganizations();
const userOrg = userOrgs[0] && userOrgs[0]._id;
// case: login does not require MFA step
createNotification({
text: "Successfully logged in",
type: "success"
});
router.push(`/org/${userOrg}/overview`);
}
}
} catch (err) {
setLoginError(true);
createNotification({
text: "Login unsuccessful. Double-check your credentials and try again.",
type: "error"
});
} }
} else {
const isLoginSuccessful = await attemptLogin({
email: email.toLowerCase(),
password
});
if (isLoginSuccessful && isLoginSuccessful.success) {
// case: login was successful
setIsLoading(false); if (isLoginSuccessful.mfaEnabled) {
// case: login requires MFA step
setStep(1);
setIsLoading(false);
return;
}
const userOrgs = await fetchOrganizations();
const userOrg = userOrgs[0] && userOrgs[0]._id;
// case: login does not require MFA step
createNotification({
text: "Successfully logged in",
type: "success"
});
router.push(`/org/${userOrg}/overview`);
}
}
} catch (err) {
setLoginError(true);
createNotification({
text: "Login unsuccessful. Double-check your credentials and try again.",
type: "error"
});
} }
return ( setIsLoading(false);
<form onSubmit={handleLogin} className='flex flex-col mx-auto w-full justify-center items-center'> };
<h1 className='text-xl font-medium text-transparent bg-clip-text bg-gradient-to-b from-white to-bunker-200 text-center mb-8' >Login to Infisical</h1>
<div className='lg:w-1/6 w-1/4 min-w-[21.2rem] md:min-w-[20.1rem] text-center rounded-md mt-4'>
<Button
colorSchema="primary"
variant="outline_bg"
onClick={() => {
const callbackPort = queryParams.get("callback_port");
window.open(`/api/v1/sso/redirect/google${callbackPort ? `?callback_port=${callbackPort}` : ""}`); return (
window.close(); <form
}} onSubmit={handleLogin}
leftIcon={<FontAwesomeIcon icon={faGoogle} className="mr-2" />} className="mx-auto flex w-full flex-col items-center justify-center"
className="h-11 w-full mx-0" >
> <h1 className="mb-8 bg-gradient-to-b from-white to-bunker-200 bg-clip-text text-center text-xl font-medium text-transparent">
{t("login.continue-with-google")} Login to Infisical
</Button> </h1>
</div> <div className="mt-4 w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
<div className='lg:w-1/6 w-1/4 min-w-[21.2rem] md:min-w-[20.1rem] text-center rounded-md mt-4'> <Button
<Button colorSchema="primary"
colorSchema="primary" variant="outline_bg"
variant="outline_bg" onClick={() => {
onClick={() => { const callbackPort = queryParams.get("callback_port");
const callbackPort = queryParams.get("callback_port");
window.open(`/api/v1/sso/redirect/github${callbackPort ? `?callback_port=${callbackPort}` : ""}`); window.open(
`/api/v1/sso/redirect/google${callbackPort ? `?callback_port=${callbackPort}` : ""}`
);
window.close();
}}
leftIcon={<FontAwesomeIcon icon={faGoogle} className="mr-2" />}
className="mx-0 h-11 w-full"
>
{t("login.continue-with-google")}
</Button>
</div>
<div className="mt-4 w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
<Button
colorSchema="primary"
variant="outline_bg"
onClick={() => {
const callbackPort = queryParams.get("callback_port");
window.close(); window.open(
}} `/api/v1/sso/redirect/github${callbackPort ? `?callback_port=${callbackPort}` : ""}`
leftIcon={<FontAwesomeIcon icon={faGithub} className="mr-2" />} );
className="h-11 w-full mx-0"
> window.close();
Continue with GitHub }}
</Button> leftIcon={<FontAwesomeIcon icon={faGithub} className="mr-2" />}
</div> className="mx-0 h-11 w-full"
<div className='lg:w-1/6 w-1/4 min-w-[21.2rem] md:min-w-[20.1rem] text-center rounded-md mt-4'> >
<Button Continue with GitHub
colorSchema="primary" </Button>
variant="outline_bg" </div>
onClick={() => { <div className="mt-4 w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
setStep(2); <Button
}} colorSchema="primary"
leftIcon={<FontAwesomeIcon icon={faLock} className="mr-2" />} variant="outline_bg"
className="h-11 w-full mx-0" onClick={() => {
> const callbackPort = queryParams.get("callback_port");
Continue with SSO
</Button> window.open(
</div> `/api/v1/sso/redirect/gitlab${callbackPort ? `?callback_port=${callbackPort}` : ""}`
<div className='lg:w-1/6 w-1/4 min-w-[20rem] flex flex-row items-center my-4 py-2'> );
<div className='w-full border-t border-mineshaft-400/60' />
<span className="mx-2 text-mineshaft-200 text-xs">or</span> window.close();
<div className='w-full border-t border-mineshaft-400/60' /> }}
</div> leftIcon={<FontAwesomeIcon icon={faGitlab} className="mr-2" />}
<div className='lg:w-1/6 w-1/4 min-w-[21.2rem] md:min-w-[20.1rem] text-center rounded-md'> className="mx-0 h-11 w-full"
<Input >
value={email} Continue with GitLab
onChange={(e) => setEmail(e.target.value)} </Button>
type="email" </div>
placeholder="Enter your email..." <div className="mt-4 w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
isRequired <Button
autoComplete="username" colorSchema="primary"
className="h-11" variant="outline_bg"
/> onClick={() => {
</div> setStep(2);
<div className='lg:w-1/6 w-1/4 min-w-[21.2rem] md:min-w-[20.1rem] text-center rounded-md mt-4'> }}
<Input leftIcon={<FontAwesomeIcon icon={faLock} className="mr-2" />}
value={password} className="mx-0 h-11 w-full"
onChange={(e) => setPassword(e.target.value)} >
type="password" Continue with SSO
placeholder="Enter your password..." </Button>
isRequired </div>
autoComplete="current-password" <div className="my-4 flex w-1/4 min-w-[20rem] flex-row items-center py-2 lg:w-1/6">
id="current-password" <div className="w-full border-t border-mineshaft-400/60" />
className="h-11 select:-webkit-autofill:focus" <span className="mx-2 text-xs text-mineshaft-200">or</span>
/> <div className="w-full border-t border-mineshaft-400/60" />
</div> </div>
<div className='lg:w-1/6 w-1/4 min-w-[21.2rem] md:min-w-[20.1rem] text-center rounded-md mt-5'> <div className="w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
<Button <Input
type="submit" value={email}
size="sm" onChange={(e) => setEmail(e.target.value)}
isFullWidth type="email"
className='h-11' placeholder="Enter your email..."
colorSchema="primary" isRequired
variant="solid" autoComplete="username"
isLoading={isLoading} className="h-11"
> Continue with Email </Button> />
</div> </div>
{!isLoading && loginError && <Error text={t("login.error-login") ?? ""} />} <div className="mt-4 w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
{ <Input
!serverDetails?.inviteOnlySignup ? value={password}
<div className="mt-6 text-bunker-400 text-sm flex flex-row"> onChange={(e) => setPassword(e.target.value)}
<span className="mr-1">Don&apos;t have an acount yet?</span> type="password"
<Link href="/signup"> placeholder="Enter your password..."
<span className='hover:underline hover:underline-offset-4 hover:decoration-primary-700 hover:text-bunker-200 duration-200 cursor-pointer'>{t("login.create-account")}</span> isRequired
</Link> autoComplete="current-password"
</div> : <div /> id="current-password"
} className="select:-webkit-autofill:focus h-11"
<div className="text-bunker-400 text-sm flex flex-row"> />
<span className="mr-1">Forgot password?</span> </div>
<Link href="/verify-email"> <div className="mt-5 w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
<span className='hover:underline hover:underline-offset-4 hover:decoration-primary-700 hover:text-bunker-200 duration-200 cursor-pointer'>Recover your account</span> <Button
</Link> type="submit"
</div> size="sm"
</form> isFullWidth
); className="h-11"
} colorSchema="primary"
variant="solid"
isLoading={isLoading}
>
{" "}
Continue with Email{" "}
</Button>
</div>
{!isLoading && loginError && <Error text={t("login.error-login") ?? ""} />}
{!serverDetails?.inviteOnlySignup ? (
<div className="mt-6 flex flex-row text-sm text-bunker-400">
<span className="mr-1">Don&apos;t have an acount yet?</span>
<Link href="/signup">
<span className="cursor-pointer duration-200 hover:text-bunker-200 hover:underline hover:decoration-primary-700 hover:underline-offset-4">
{t("login.create-account")}
</span>
</Link>
</div>
) : (
<div />
)}
<div className="flex flex-row text-sm text-bunker-400">
<span className="mr-1">Forgot password?</span>
<Link href="/verify-email">
<span className="cursor-pointer duration-200 hover:text-bunker-200 hover:underline hover:decoration-primary-700 hover:underline-offset-4">
Recover your account
</span>
</Link>
</div>
</form>
);
};
@@ -1,6 +1,6 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { faGithub, faGoogle, IconDefinition } from "@fortawesome/free-brands-svg-icons"; import { faGithub, faGitlab, faGoogle, IconDefinition } from "@fortawesome/free-brands-svg-icons";
import { faEnvelope } from "@fortawesome/free-regular-svg-icons"; import { faEnvelope } from "@fortawesome/free-regular-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { yupResolver } from "@hookform/resolvers/yup"; import { yupResolver } from "@hookform/resolvers/yup";
@@ -10,129 +10,127 @@ import { useNotificationContext } from "@app/components/context/Notifications/No
import { Switch } from "@app/components/v2"; import { Switch } from "@app/components/v2";
import { useUser } from "@app/context"; import { useUser } from "@app/context";
import { useUpdateUserAuthMethods } from "@app/hooks/api"; import { useUpdateUserAuthMethods } from "@app/hooks/api";
import { import { AuthMethod } from "@app/hooks/api/users/types";
AuthMethod
} from "@app/hooks/api/users/types";
interface AuthMethodOption { interface AuthMethodOption {
label: string, label: string;
value: AuthMethod, value: AuthMethod;
icon: IconDefinition; icon: IconDefinition;
} }
const authMethodOpts: AuthMethodOption[] = [ const authMethodOpts: AuthMethodOption[] = [
{ label: "Email", value: AuthMethod.EMAIL, icon: faEnvelope }, { label: "Email", value: AuthMethod.EMAIL, icon: faEnvelope },
{ label: "Google", value: AuthMethod.GOOGLE, icon: faGoogle }, { label: "Google", value: AuthMethod.GOOGLE, icon: faGoogle },
{ label: "GitHub", value: AuthMethod.GITHUB, icon: faGithub } { label: "GitHub", value: AuthMethod.GITHUB, icon: faGithub },
{ label: "GitLab", value: AuthMethod.GITLAB, icon: faGitlab }
]; ];
const samlProviders = [AuthMethod.OKTA_SAML, AuthMethod.JUMPCLOUD_SAML, AuthMethod.AZURE_SAML]; const samlProviders = [AuthMethod.OKTA_SAML, AuthMethod.JUMPCLOUD_SAML, AuthMethod.AZURE_SAML];
const schema = yup.object({ const schema = yup.object({
authMethods: yup.array().required("Auth method is required") authMethods: yup.array().required("Auth method is required")
}); });
export type FormData = yup.InferType<typeof schema>; export type FormData = yup.InferType<typeof schema>;
export const AuthMethodSection = () => { export const AuthMethodSection = () => {
const { createNotification } = useNotificationContext(); const { createNotification } = useNotificationContext();
const { user } = useUser(); const { user } = useUser();
const { mutateAsync } = useUpdateUserAuthMethods(); const { mutateAsync } = useUpdateUserAuthMethods();
const { const { reset, setValue, watch } = useForm<FormData>({
reset, defaultValues: {
setValue, authMethods: user.authMethods
watch, },
} = useForm<FormData>({ resolver: yupResolver(schema)
defaultValues: { });
authMethods: user.authMethods,
},
resolver: yupResolver(schema)
});
const authMethods = watch("authMethods"); const authMethods = watch("authMethods");
useEffect(() => { useEffect(() => {
if (user) { if (user) {
reset({ reset({
authMethods: user.authMethods, authMethods: user.authMethods
}); });
} }
}, [user]); }, [user]);
const onAuthMethodToggle = async (value: boolean, authMethodOpt: AuthMethodOption) => { const onAuthMethodToggle = async (value: boolean, authMethodOpt: AuthMethodOption) => {
const hasSamlEnabled = user.authMethods const hasSamlEnabled = user.authMethods.some((authMethod: AuthMethod) =>
.some((authMethod: AuthMethod) => samlProviders.includes(authMethod)); samlProviders.includes(authMethod)
);
if (hasSamlEnabled) { if (hasSamlEnabled) {
createNotification({ createNotification({
text: "SAML authentication can only be configured in your organization settings", text: "SAML authentication can only be configured in your organization settings",
type: "error" type: "error"
}); });
}
const newAuthMethods = value
? [...authMethods, authMethodOpt.value]
: authMethods.filter(auth => auth !== authMethodOpt.value);
if (value) {
const newUser = await mutateAsync({
authMethods: newAuthMethods
});
setValue("authMethods", newUser.authMethods);
createNotification({
text: "Successfully enabled authentication method",
type: "success"
});
return;
}
if (newAuthMethods.length === 0) {
createNotification({
text: "You must keep at least 1 authentication method enabled",
type: "error"
});
return;
}
const newUser = await mutateAsync({
authMethods: newAuthMethods
});
setValue("authMethods", newUser.authMethods);
createNotification({
text: "Successfully disabled authentication method",
type: "success"
});
} }
return ( const newAuthMethods = value
<div className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600"> ? [...authMethods, authMethodOpt.value]
<h2 className="text-xl font-semibold flex-1 text-mineshaft-100 mb-8"> : authMethods.filter((auth) => auth !== authMethodOpt.value);
Authentication methods
</h2> if (value) {
<p className="text-gray-400 mb-4"> const newUser = await mutateAsync({
By enabling a SSO provider, you are allowing an account with that provider which uses the same email address as your existing Infisical account to be able to log in to Infisical. authMethods: newAuthMethods
</p> });
<div className="mb-4">
{user && authMethodOpts.map((authMethodOpt) => { setValue("authMethods", newUser.authMethods);
return ( createNotification({
<div className="flex p-4 items-center" key={`auth-method-${authMethodOpt.value}`}> text: "Successfully enabled authentication method",
<div className="flex items-center"> type: "success"
<FontAwesomeIcon icon={authMethodOpt.icon} className="mr-4" /> });
</div> return;
<Switch }
id={`enable-${authMethodOpt.value}-auth`}
onCheckedChange={(value) => onAuthMethodToggle(value, authMethodOpt)} if (newAuthMethods.length === 0) {
isChecked={authMethods?.includes(authMethodOpt.value) ?? false} createNotification({
> text: "You must keep at least 1 authentication method enabled",
<p className="w-12 mr-4">{authMethodOpt.label}</p> type: "error"
</Switch> });
</div> return;
); }
})}
</div> const newUser = await mutateAsync({
</div> authMethods: newAuthMethods
); });
}
setValue("authMethods", newUser.authMethods);
createNotification({
text: "Successfully disabled authentication method",
type: "success"
});
};
return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<h2 className="mb-8 flex-1 text-xl font-semibold text-mineshaft-100">
Authentication methods
</h2>
<p className="mb-4 text-gray-400">
By enabling a SSO provider, you are allowing an account with that provider which uses the
same email address as your existing Infisical account to be able to log in to Infisical.
</p>
<div className="mb-4">
{user &&
authMethodOpts.map((authMethodOpt) => {
return (
<div className="flex items-center p-4" key={`auth-method-${authMethodOpt.value}`}>
<div className="flex items-center">
<FontAwesomeIcon icon={authMethodOpt.icon} className="mr-4" />
</div>
<Switch
id={`enable-${authMethodOpt.value}-auth`}
onCheckedChange={(value) => onAuthMethodToggle(value, authMethodOpt)}
isChecked={authMethods?.includes(authMethodOpt.value) ?? false}
>
<p className="mr-4 w-12">{authMethodOpt.label}</p>
</Switch>
</div>
);
})}
</div>
</div>
);
};