Merge pull request #1029 from atimapreandrew/gitlab-sso

Gitlab sso
potential-cli-login-patch
BlackMagiq 8 months ago committed by GitHub
commit 25b988ca9a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -50,6 +50,7 @@
"nodemailer": "^6.8.0",
"passport": "^0.6.0",
"passport-github": "^1.1.0",
"passport-gitlab2": "^5.0.0",
"passport-google-oauth20": "^2.0.0",
"posthog-node": "^2.6.0",
"probot": "^12.3.1",
@ -13727,6 +13728,17 @@
"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": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/passport-google-oauth20/-/passport-google-oauth20-2.0.0.tgz",
@ -27163,6 +27175,14 @@
"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": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/passport-google-oauth20/-/passport-google-oauth20-2.0.0.tgz",

@ -41,6 +41,7 @@
"nodemailer": "^6.8.0",
"passport": "^0.6.0",
"passport-github": "^1.1.0",
"passport-gitlab2": "^5.0.0",
"passport-google-oauth20": "^2.0.0",
"posthog-node": "^2.6.0",
"probot": "^12.3.1",

@ -1,3 +1,5 @@
import { GITLAB_URL } from "../variables";
import InfisicalClient from "infisical-node";
export const client = new InfisicalClient({
@ -52,6 +54,9 @@ export const getClientIdGoogleLogin = async () => (await client.getSecret("CLIEN
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 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 getUrlGitLabLogin = async () => (await client.getSecret("URL_GITLAB_LOGIN")).secretValue || GITLAB_URL;
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";

@ -6,58 +6,20 @@ import { ssoController } from "../../controllers/v1";
import { authLimiter } from "../../../helpers/rateLimiter";
import { AuthMode } from "../../../variables";
router.get("/redirect/google", authLimiter, (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(
"/google",
passport.authenticate("google", {
failureRedirect: "/login/provider/error",
session: false
}),
ssoController.redirectSSO
);
router.get("/redirect/github", authLimiter, (req, res, next) => {
passport.authenticate("github", {
session: false,
...(req.query.callback_port
? {
state: req.query.callback_port as string
}
: {})
})(req, res, next);
});
router.get(
"/github",
"/redirect/saml2/:ssoIdentifier",
authLimiter,
passport.authenticate("github", {
failureRedirect: "/login/provider/error",
session: false
}),
ssoController.redirectSSO
(req, res, next) => {
const options = {
failureRedirect: "/",
additionalParams: {
RelayState: req.query.callback_port ?? ""
},
};
passport.authenticate("saml", options)(req, res, next);
}
);
router.get("/redirect/saml2/:ssoIdentifier", authLimiter, (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", {

@ -38,6 +38,7 @@ import {
membership as v1MembershipRouter,
organization as v1OrganizationRouter,
password as v1PasswordRouter,
sso as v1SSORouter,
secretApprovalPolicy as v1SecretApprovalPolicy,
secretImps as v1SecretImpsRouter,
secret as v1SecretRouter,
@ -178,6 +179,7 @@ const main = async () => {
app.use("/api/v1/secret-imports", v1SecretImpsRouter);
app.use("/api/v1/roles", v1RoleRouter);
app.use("/api/v1/secret-approvals", v1SecretApprovalPolicy);
app.use("/api/v1/sso", v1SSORouter);
// v2 routes (improvements)
app.use("/api/v2/signup", v2SignupRouter);

@ -4,6 +4,7 @@ export enum AuthMethod {
EMAIL = "email",
GOOGLE = "google",
GITHUB = "github",
GITLAB = "gitlab",
OKTA_SAML = "okta-saml",
AZURE_SAML = "azure-saml",
JUMPCLOUD_SAML = "jumpcloud-saml",

@ -11,6 +11,7 @@ import key from "./key";
import inviteOrg from "./inviteOrg";
import secret from "./secret";
import serviceToken from "./serviceToken";
import sso from "./sso";
import password from "./password";
import integration from "./integration";
import integrationAuth from "./integrationAuth";
@ -39,5 +40,6 @@ export {
secretsFolder,
webhooks,
secretImps,
sso,
secretApprovalPolicy
};

@ -0,0 +1,72 @@
import express from "express";
const router = express.Router();
import passport from "passport";
import { authLimiter } from "../../helpers/rateLimiter";
import { ssoController } from "../../ee/controllers/v1";
router.get("/redirect/google", authLimiter, (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(
"/google",
passport.authenticate("google", {
failureRedirect: "/login/provider/error",
session: false
}),
ssoController.redirectSSO
);
router.get("/redirect/github", authLimiter, (req, res, next) => {
passport.authenticate("github", {
session: false,
...(req.query.callback_port
? {
state: req.query.callback_port as string
}
: {})
})(req, res, next);
});
router.get(
"/github",
authLimiter,
passport.authenticate("github", {
failureRedirect: "/login/provider/error",
session: false
}),
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
);
export default router;

@ -29,11 +29,11 @@ router.patch(
);
router.put(
"/me/auth-methods",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY]
}),
usersController.updateAuthMethods
"/me/auth-methods",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY],
}),
usersController.updateAuthMethods,
);
router.get(

@ -13,16 +13,19 @@ import {
import { createToken } from "../helpers/auth";
import {
getClientIdGitHubLogin,
getClientIdGitLabLogin,
getClientIdGoogleLogin,
getClientSecretGitHubLogin,
getClientSecretGitLabLogin,
getClientSecretGoogleLogin,
getJwtProviderAuthLifetime,
getJwtProviderAuthSecret,
getSiteURL,
getUrlGitLabLogin
} from "../config";
import { getSSOConfigHelper } from "../ee/helpers/organizations";
import { InternalServerError, OrganizationNotFoundError } from "./errors";
import { ACCEPTED, INTEGRATION_GITHUB_API_URL, INVITED, MEMBER } from "../variables";
import { getSiteURL } from "../config";
import { standardRequest } from "../config/request";
// eslint-disable-next-line @typescript-eslint/no-var-requires
@ -30,6 +33,8 @@ const GoogleStrategy = require("passport-google-oauth20").Strategy;
// eslint-disable-next-line @typescript-eslint/no-var-requires
const GitHubStrategy = require("passport-github").Strategy;
// eslint-disable-next-line @typescript-eslint/no-var-requires
const GitLabStrategy = require("passport-gitlab2").Strategy;
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { MultiSamlStrategy } = require("@node-saml/passport-saml");
/**
@ -76,6 +81,9 @@ const initializePassport = async () => {
const clientSecretGoogleLogin = await getClientSecretGoogleLogin();
const clientIdGitHubLogin = await getClientIdGitHubLogin();
const clientSecretGitHubLogin = await getClientSecretGitHubLogin();
const urlGitLab = await getUrlGitLabLogin();
const clientIdGitLabLogin = await getClientIdGitLabLogin();
const clientSecretGitLabLogin = await getClientSecretGitLabLogin();
if (clientIdGoogleLogin && clientSecretGoogleLogin) {
passport.use(new GoogleStrategy({
@ -209,6 +217,60 @@ const initializePassport = async () => {
}
));
}
if (urlGitLab && clientIdGitLabLogin && clientSecretGitLabLogin) {
passport.use(new GitLabStrategy({
passReqToCallback: true,
clientID: clientIdGitLabLogin,
clientSecret: clientSecretGitLabLogin,
callbackURL: "/api/v1/sso/gitlab",
baseURL: urlGitLab
},
async (req : express.Request, accessToken : any, refreshToken : any, profile : any, done : any) => {
const email = profile.emails[0].value;
let user = await User.findOne({
email
}).select("+publicKey");
if (!user) {
user = await new User({
email: email,
authMethods: [AuthMethod.GITLAB],
firstName: profile.displayName,
lastName: ""
}).save();
}
let isLinkingRequired = false;
if (!user.authMethods.includes(AuthMethod.GITLAB)) {
isLinkingRequired = true;
}
const isUserCompleted = !!user.publicKey;
const providerAuthToken = createToken({
payload: {
userId: user._id.toString(),
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
authMethod: AuthMethod.GITLAB,
isUserCompleted,
isLinkingRequired,
...(req.query.state ? {
callbackPort: req.query.state as string
} : {})
},
expiresIn: await getJwtProviderAuthLifetime(),
secret: await getJwtProviderAuthSecret(),
});
req.isUserCompleted = isUserCompleted;
req.providerAuthToken = providerAuthToken;
return done(null, profile);
}
));
}
passport.use("saml", new MultiSamlStrategy(
{

@ -84,7 +84,8 @@ export const INTEGRATION_BITBUCKET_TOKEN_URL = "https://bitbucket.org/site/oauth
// integration apps endpoints
export const INTEGRATION_GCP_API_URL = "https://cloudresourcemanager.googleapis.com";
export const INTEGRATION_HEROKU_API_URL = "https://api.heroku.com";
export const INTEGRATION_GITLAB_API_URL = "https://gitlab.com/api";
export const GITLAB_URL = "https://gitlab.com";
export const INTEGRATION_GITLAB_API_URL = `${GITLAB_URL}/api`;
export const INTEGRATION_GITHUB_API_URL = "https://api.github.com";
export const INTEGRATION_VERCEL_API_URL = "https://api.vercel.com";
export const INTEGRATION_NETLIFY_API_URL = "https://api.netlify.com";

@ -0,0 +1,37 @@
---
title: "GitLab SSO"
description: "Configure GitLab SSO for Infisical"
---
Using GitLab SSO on a self-hosted instance of Infisical requires configuring an OAuth application in GitLab and registering your instance with it.
## Create an OAuth application in GitLab
Navigate to your user Settings > Applications to create a new GitLab application.
![sso gitlab config](../../images/sso/gitlab/edit-profile.png)
![sso gitlab config](../../images/sso/gitlab/new-app.png)
Create the application. As part of the form, set the **Redirect URI** to `https://your-domain.com/api/v1/sso/gitlab`.
Note that only `read_user` is required as part of the **Scopes** configuration.
![sso gitlab config](../../images/sso/gitlab/new-app-form.png)
<Note>
If you have a GitLab group, you can create an OAuth application under it
in your group Settings > Applications.
</Note>
## Add your OAuth application credentials to Infisical
Obtain the **Application ID** and **Secret** for your GitLab application.
![sso gitlab config](../../images/sso/gitlab/credentials.png)
Back in your Infisical instance, add 2-3 new environment variables for the credentials of your GitLab application:
- `CLIENT_ID_GITLAB_LOGIN`: The **Client ID** of your GitLab application.
- `CLIENT_SECRET_GITLAB_LOGIN`: The **Secret** of your GitLab application.
- (optional) `URL_GITLAB_LOGIN`: The URL of your self-hosted instance of GitLab where the OAuth application is registered. If no URL is passed in, this will default to `https://gitlab.com`.
Once added, restart your Infisical instance and log in with GitLab.

@ -19,6 +19,7 @@ your IdP cannot and will not have access to the decryption key needed to decrypt
- [Google SSO](/documentation/platform/sso/google)
- [GitHub SSO](/documentation/platform/sso/github)
- [GitLab SSO](/documentation/platform/sso/gitlab)
- [Okta SAML](/documentation/platform/sso/okta)
- [Azure SAML](/documentation/platform/sso/azure)
- [JumpCloud SAML](/documentation/platform/sso/jumpcloud)

Binary file not shown.

After

Width:  |  Height:  |  Size: 365 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 959 KiB

@ -107,7 +107,7 @@ build-job:
Back in your Infisical instance, add two new environment variables for the credentials of your GitLab application:
- `CLIENT_ID_GITLAB`: The **Client ID** of your GitLab application.
- `CLIENT_SECRET_GITLAB`: The **Client Secret** of your GitLab application.
- `CLIENT_SECRET_GITLAB`: The **Secret** of your GitLab application.
Once added, restart your Infisical instance and use the GitLab integration.

@ -126,6 +126,7 @@
"documentation/platform/sso/overview",
"documentation/platform/sso/google",
"documentation/platform/sso/github",
"documentation/platform/sso/gitlab",
"documentation/platform/sso/okta",
"documentation/platform/sso/azure",
"documentation/platform/sso/jumpcloud"

@ -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>
OAuth2 client secret for GitHub login
</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 title="Others">
#### JWT

@ -15,6 +15,7 @@ You can view specific documentation for how to set up each SSO authentication me
- [Google SSO](/documentation/platform/sso/google)
- [GitHub SSO](/documentation/platform/sso/github)
- [GitLab SSO](/documentation/platform/sso/gitlab)
- [Okta SAML](/documentation/platform/sso/okta)
- [Azure SAML](/documentation/platform/sso/azure)
- [JumpCloud SAML](/documentation/platform/sso/jumpcloud)

@ -1,7 +1,7 @@
import { useTranslation } from "react-i18next";
import Link from "next/link";
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 { faEnvelope } from "@fortawesome/free-regular-svg-icons";
import { faLock } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@ -9,74 +9,94 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Button } from "../v2";
export default function InitialSignupStep({
setIsSignupWithEmail,
setIsSignupWithEmail
}: {
setIsSignupWithEmail: (value: boolean) => void
setIsSignupWithEmail: (value: boolean) => void;
}) {
const { t } = useTranslation();
const router = useRouter();
const { t } = useTranslation();
const router = useRouter();
return <div 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' >{t("signup.initial-title")}</h1>
<div className='lg:w-1/6 w-1/4 min-w-[20rem] rounded-md'>
<Button
colorSchema="primary"
variant="solid"
onClick={() => {
window.open("/api/v1/sso/redirect/google");
window.close();
}}
leftIcon={<FontAwesomeIcon icon={faGoogle} className="mr-2" />}
className="h-12 w-full mx-0"
>
{t("signup.continue-with-google")}
</Button>
</div>
<div className='lg:w-1/6 w-1/4 min-w-[20rem] rounded-md mt-4'>
<Button
colorSchema="primary"
variant="outline_bg"
onClick={() => {
window.open("/api/v1/sso/redirect/github");
window.close();
}}
leftIcon={<FontAwesomeIcon icon={faGithub} className="mr-2" />}
className="h-12 w-full mx-0"
>
Continue with GitHub
</Button>
</div>
<div className='lg:w-1/6 w-1/4 min-w-[20rem] text-center rounded-md mt-4'>
<Button
colorSchema="primary"
variant="outline_bg"
onClick={() => {
setIsSignupWithEmail(true);
}}
leftIcon={<FontAwesomeIcon icon={faEnvelope} className="mr-2" />}
className="h-12 w-full mx-0"
>
Continue with Email
</Button>
</div>
<div className='lg:w-1/6 w-1/4 min-w-[20rem] text-center rounded-md mt-4'>
<Button
colorSchema="primary"
variant="outline_bg"
onClick={() => router.push("/saml-sso")}
leftIcon={<FontAwesomeIcon icon={faLock} className="mr-2" />}
className="h-12 w-full mx-0"
>
Continue with SSO
</Button>
</div>
<div className='lg:w-1/6 w-1/4 min-w-[20rem] px-8 text-center mt-6 text-xs text-bunker-400'>
{t("signup.create-policy")}
</div>
<div className="mt-2 text-bunker-400 text-xs flex flex-row">
<Link href="/login">
<span className='hover:underline hover:underline-offset-4 hover:decoration-primary-700 hover:text-bunker-200 duration-200 cursor-pointer'>{t("signup.already-have-account")}</span>
</Link>
</div>
return (
<div className="mx-auto flex w-full flex-col items-center justify-center">
<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("signup.initial-title")}
</h1>
<div className="w-1/4 min-w-[20rem] rounded-md lg:w-1/6">
<Button
colorSchema="primary"
variant="solid"
onClick={() => {
window.open("/api/v1/sso/redirect/google");
window.close();
}}
leftIcon={<FontAwesomeIcon icon={faGoogle} className="mr-2" />}
className="mx-0 h-12 w-full"
>
{t("signup.continue-with-google")}
</Button>
</div>
<div className="mt-4 w-1/4 min-w-[20rem] rounded-md lg:w-1/6">
<Button
colorSchema="primary"
variant="outline_bg"
onClick={() => {
window.open("/api/v1/sso/redirect/github");
window.close();
}}
leftIcon={<FontAwesomeIcon icon={faGithub} className="mr-2" />}
className="mx-0 h-12 w-full"
>
Continue with GitHub
</Button>
</div>
<div className="mt-4 w-1/4 min-w-[20rem] rounded-md lg:w-1/6">
<Button
colorSchema="primary"
variant="outline_bg"
onClick={() => {
window.open("/api/v1/sso/redirect/gitlab");
window.close();
}}
leftIcon={<FontAwesomeIcon icon={faGitlab} className="mr-2" />}
className="mx-0 h-12 w-full"
>
Continue with GitLab
</Button>
</div>
<div className="mt-4 w-1/4 min-w-[20rem] rounded-md text-center lg:w-1/6">
<Button
colorSchema="primary"
variant="outline_bg"
onClick={() => {
setIsSignupWithEmail(true);
}}
leftIcon={<FontAwesomeIcon icon={faEnvelope} className="mr-2" />}
className="mx-0 h-12 w-full"
>
Continue with Email
</Button>
</div>
<div className="mt-4 w-1/4 min-w-[20rem] rounded-md text-center lg:w-1/6">
<Button
colorSchema="primary"
variant="outline_bg"
onClick={() => router.push("/saml-sso")}
leftIcon={<FontAwesomeIcon icon={faLock} className="mr-2" />}
className="mx-0 h-12 w-full"
>
Continue with SSO
</Button>
</div>
<div className="mt-6 w-1/4 min-w-[20rem] px-8 text-center text-xs text-bunker-400 lg:w-1/6">
{t("signup.create-policy")}
</div>
<div className="mt-2 flex flex-row text-xs text-bunker-400">
<Link href="/login">
<span className="cursor-pointer duration-200 hover:text-bunker-200 hover:underline hover:decoration-primary-700 hover:underline-offset-4">
{t("signup.already-have-account")}
</span>
</Link>
</div>
</div>
);
}

@ -4,9 +4,10 @@ export enum AuthMethod {
EMAIL = "email",
GOOGLE = "google",
GITHUB = "github",
OKTA_SAML = "okta-saml",
AZURE_SAML = "azure-saml",
JUMPCLOUD_SAML = "jumpcloud-saml"
GITLAB = "gitlab",
OKTA_SAML = "okta-saml",
AZURE_SAML = "azure-saml",
JUMPCLOUD_SAML = "jumpcloud-saml"
}
export type User = {

@ -2,10 +2,10 @@ import { FormEvent, useState } from "react";
import { useTranslation } from "react-i18next";
import Link from "next/link";
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 { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import axios from "axios"
import axios from "axios";
import Error from "@app/components/basic/Error";
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";
type Props = {
setStep: (step: number) => void;
email: string;
setEmail: (email: string) => void;
password: string;
setPassword: (email: string) => void;
}
export const InitialStep = ({
setStep,
email,
setEmail,
password,
setPassword
}: Props) => {
const router = useRouter();
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>) => {
e.preventDefault()
try {
if (!email || !password) {
return;
}
setIsLoading(true);
if (queryParams && queryParams.get("callback_port")) {
const callbackPort = queryParams.get("callback_port")
// attemptCliLogin
const isCliLoginSuccessful = await attemptCliLogin({
email: email.toLowerCase(),
password,
})
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}`
// 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 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"
});
setStep: (step: number) => void;
email: string;
setEmail: (email: string) => void;
password: string;
setPassword: (email: string) => void;
};
export const InitialStep = ({ setStep, email, setEmail, password, setPassword }: Props) => {
const router = useRouter();
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>) => {
e.preventDefault();
try {
if (!email || !password) {
return;
}
setIsLoading(true);
if (queryParams && queryParams.get("callback_port")) {
const callbackPort = queryParams.get("callback_port");
// attemptCliLogin
const isCliLoginSuccessful = await attemptCliLogin({
email: email.toLowerCase(),
password
});
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}`;
// 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
}
setIsLoading(false);
} 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"
});
}
return (
<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}` : ""}`);
window.close();
}}
leftIcon={<FontAwesomeIcon icon={faGoogle} className="mr-2" />}
className="h-11 w-full mx-0"
>
{t("login.continue-with-google")}
</Button>
</div>
<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/github${callbackPort ? `?callback_port=${callbackPort}` : ""}`);
window.close();
}}
leftIcon={<FontAwesomeIcon icon={faGithub} className="mr-2" />}
className="h-11 w-full mx-0"
>
Continue with GitHub
</Button>
</div>
<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={() => {
setStep(2);
}}
leftIcon={<FontAwesomeIcon icon={faLock} className="mr-2" />}
className="h-11 w-full mx-0"
>
Continue with SSO
</Button>
</div>
<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>
<div className='w-full border-t border-mineshaft-400/60' />
</div>
<div className='lg:w-1/6 w-1/4 min-w-[21.2rem] md:min-w-[20.1rem] text-center rounded-md'>
<Input
value={email}
onChange={(e) => setEmail(e.target.value)}
type="email"
placeholder="Enter your email..."
isRequired
autoComplete="username"
className="h-11"
/>
</div>
<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
value={password}
onChange={(e) => setPassword(e.target.value)}
type="password"
placeholder="Enter your password..."
isRequired
autoComplete="current-password"
id="current-password"
className="h-11 select:-webkit-autofill:focus"
/>
</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'>
<Button
type="submit"
size="sm"
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 text-bunker-400 text-sm flex flex-row">
<span className="mr-1">Don&apos;t have an acount yet?</span>
<Link href="/signup">
<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>
</Link>
</div> : <div />
}
<div className="text-bunker-400 text-sm flex flex-row">
<span className="mr-1">Forgot password?</span>
<Link href="/verify-email">
<span className='hover:underline hover:underline-offset-4 hover:decoration-primary-700 hover:text-bunker-200 duration-200 cursor-pointer'>Recover your account</span>
</Link>
</div>
</form>
);
}
setIsLoading(false);
};
return (
<form
onSubmit={handleLogin}
className="mx-auto flex w-full flex-col items-center justify-center"
>
<h1 className="mb-8 bg-gradient-to-b from-white to-bunker-200 bg-clip-text text-center text-xl font-medium text-transparent">
Login to Infisical
</h1>
<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.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.open(
`/api/v1/sso/redirect/github${callbackPort ? `?callback_port=${callbackPort}` : ""}`
);
window.close();
}}
leftIcon={<FontAwesomeIcon icon={faGithub} className="mr-2" />}
className="mx-0 h-11 w-full"
>
Continue with GitHub
</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.open(
`/api/v1/sso/redirect/gitlab${callbackPort ? `?callback_port=${callbackPort}` : ""}`
);
window.close();
}}
leftIcon={<FontAwesomeIcon icon={faGitlab} className="mr-2" />}
className="mx-0 h-11 w-full"
>
Continue with GitLab
</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={() => {
setStep(2);
}}
leftIcon={<FontAwesomeIcon icon={faLock} className="mr-2" />}
className="mx-0 h-11 w-full"
>
Continue with SSO
</Button>
</div>
<div className="my-4 flex w-1/4 min-w-[20rem] flex-row items-center py-2 lg:w-1/6">
<div className="w-full border-t border-mineshaft-400/60" />
<span className="mx-2 text-xs text-mineshaft-200">or</span>
<div className="w-full border-t border-mineshaft-400/60" />
</div>
<div className="w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
<Input
value={email}
onChange={(e) => setEmail(e.target.value)}
type="email"
placeholder="Enter your email..."
isRequired
autoComplete="username"
className="h-11"
/>
</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">
<Input
value={password}
onChange={(e) => setPassword(e.target.value)}
type="password"
placeholder="Enter your password..."
isRequired
autoComplete="current-password"
id="current-password"
className="select:-webkit-autofill:focus h-11"
/>
</div>
<div className="mt-5 w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
<Button
type="submit"
size="sm"
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 { 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 { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
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 { useUser } from "@app/context";
import { useUpdateUserAuthMethods } from "@app/hooks/api";
import {
AuthMethod
} from "@app/hooks/api/users/types";
import { AuthMethod } from "@app/hooks/api/users/types";
interface AuthMethodOption {
label: string,
value: AuthMethod,
icon: IconDefinition;
label: string;
value: AuthMethod;
icon: IconDefinition;
}
const authMethodOpts: AuthMethodOption[] = [
{ label: "Email", value: AuthMethod.EMAIL, icon: faEnvelope },
{ label: "Google", value: AuthMethod.GOOGLE, icon: faGoogle },
{ label: "GitHub", value: AuthMethod.GITHUB, icon: faGithub }
{ label: "Email", value: AuthMethod.EMAIL, icon: faEnvelope },
{ label: "Google", value: AuthMethod.GOOGLE, icon: faGoogle },
{ 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 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 const AuthMethodSection = () => {
const { createNotification } = useNotificationContext();
const { user } = useUser();
const { mutateAsync } = useUpdateUserAuthMethods();
const {
reset,
setValue,
watch,
} = useForm<FormData>({
defaultValues: {
authMethods: user.authMethods,
},
resolver: yupResolver(schema)
});
const authMethods = watch("authMethods");
useEffect(() => {
if (user) {
reset({
authMethods: user.authMethods,
});
}
}, [user]);
const onAuthMethodToggle = async (value: boolean, authMethodOpt: AuthMethodOption) => {
const hasSamlEnabled = user.authMethods
.some((authMethod: AuthMethod) => samlProviders.includes(authMethod));
if (hasSamlEnabled) {
createNotification({
text: "SAML authentication can only be configured in your organization settings",
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"
});
const { createNotification } = useNotificationContext();
const { user } = useUser();
const { mutateAsync } = useUpdateUserAuthMethods();
const { reset, setValue, watch } = useForm<FormData>({
defaultValues: {
authMethods: user.authMethods
},
resolver: yupResolver(schema)
});
const authMethods = watch("authMethods");
useEffect(() => {
if (user) {
reset({
authMethods: user.authMethods
});
}
return (
<div className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600">
<h2 className="text-xl font-semibold flex-1 text-mineshaft-100 mb-8">
Authentication methods
</h2>
<p className="text-gray-400 mb-4">
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 p-4 items-center" 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="w-12 mr-4">{authMethodOpt.label}</p>
</Switch>
</div>
);
})}
</div>
</div>
}, [user]);
const onAuthMethodToggle = async (value: boolean, authMethodOpt: AuthMethodOption) => {
const hasSamlEnabled = user.authMethods.some((authMethod: AuthMethod) =>
samlProviders.includes(authMethod)
);
}
if (hasSamlEnabled) {
createNotification({
text: "SAML authentication can only be configured in your organization settings",
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 (
<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>
);
};

Loading…
Cancel
Save