diff --git a/backend/package-lock.json b/backend/package-lock.json index 1b6ab8eef8..3429181a20 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -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", diff --git a/backend/package.json b/backend/package.json index 8f2faab313..cd38aa20f2 100644 --- a/backend/package.json +++ b/backend/package.json @@ -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", diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index b5e50d9dac..5f055b73bd 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -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"; diff --git a/backend/src/ee/routes/v1/sso.ts b/backend/src/ee/routes/v1/sso.ts index 564073aecc..baa75d505c 100644 --- a/backend/src/ee/routes/v1/sso.ts +++ b/backend/src/ee/routes/v1/sso.ts @@ -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", { diff --git a/backend/src/index.ts b/backend/src/index.ts index 3d6dfaaa9d..c516f06c24 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -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); diff --git a/backend/src/models/user.ts b/backend/src/models/user.ts index c85c0936bf..911776a639 100644 --- a/backend/src/models/user.ts +++ b/backend/src/models/user.ts @@ -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", diff --git a/backend/src/routes/v1/index.ts b/backend/src/routes/v1/index.ts index cfdccdc926..50275d00f2 100644 --- a/backend/src/routes/v1/index.ts +++ b/backend/src/routes/v1/index.ts @@ -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 }; diff --git a/backend/src/routes/v1/sso.ts b/backend/src/routes/v1/sso.ts new file mode 100644 index 0000000000..b06ba99864 --- /dev/null +++ b/backend/src/routes/v1/sso.ts @@ -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; \ No newline at end of file diff --git a/backend/src/routes/v2/users.ts b/backend/src/routes/v2/users.ts index c093be21c4..41d4c8e6b9 100644 --- a/backend/src/routes/v2/users.ts +++ b/backend/src/routes/v2/users.ts @@ -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( diff --git a/backend/src/utils/auth.ts b/backend/src/utils/auth.ts index 3a04639a6a..49a77cef65 100644 --- a/backend/src/utils/auth.ts +++ b/backend/src/utils/auth.ts @@ -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( { diff --git a/backend/src/variables/integration.ts b/backend/src/variables/integration.ts index 3adfad4a84..e6e20e73c2 100644 --- a/backend/src/variables/integration.ts +++ b/backend/src/variables/integration.ts @@ -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"; diff --git a/docs/documentation/platform/sso/gitlab.mdx b/docs/documentation/platform/sso/gitlab.mdx new file mode 100644 index 0000000000..354948f7d7 --- /dev/null +++ b/docs/documentation/platform/sso/gitlab.mdx @@ -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) + + + If you have a GitLab group, you can create an OAuth application under it + in your group Settings > Applications. + + +## 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. \ No newline at end of file diff --git a/docs/documentation/platform/sso/overview.mdx b/docs/documentation/platform/sso/overview.mdx index 359f09fb3b..917bb7e194 100644 --- a/docs/documentation/platform/sso/overview.mdx +++ b/docs/documentation/platform/sso/overview.mdx @@ -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) \ No newline at end of file diff --git a/docs/images/sso/gitlab/credentials.png b/docs/images/sso/gitlab/credentials.png new file mode 100644 index 0000000000..f44223a1ef Binary files /dev/null and b/docs/images/sso/gitlab/credentials.png differ diff --git a/docs/images/sso/gitlab/edit-profile.png b/docs/images/sso/gitlab/edit-profile.png new file mode 100644 index 0000000000..c6eb4d95ae Binary files /dev/null and b/docs/images/sso/gitlab/edit-profile.png differ diff --git a/docs/images/sso/gitlab/new-app-form.png b/docs/images/sso/gitlab/new-app-form.png new file mode 100644 index 0000000000..988778b907 Binary files /dev/null and b/docs/images/sso/gitlab/new-app-form.png differ diff --git a/docs/images/sso/gitlab/new-app.png b/docs/images/sso/gitlab/new-app.png new file mode 100644 index 0000000000..fac7490a60 Binary files /dev/null and b/docs/images/sso/gitlab/new-app.png differ diff --git a/docs/integrations/cicd/gitlab.mdx b/docs/integrations/cicd/gitlab.mdx index 9c3df97208..8635a55274 100644 --- a/docs/integrations/cicd/gitlab.mdx +++ b/docs/integrations/cicd/gitlab.mdx @@ -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. diff --git a/docs/mint.json b/docs/mint.json index 41c406bc68..9ed8b8ab42 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -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" diff --git a/docs/self-hosting/configuration/envars.mdx b/docs/self-hosting/configuration/envars.mdx index d03f571283..eeb0e60312 100644 --- a/docs/self-hosting/configuration/envars.mdx +++ b/docs/self-hosting/configuration/envars.mdx @@ -155,6 +155,12 @@ Other environment variables are listed below to increase the functionality of yo OAuth2 client secret for GitHub login + + OAuth2 client ID for GitLab login + + + OAuth2 client secret for GitLab login + #### JWT diff --git a/docs/self-hosting/configuration/sso.mdx b/docs/self-hosting/configuration/sso.mdx index 2497e368b6..2d663790d2 100644 --- a/docs/self-hosting/configuration/sso.mdx +++ b/docs/self-hosting/configuration/sso.mdx @@ -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) \ No newline at end of file diff --git a/frontend/src/components/signup/InitialSignupStep.tsx b/frontend/src/components/signup/InitialSignupStep.tsx index 0a1248b2db..7953ff0dfd 100644 --- a/frontend/src/components/signup/InitialSignupStep.tsx +++ b/frontend/src/components/signup/InitialSignupStep.tsx @@ -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
-

{t("signup.initial-title")}

-
- -
-
- -
-
- -
-
- -
-
- {t("signup.create-policy")} -
-
- - {t("signup.already-have-account")} - -
+ return ( +
+

+ {t("signup.initial-title")} +

+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ {t("signup.create-policy")} +
+
+ + + {t("signup.already-have-account")} + + +
+ ); } diff --git a/frontend/src/hooks/api/users/types.ts b/frontend/src/hooks/api/users/types.ts index 07f497c078..6917bcdc71 100644 --- a/frontend/src/hooks/api/users/types.ts +++ b/frontend/src/hooks/api/users/types.ts @@ -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 = { diff --git a/frontend/src/views/Login/components/InitialStep/InitialStep.tsx b/frontend/src/views/Login/components/InitialStep/InitialStep.tsx index 7bab7414e6..50eaf5cb71 100644 --- a/frontend/src/views/Login/components/InitialStep/InitialStep.tsx +++ b/frontend/src/views/Login/components/InitialStep/InitialStep.tsx @@ -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; -} + 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); +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) => { - e.preventDefault() - try { - if (!email || !password) { - return; - } + const handleLogin = async (e: FormEvent) => { + e.preventDefault(); + try { + if (!email || !password) { + return; + } - setIsLoading(true); - if (queryParams && queryParams.get("callback_port")) { - const callbackPort = queryParams.get("callback_port") + setIsLoading(true); + if (queryParams && queryParams.get("callback_port")) { + const callbackPort = queryParams.get("callback_port"); - // attemptCliLogin - const isCliLoginSuccessful = await attemptCliLogin({ - email: email.toLowerCase(), - password, - }) + // attemptCliLogin + const isCliLoginSuccessful = await attemptCliLogin({ + email: email.toLowerCase(), + 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) { - // 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 }); - // send request to server endpoint - const instance = axios.create() - await instance.post(cliUrl, { ...isCliLoginSuccessful.loginResponse }) + // cli page + router.push("/cli-redirect"); - // 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" - }); + // 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 - 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 ( -
-

Login to Infisical

-
- -
-
- -
-
- -
-
-
- or -
-
-
- setEmail(e.target.value)} - type="email" - placeholder="Enter your email..." - isRequired - autoComplete="username" - className="h-11" - /> -
-
- setPassword(e.target.value)} - type="password" - placeholder="Enter your password..." - isRequired - autoComplete="current-password" - id="current-password" - className="h-11 select:-webkit-autofill:focus" - /> -
-
- -
- {!isLoading && loginError && } - { - !serverDetails?.inviteOnlySignup ? -
- Don't have an acount yet? - - {t("login.create-account")} - -
:
- } -
- Forgot password? - - Recover your account - -
- - ); -} \ No newline at end of file + setIsLoading(false); + }; + + return ( +
+

+ Login to Infisical +

+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ or +
+
+
+ setEmail(e.target.value)} + type="email" + placeholder="Enter your email..." + isRequired + autoComplete="username" + className="h-11" + /> +
+
+ setPassword(e.target.value)} + type="password" + placeholder="Enter your password..." + isRequired + autoComplete="current-password" + id="current-password" + className="select:-webkit-autofill:focus h-11" + /> +
+
+ +
+ {!isLoading && loginError && } + {!serverDetails?.inviteOnlySignup ? ( +
+ Don't have an acount yet? + + + {t("login.create-account")} + + +
+ ) : ( +
+ )} +
+ Forgot password? + + + Recover your account + + +
+ + ); +}; diff --git a/frontend/src/views/Settings/PersonalSettingsPage/AuthMethodSection/AuthMethodSection.tsx b/frontend/src/views/Settings/PersonalSettingsPage/AuthMethodSection/AuthMethodSection.tsx index 42917aae68..73d266e2eb 100644 --- a/frontend/src/views/Settings/PersonalSettingsPage/AuthMethodSection/AuthMethodSection.tsx +++ b/frontend/src/views/Settings/PersonalSettingsPage/AuthMethodSection/AuthMethodSection.tsx @@ -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; export const AuthMethodSection = () => { - const { createNotification } = useNotificationContext(); - const { user } = useUser(); - const { mutateAsync } = useUpdateUserAuthMethods(); - - const { - reset, - setValue, - watch, - } = useForm({ - 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)); + const { createNotification } = useNotificationContext(); + const { user } = useUser(); + const { mutateAsync } = useUpdateUserAuthMethods(); - 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 - }); + const { reset, setValue, watch } = useForm({ + defaultValues: { + authMethods: user.authMethods + }, + resolver: yupResolver(schema) + }); - 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 authMethods = watch("authMethods"); + + useEffect(() => { + if (user) { + reset({ + authMethods: user.authMethods + }); } - - return ( -
-

- Authentication methods -

-

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

-
- {user && authMethodOpts.map((authMethodOpt) => { - return ( -
-
- -
- onAuthMethodToggle(value, authMethodOpt)} - isChecked={authMethods?.includes(authMethodOpt.value) ?? false} - > -

{authMethodOpt.label}

-
-
- ); - })} -
-
+ }, [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 ( +
+

+ Authentication methods +

+

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

+
+ {user && + authMethodOpts.map((authMethodOpt) => { + return ( +
+
+ +
+ onAuthMethodToggle(value, authMethodOpt)} + isChecked={authMethods?.includes(authMethodOpt.value) ?? false} + > +

{authMethodOpt.label}

+
+
+ ); + })} +
+
+ ); +};