Merge pull request #1029 from atimapreandrew/gitlab-sso

Gitlab sso
This commit is contained in:
BlackMagiq
2023-10-04 21:39:52 +01:00
committed by GitHub
25 changed files with 648 additions and 429 deletions

View File

@@ -50,6 +50,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",
@@ -13727,6 +13728,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",
@@ -27163,6 +27175,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",

View File

@@ -41,6 +41,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",

View File

@@ -1,3 +1,5 @@
import { GITLAB_URL } from "../variables";
import InfisicalClient from "infisical-node"; import InfisicalClient from "infisical-node";
export const client = new InfisicalClient({ 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 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 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 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";

View File

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

View File

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

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",

View File

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

View File

@@ -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;

View File

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

View File

@@ -13,16 +13,19 @@ import {
import { createToken } from "../helpers/auth"; import { createToken } from "../helpers/auth";
import { import {
getClientIdGitHubLogin, getClientIdGitHubLogin,
getClientIdGitLabLogin,
getClientIdGoogleLogin, getClientIdGoogleLogin,
getClientSecretGitHubLogin, getClientSecretGitHubLogin,
getClientSecretGitLabLogin,
getClientSecretGoogleLogin, getClientSecretGoogleLogin,
getJwtProviderAuthLifetime, getJwtProviderAuthLifetime,
getJwtProviderAuthSecret, getJwtProviderAuthSecret,
getSiteURL,
getUrlGitLabLogin
} from "../config"; } from "../config";
import { getSSOConfigHelper } from "../ee/helpers/organizations"; import { getSSOConfigHelper } from "../ee/helpers/organizations";
import { InternalServerError, OrganizationNotFoundError } from "./errors"; import { InternalServerError, OrganizationNotFoundError } from "./errors";
import { ACCEPTED, INTEGRATION_GITHUB_API_URL, INVITED, MEMBER } from "../variables"; import { ACCEPTED, INTEGRATION_GITHUB_API_URL, INVITED, MEMBER } from "../variables";
import { getSiteURL } from "../config";
import { standardRequest } from "../config/request"; import { standardRequest } from "../config/request";
// eslint-disable-next-line @typescript-eslint/no-var-requires // 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 // eslint-disable-next-line @typescript-eslint/no-var-requires
const GitHubStrategy = require("passport-github").Strategy; const GitHubStrategy = require("passport-github").Strategy;
// eslint-disable-next-line @typescript-eslint/no-var-requires // 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"); const { MultiSamlStrategy } = require("@node-saml/passport-saml");
/** /**
@@ -76,6 +81,9 @@ const initializePassport = async () => {
const clientSecretGoogleLogin = await getClientSecretGoogleLogin(); const clientSecretGoogleLogin = await getClientSecretGoogleLogin();
const clientIdGitHubLogin = await getClientIdGitHubLogin(); const clientIdGitHubLogin = await getClientIdGitHubLogin();
const clientSecretGitHubLogin = await getClientSecretGitHubLogin(); const clientSecretGitHubLogin = await getClientSecretGitHubLogin();
const urlGitLab = await getUrlGitLabLogin();
const clientIdGitLabLogin = await getClientIdGitLabLogin();
const clientSecretGitLabLogin = await getClientSecretGitLabLogin();
if (clientIdGoogleLogin && clientSecretGoogleLogin) { if (clientIdGoogleLogin && clientSecretGoogleLogin) {
passport.use(new GoogleStrategy({ passport.use(new GoogleStrategy({
@@ -210,6 +218,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( passport.use("saml", new MultiSamlStrategy(
{ {
passReqToCallback: true, passReqToCallback: true,

View File

@@ -84,7 +84,8 @@ export const INTEGRATION_BITBUCKET_TOKEN_URL = "https://bitbucket.org/site/oauth
// integration apps endpoints // integration apps endpoints
export const INTEGRATION_GCP_API_URL = "https://cloudresourcemanager.googleapis.com"; export const INTEGRATION_GCP_API_URL = "https://cloudresourcemanager.googleapis.com";
export const INTEGRATION_HEROKU_API_URL = "https://api.heroku.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_GITHUB_API_URL = "https://api.github.com";
export const INTEGRATION_VERCEL_API_URL = "https://api.vercel.com"; export const INTEGRATION_VERCEL_API_URL = "https://api.vercel.com";
export const INTEGRATION_NETLIFY_API_URL = "https://api.netlify.com"; export const INTEGRATION_NETLIFY_API_URL = "https://api.netlify.com";

View File

@@ -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.

View File

@@ -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) - [Google SSO](/documentation/platform/sso/google)
- [GitHub SSO](/documentation/platform/sso/github) - [GitHub SSO](/documentation/platform/sso/github)
- [GitLab SSO](/documentation/platform/sso/gitlab)
- [Okta SAML](/documentation/platform/sso/okta) - [Okta SAML](/documentation/platform/sso/okta)
- [Azure SAML](/documentation/platform/sso/azure) - [Azure SAML](/documentation/platform/sso/azure)
- [JumpCloud SAML](/documentation/platform/sso/jumpcloud) - [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

View File

@@ -107,7 +107,7 @@ build-job:
Back in your Infisical instance, add two new environment variables for the credentials of your GitLab application: 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_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. Once added, restart your Infisical instance and use the GitLab integration.

View File

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

View File

@@ -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

View File

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

View File

@@ -1,7 +1,7 @@
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 { faEnvelope } from "@fortawesome/free-regular-svg-icons"; import { faEnvelope } from "@fortawesome/free-regular-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";
@@ -9,16 +9,19 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Button } from "../v2"; import { Button } from "../v2";
export default function InitialSignupStep({ export default function InitialSignupStep({
setIsSignupWithEmail, setIsSignupWithEmail
}: { }: {
setIsSignupWithEmail: (value: boolean) => void setIsSignupWithEmail: (value: boolean) => void;
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const router = useRouter(); const router = useRouter();
return <div className='flex flex-col mx-auto w-full justify-center items-center'> return (
<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="mx-auto flex w-full flex-col items-center justify-center">
<div className='lg:w-1/6 w-1/4 min-w-[20rem] rounded-md'> <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 <Button
colorSchema="primary" colorSchema="primary"
variant="solid" variant="solid"
@@ -27,12 +30,12 @@ export default function InitialSignupStep({
window.close(); window.close();
}} }}
leftIcon={<FontAwesomeIcon icon={faGoogle} className="mr-2" />} leftIcon={<FontAwesomeIcon icon={faGoogle} className="mr-2" />}
className="h-12 w-full mx-0" className="mx-0 h-12 w-full"
> >
{t("signup.continue-with-google")} {t("signup.continue-with-google")}
</Button> </Button>
</div> </div>
<div className='lg:w-1/6 w-1/4 min-w-[20rem] rounded-md mt-4'> <div className="mt-4 w-1/4 min-w-[20rem] rounded-md lg:w-1/6">
<Button <Button
colorSchema="primary" colorSchema="primary"
variant="outline_bg" variant="outline_bg"
@@ -41,12 +44,26 @@ export default function InitialSignupStep({
window.close(); window.close();
}} }}
leftIcon={<FontAwesomeIcon icon={faGithub} className="mr-2" />} leftIcon={<FontAwesomeIcon icon={faGithub} className="mr-2" />}
className="h-12 w-full mx-0" className="mx-0 h-12 w-full"
> >
Continue with GitHub Continue with GitHub
</Button> </Button>
</div> </div>
<div className='lg:w-1/6 w-1/4 min-w-[20rem] text-center rounded-md mt-4'> <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 <Button
colorSchema="primary" colorSchema="primary"
variant="outline_bg" variant="outline_bg"
@@ -54,29 +71,32 @@ export default function InitialSignupStep({
setIsSignupWithEmail(true); setIsSignupWithEmail(true);
}} }}
leftIcon={<FontAwesomeIcon icon={faEnvelope} className="mr-2" />} leftIcon={<FontAwesomeIcon icon={faEnvelope} className="mr-2" />}
className="h-12 w-full mx-0" className="mx-0 h-12 w-full"
> >
Continue with Email Continue with Email
</Button> </Button>
</div> </div>
<div className='lg:w-1/6 w-1/4 min-w-[20rem] text-center rounded-md mt-4'> <div className="mt-4 w-1/4 min-w-[20rem] rounded-md text-center lg:w-1/6">
<Button <Button
colorSchema="primary" colorSchema="primary"
variant="outline_bg" variant="outline_bg"
onClick={() => router.push("/saml-sso")} onClick={() => router.push("/saml-sso")}
leftIcon={<FontAwesomeIcon icon={faLock} className="mr-2" />} leftIcon={<FontAwesomeIcon icon={faLock} className="mr-2" />}
className="h-12 w-full mx-0" className="mx-0 h-12 w-full"
> >
Continue with SSO Continue with SSO
</Button> </Button>
</div> </div>
<div className='lg:w-1/6 w-1/4 min-w-[20rem] px-8 text-center mt-6 text-xs text-bunker-400'> <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")} {t("signup.create-policy")}
</div> </div>
<div className="mt-2 text-bunker-400 text-xs flex flex-row"> <div className="mt-2 flex flex-row text-xs text-bunker-400">
<Link href="/login"> <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> <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> </Link>
</div> </div>
</div> </div>
);
} }

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"

View File

@@ -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";
@@ -21,15 +21,9 @@ type Props = {
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,
email,
setEmail,
password,
setPassword
}: Props) => {
const router = useRouter(); const router = useRouter();
const { createNotification } = useNotificationContext(); const { createNotification } = useNotificationContext();
const { t } = useTranslation(); const { t } = useTranslation();
@@ -39,7 +33,7 @@ export const InitialStep = ({
const queryParams = new URLSearchParams(window.location.search); 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;
@@ -47,16 +41,15 @@ export const InitialStep = ({
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) { if (isCliLoginSuccessful.mfaEnabled) {
// case: login requires MFA step // case: login requires MFA step
setStep(1); setStep(1);
@@ -64,22 +57,21 @@ export const InitialStep = ({
return; return;
} }
// case: login was successful // case: login was successful
const cliUrl = `http://localhost:${callbackPort}` const cliUrl = `http://localhost:${callbackPort}`;
// send request to server endpoint // send request to server endpoint
const instance = axios.create() const instance = axios.create();
await instance.post(cliUrl, { ...isCliLoginSuccessful.loginResponse }) await instance.post(cliUrl, { ...isCliLoginSuccessful.loginResponse });
// cli page // cli page
router.push("/cli-redirect"); router.push("/cli-redirect");
// on success, router.push to cli Login Successful page // on success, router.push to cli Login Successful page
} }
} else { } else {
const isLoginSuccessful = await attemptLogin({ const isLoginSuccessful = await attemptLogin({
email: email.toLowerCase(), email: email.toLowerCase(),
password, password
}); });
if (isLoginSuccessful && isLoginSuccessful.success) { if (isLoginSuccessful && isLoginSuccessful.success) {
// case: login was successful // case: login was successful
@@ -101,8 +93,6 @@ export const InitialStep = ({
router.push(`/org/${userOrg}/overview`); router.push(`/org/${userOrg}/overview`);
} }
} }
} catch (err) { } catch (err) {
setLoginError(true); setLoginError(true);
createNotification({ createNotification({
@@ -112,45 +102,73 @@ export const InitialStep = ({
} }
setIsLoading(false); setIsLoading(false);
} };
return ( return (
<form onSubmit={handleLogin} className='flex flex-col mx-auto w-full justify-center items-center'> <form
<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> onSubmit={handleLogin}
<div className='lg:w-1/6 w-1/4 min-w-[21.2rem] md:min-w-[20.1rem] text-center rounded-md mt-4'> 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 <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/google${callbackPort ? `?callback_port=${callbackPort}` : ""}`); window.open(
`/api/v1/sso/redirect/google${callbackPort ? `?callback_port=${callbackPort}` : ""}`
);
window.close(); window.close();
}} }}
leftIcon={<FontAwesomeIcon icon={faGoogle} className="mr-2" />} leftIcon={<FontAwesomeIcon icon={faGoogle} className="mr-2" />}
className="h-11 w-full mx-0" className="mx-0 h-11 w-full"
> >
{t("login.continue-with-google")} {t("login.continue-with-google")}
</Button> </Button>
</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-4'> <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 <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/github${callbackPort ? `?callback_port=${callbackPort}` : ""}`
);
window.close(); window.close();
}} }}
leftIcon={<FontAwesomeIcon icon={faGithub} className="mr-2" />} leftIcon={<FontAwesomeIcon icon={faGithub} className="mr-2" />}
className="h-11 w-full mx-0" className="mx-0 h-11 w-full"
> >
Continue with GitHub Continue with GitHub
</Button> </Button>
</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-4'> <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 <Button
colorSchema="primary" colorSchema="primary"
variant="outline_bg" variant="outline_bg"
@@ -158,17 +176,17 @@ export const InitialStep = ({
setStep(2); setStep(2);
}} }}
leftIcon={<FontAwesomeIcon icon={faLock} className="mr-2" />} leftIcon={<FontAwesomeIcon icon={faLock} className="mr-2" />}
className="h-11 w-full mx-0" className="mx-0 h-11 w-full"
> >
Continue with SSO Continue with SSO
</Button> </Button>
</div> </div>
<div className='lg:w-1/6 w-1/4 min-w-[20rem] flex flex-row items-center my-4 py-2'> <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' /> <div className="w-full border-t border-mineshaft-400/60" />
<span className="mx-2 text-mineshaft-200 text-xs">or</span> <span className="mx-2 text-xs text-mineshaft-200">or</span>
<div className='w-full border-t border-mineshaft-400/60' /> <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'> <div className="w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
<Input <Input
value={email} value={email}
onChange={(e) => setEmail(e.target.value)} onChange={(e) => setEmail(e.target.value)}
@@ -179,7 +197,7 @@ export const InitialStep = ({
className="h-11" className="h-11"
/> />
</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-4'> <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 <Input
value={password} value={password}
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
@@ -188,36 +206,44 @@ export const InitialStep = ({
isRequired isRequired
autoComplete="current-password" autoComplete="current-password"
id="current-password" id="current-password"
className="h-11 select:-webkit-autofill:focus" className="select:-webkit-autofill:focus h-11"
/> />
</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="mt-5 w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
<Button <Button
type="submit" type="submit"
size="sm" size="sm"
isFullWidth isFullWidth
className='h-11' className="h-11"
colorSchema="primary" colorSchema="primary"
variant="solid" variant="solid"
isLoading={isLoading} isLoading={isLoading}
> Continue with Email </Button> >
{" "}
Continue with Email{" "}
</Button>
</div> </div>
{!isLoading && loginError && <Error text={t("login.error-login") ?? ""} />} {!isLoading && loginError && <Error text={t("login.error-login") ?? ""} />}
{ {!serverDetails?.inviteOnlySignup ? (
!serverDetails?.inviteOnlySignup ? <div className="mt-6 flex flex-row text-sm text-bunker-400">
<div className="mt-6 text-bunker-400 text-sm flex flex-row">
<span className="mr-1">Don&apos;t have an acount yet?</span> <span className="mr-1">Don&apos;t have an acount yet?</span>
<Link href="/signup"> <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> <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> </Link>
</div> : <div /> </div>
} ) : (
<div className="text-bunker-400 text-sm flex flex-row"> <div />
)}
<div className="flex flex-row text-sm text-bunker-400">
<span className="mr-1">Forgot password?</span> <span className="mr-1">Forgot password?</span>
<Link href="/verify-email"> <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> <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> </Link>
</div> </div>
</form> </form>
); );
} };

View File

@@ -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,20 +10,19 @@ 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];
@@ -39,13 +38,9 @@ export const AuthMethodSection = () => {
const { user } = useUser(); const { user } = useUser();
const { mutateAsync } = useUpdateUserAuthMethods(); const { mutateAsync } = useUpdateUserAuthMethods();
const { const { reset, setValue, watch } = useForm<FormData>({
reset,
setValue,
watch,
} = useForm<FormData>({
defaultValues: { defaultValues: {
authMethods: user.authMethods, authMethods: user.authMethods
}, },
resolver: yupResolver(schema) resolver: yupResolver(schema)
}); });
@@ -55,14 +50,15 @@ export const AuthMethodSection = () => {
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({
@@ -73,7 +69,7 @@ export const AuthMethodSection = () => {
const newAuthMethods = value const newAuthMethods = value
? [...authMethods, authMethodOpt.value] ? [...authMethods, authMethodOpt.value]
: authMethods.filter(auth => auth !== authMethodOpt.value); : authMethods.filter((auth) => auth !== authMethodOpt.value);
if (value) { if (value) {
const newUser = await mutateAsync({ const newUser = await mutateAsync({
@@ -105,20 +101,22 @@ export const AuthMethodSection = () => {
text: "Successfully disabled authentication method", text: "Successfully disabled authentication method",
type: "success" type: "success"
}); });
} };
return ( return (
<div className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600"> <div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<h2 className="text-xl font-semibold flex-1 text-mineshaft-100 mb-8"> <h2 className="mb-8 flex-1 text-xl font-semibold text-mineshaft-100">
Authentication methods Authentication methods
</h2> </h2>
<p className="text-gray-400 mb-4"> <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. 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> </p>
<div className="mb-4"> <div className="mb-4">
{user && authMethodOpts.map((authMethodOpt) => { {user &&
authMethodOpts.map((authMethodOpt) => {
return ( return (
<div className="flex p-4 items-center" key={`auth-method-${authMethodOpt.value}`}> <div className="flex items-center p-4" key={`auth-method-${authMethodOpt.value}`}>
<div className="flex items-center"> <div className="flex items-center">
<FontAwesomeIcon icon={authMethodOpt.icon} className="mr-4" /> <FontAwesomeIcon icon={authMethodOpt.icon} className="mr-4" />
</div> </div>
@@ -127,7 +125,7 @@ export const AuthMethodSection = () => {
onCheckedChange={(value) => onAuthMethodToggle(value, authMethodOpt)} onCheckedChange={(value) => onAuthMethodToggle(value, authMethodOpt)}
isChecked={authMethods?.includes(authMethodOpt.value) ?? false} isChecked={authMethods?.includes(authMethodOpt.value) ?? false}
> >
<p className="w-12 mr-4">{authMethodOpt.label}</p> <p className="mr-4 w-12">{authMethodOpt.label}</p>
</Switch> </Switch>
</div> </div>
); );
@@ -135,4 +133,4 @@ export const AuthMethodSection = () => {
</div> </div>
</div> </div>
); );
} };