diff --git a/backend/src/helpers/secrets.ts b/backend/src/helpers/secrets.ts index 82a98d1e9..44bbf79f1 100644 --- a/backend/src/helpers/secrets.ts +++ b/backend/src/helpers/secrets.ts @@ -49,7 +49,7 @@ import { import { TelemetryService } from "../services"; import { client, getEncryptionKey, getRootEncryptionKey } from "../config"; import { EEAuditLogService, EELogService, EESecretService } from "../ee/services"; -import { getAuthDataPayloadIdObj, getAuthDataPayloadUserObj } from "../utils/auth"; +import { getAuthDataPayloadIdObj, getAuthDataPayloadUserObj } from "../utils/auth/authDataExtractors"; import { getFolderByPath, getFolderIdFromServiceToken } from "../services/FolderService"; import picomatch from "picomatch"; import path from "path"; diff --git a/backend/src/utils/auth.ts b/backend/src/utils/auth.ts deleted file mode 100644 index c1c0e6ac5..000000000 --- a/backend/src/utils/auth.ts +++ /dev/null @@ -1,435 +0,0 @@ -import express from "express"; -import passport from "passport"; -import { Types } from "mongoose"; -import { AuthData } from "../interfaces/middleware"; -import { - AuthMethod, - MembershipOrg, - Organization, - ServiceAccount, - ServiceTokenData, - ServiceTokenDataV3, - User -} from "../models"; -import { createToken } from "../helpers/auth"; -import { - getAuthSecret, - getClientIdGitHubLogin, - getClientIdGitLabLogin, - getClientIdGoogleLogin, - getClientSecretGitHubLogin, - getClientSecretGitLabLogin, - getClientSecretGoogleLogin, - getJwtProviderAuthLifetime, - getSiteURL, - getUrlGitLabLogin -} from "../config"; -import { getSSOConfigHelper } from "../ee/helpers/organizations"; -import { InternalServerError, OrganizationNotFoundError } from "./errors"; -import { ACCEPTED, AuthTokenType, INTEGRATION_GITHUB_API_URL, INVITED, MEMBER } from "../variables"; -import { standardRequest } from "../config/request"; - -// eslint-disable-next-line @typescript-eslint/no-var-requires -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"); - -/** - * Returns an object containing the id of the authentication data payload - * @param {AuthData} authData - authentication data object - * @returns - */ -const getAuthDataPayloadIdObj = (authData: AuthData) => { - if (authData.authPayload instanceof User) { - return { userId: authData.authPayload._id }; - } - - if (authData.authPayload instanceof ServiceAccount) { - return { serviceAccountId: authData.authPayload._id }; - } - - if (authData.authPayload instanceof ServiceTokenData) { - return { serviceTokenDataId: authData.authPayload._id }; - } - - if (authData.authPayload instanceof ServiceTokenDataV3) { - return { serviceTokenDataId: authData.authPayload._id }; - } -}; - -/** - * Returns an object containing the user associated with the authentication data payload - * @param {AuthData} authData - authentication data object - * @returns - */ -const getAuthDataPayloadUserObj = (authData: AuthData) => { - if (authData.authPayload instanceof User) { - return { user: authData.authPayload._id }; - } - - if (authData.authPayload instanceof ServiceAccount) { - return { user: authData.authPayload.user }; - } - - if (authData.authPayload instanceof ServiceTokenData) { - return { user: authData.authPayload.user }; - } - - if (authData.authPayload instanceof ServiceTokenDataV3) { - return { user: authData.authPayload.user }; - } -} - -const initializePassport = async () => { - const clientIdGoogleLogin = await getClientIdGoogleLogin(); - 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({ - passReqToCallback: true, - clientID: clientIdGoogleLogin, - clientSecret: clientSecretGoogleLogin, - callbackURL: "/api/v1/sso/google", - scope: ["profile", " email"], - }, async ( - req: express.Request, - accessToken: string, - refreshToken: string, - profile: any, - done: any - ) => { - try { - const email = profile.emails[0].value; - - let user = await User.findOne({ - email - }).select("+publicKey"); - - if (!user) { - user = await new User({ - email, - authMethods: [AuthMethod.GOOGLE], - firstName: profile.name.givenName, - lastName: profile.name.familyName - }).save(); - } - - let isLinkingRequired = false; - if (!user.authMethods.includes(AuthMethod.GOOGLE)) { - isLinkingRequired = true; - } - - const isUserCompleted = !!user.publicKey; - const providerAuthToken = createToken({ - payload: { - authTokenType: AuthTokenType.PROVIDER_TOKEN, - userId: user._id.toString(), - email: user.email, - firstName: user.firstName, - lastName: user.lastName, - authMethod: AuthMethod.GOOGLE, - isUserCompleted, - isLinkingRequired, - ...(req.query.state ? { - callbackPort: req.query.state as string - } : {}) - }, - expiresIn: await getJwtProviderAuthLifetime(), - secret: await getAuthSecret(), - }); - - req.isUserCompleted = isUserCompleted; - req.providerAuthToken = providerAuthToken; - done(null, profile); - } catch (err) { - done(null, false); - } - })); - } - - if (clientIdGitHubLogin && clientSecretGitHubLogin) { - passport.use(new GitHubStrategy({ - passReqToCallback: true, - clientID: clientIdGitHubLogin, - clientSecret: clientSecretGitHubLogin, - callbackURL: "/api/v1/sso/github", - scope: ["user:email"] - }, - async (req : express.Request, accessToken : any, refreshToken : any, profile : any, done : any) => { - interface GitHubEmail { - email: string; - primary: boolean; - verified: boolean; - visibility: null | string; - } - - const { data }: { data: GitHubEmail[] } = await standardRequest.get( - `${INTEGRATION_GITHUB_API_URL}/user/emails`, - { - headers: { - Authorization: `Bearer ${accessToken}` - } - } - ); - - const primaryEmail = data.filter((gitHubEmail: GitHubEmail) => gitHubEmail.primary)[0]; - const email = primaryEmail.email; - - let user = await User.findOne({ - email - }).select("+publicKey"); - - if (!user) { - user = await new User({ - email: email, - authMethods: [AuthMethod.GITHUB], - firstName: profile.displayName, - lastName: "" - }).save(); - } - - let isLinkingRequired = false; - if (!user.authMethods.includes(AuthMethod.GITHUB)) { - isLinkingRequired = true; - } - - const isUserCompleted = !!user.publicKey; - const providerAuthToken = createToken({ - payload: { - authTokenType: AuthTokenType.PROVIDER_TOKEN, - userId: user._id.toString(), - email: user.email, - firstName: user.firstName, - lastName: user.lastName, - authMethod: AuthMethod.GITHUB, - isUserCompleted, - isLinkingRequired, - ...(req.query.state ? { - callbackPort: req.query.state as string - } : {}) - }, - expiresIn: await getJwtProviderAuthLifetime(), - secret: await getAuthSecret(), - }); - - req.isUserCompleted = isUserCompleted; - req.providerAuthToken = providerAuthToken; - return done(null, profile); - } - )); - } - - 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: { - authTokenType: AuthTokenType.PROVIDER_TOKEN, - 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 getAuthSecret(), - }); - - req.isUserCompleted = isUserCompleted; - req.providerAuthToken = providerAuthToken; - return done(null, profile); - } - )); - } - - passport.use("saml", new MultiSamlStrategy( - { - passReqToCallback: true, - getSamlOptions: async (req: any, done: any) => { - const { ssoIdentifier } = req.params; - - const ssoConfig = await getSSOConfigHelper({ - ssoConfigId: new Types.ObjectId(ssoIdentifier) - }); - - interface ISAMLConfig { - callbackUrl: string; - entryPoint: string; - issuer: string; - cert: string; - audience: string; - wantAuthnResponseSigned?: boolean; - } - - const samlConfig: ISAMLConfig = ({ - callbackUrl: `${await getSiteURL()}/api/v1/sso/saml2/${ssoIdentifier}`, - entryPoint: ssoConfig.entryPoint, - issuer: ssoConfig.issuer, - cert: ssoConfig.cert, - audience: await getSiteURL() - }); - - if (ssoConfig.authProvider.toString() === AuthMethod.JUMPCLOUD_SAML.toString()) { - samlConfig.wantAuthnResponseSigned = false; - } - - if (ssoConfig.authProvider.toString() === AuthMethod.AZURE_SAML.toString()) { - if (req.body.RelayState && JSON.parse(req.body.RelayState).spInitiated) { - samlConfig.audience = `spn:${ssoConfig.issuer}`; - } - } - - req.ssoConfig = ssoConfig; - - done(null, samlConfig); - }, - }, - async (req: any, profile: any, done: any) => { - if (!req.ssoConfig.isActive) return done(InternalServerError()); - - const organization = await Organization.findById(req.ssoConfig.organization); - - if (!organization) return done(OrganizationNotFoundError()); - - const email = profile.email; - const firstName = profile.firstName; - const lastName = profile.lastName; - - let user = await User.findOne({ - email - }).select("+publicKey"); - - if (user) { - // if user does not have SAML enabled then update - const hasSamlEnabled = user.authMethods - .some( - (authMethod: AuthMethod) => [ - AuthMethod.OKTA_SAML, - AuthMethod.AZURE_SAML, - AuthMethod.JUMPCLOUD_SAML - ].includes(authMethod) - ); - - if (!hasSamlEnabled) { - await User.findByIdAndUpdate( - user._id, - { - authMethods: [req.ssoConfig.authProvider] - }, - { - new: true - } - ); - } - - let membershipOrg = await MembershipOrg.findOne( - { - user: user._id, - organization: organization._id - } - ); - - if (!membershipOrg) { - membershipOrg = await new MembershipOrg({ - inviteEmail: email, - user: user._id, - organization: organization._id, - role: MEMBER, - status: ACCEPTED - }).save(); - } - - if (membershipOrg.status === INVITED) { - membershipOrg.status = ACCEPTED; - await membershipOrg.save(); - } - } else { - user = await new User({ - email, - authMethods: [req.ssoConfig.authProvider], - firstName, - lastName - }).save(); - - await new MembershipOrg({ - inviteEmail: email, - user: user._id, - organization: organization._id, - role: MEMBER, - status: INVITED - }).save(); - } - - const isUserCompleted = !!user.publicKey; - const providerAuthToken = createToken({ - payload: { - authTokenType: AuthTokenType.PROVIDER_TOKEN, - userId: user._id.toString(), - email: user.email, - firstName, - lastName, - organizationName: organization?.name, - authMethod: req.ssoConfig.authProvider, - isUserCompleted, - ...(req.body.RelayState ? { - callbackPort: JSON.parse(req.body.RelayState).callbackPort as string - } : {}) - }, - expiresIn: await getJwtProviderAuthLifetime(), - secret: await getAuthSecret(), - }); - - req.isUserCompleted = isUserCompleted; - req.providerAuthToken = providerAuthToken; - - done(null, profile); - } - )); -} - -export { - getAuthDataPayloadIdObj, - getAuthDataPayloadUserObj, - initializePassport, -} diff --git a/backend/src/utils/auth/authDataExtractors/index.ts b/backend/src/utils/auth/authDataExtractors/index.ts new file mode 100644 index 000000000..89c1f419e --- /dev/null +++ b/backend/src/utils/auth/authDataExtractors/index.ts @@ -0,0 +1,53 @@ +import { AuthData } from "../../../interfaces/middleware"; +import { + ServiceAccount, + ServiceTokenData, + ServiceTokenDataV3, + User +} from "../../../models"; + +/** + * Returns an object containing the id of the authentication data payload + * @param {AuthData} authData - authentication data object + * @returns + */ +export const getAuthDataPayloadIdObj = (authData: AuthData) => { + if (authData.authPayload instanceof User) { + return { userId: authData.authPayload._id }; + } + + if (authData.authPayload instanceof ServiceAccount) { + return { serviceAccountId: authData.authPayload._id }; + } + + if (authData.authPayload instanceof ServiceTokenData) { + return { serviceTokenDataId: authData.authPayload._id }; + } + + if (authData.authPayload instanceof ServiceTokenDataV3) { + return { serviceTokenDataId: authData.authPayload._id }; + } +}; + +/** + * Returns an object containing the user associated with the authentication data payload + * @param {AuthData} authData - authentication data object + * @returns + */ +export const getAuthDataPayloadUserObj = (authData: AuthData) => { + if (authData.authPayload instanceof User) { + return { user: authData.authPayload._id }; + } + + if (authData.authPayload instanceof ServiceAccount) { + return { user: authData.authPayload.user }; + } + + if (authData.authPayload instanceof ServiceTokenData) { + return { user: authData.authPayload.user }; + } + + if (authData.authPayload instanceof ServiceTokenDataV3) { + return { user: authData.authPayload.user }; + } +} \ No newline at end of file diff --git a/backend/src/utils/auth/passport/github.ts b/backend/src/utils/auth/passport/github.ts new file mode 100644 index 000000000..2f0a9b1ab --- /dev/null +++ b/backend/src/utils/auth/passport/github.ts @@ -0,0 +1,60 @@ +import express from "express"; +import passport from "passport"; +import { + getClientIdGitHubLogin, + getClientSecretGitHubLogin, +} from "../../../config"; +import { standardRequest } from "../../../config/request"; +import { AuthMethod } from "../../../models"; +import { INTEGRATION_GITHUB_API_URL } from "../../../variables"; +import { handleSSOUserTokenFlow } from "./helpers"; + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const GitHubStrategy = require("passport-github").Strategy; + +export const initializeGitHubStrategy = async () => { + const clientIdGitHubLogin = await getClientIdGitHubLogin(); + const clientSecretGitHubLogin = await getClientSecretGitHubLogin(); + if (clientIdGitHubLogin && clientSecretGitHubLogin) { + passport.use( + new GitHubStrategy({ + passReqToCallback: true, + clientID: clientIdGitHubLogin, + clientSecret: clientSecretGitHubLogin, + callbackURL: "/api/v1/sso/github", + scope: ["user:email"] + }, async (req : express.Request, accessToken : any, refreshToken : any, profile : any, done : any) => { + interface GitHubEmail { + email: string; + primary: boolean; + verified: boolean; + visibility: null | string; + } + + const { data }: { data: GitHubEmail[] } = await standardRequest.get( + `${INTEGRATION_GITHUB_API_URL}/user/emails`, + { + headers: { + Authorization: `Bearer ${accessToken}` + } + } + ); + + const primaryEmail = data.filter((gitHubEmail: GitHubEmail) => gitHubEmail.primary)[0]; + const email = primaryEmail.email; + + const { isUserCompleted, providerAuthToken } = await handleSSOUserTokenFlow({ + email, + firstName: profile.displayName, + lastName: "", + authMethod: AuthMethod.GITHUB, + callbackPort: req.query.state as string + }); + + req.isUserCompleted = isUserCompleted; + req.providerAuthToken = providerAuthToken; + return done(null, profile); + }) + ); + } +} \ No newline at end of file diff --git a/backend/src/utils/auth/passport/gitlab.ts b/backend/src/utils/auth/passport/gitlab.ts new file mode 100644 index 000000000..22851a450 --- /dev/null +++ b/backend/src/utils/auth/passport/gitlab.ts @@ -0,0 +1,44 @@ +import express from "express"; +import passport from "passport"; +import { + getClientIdGitLabLogin, + getClientSecretGitLabLogin, + getUrlGitLabLogin +} from "../../../config"; +import { AuthMethod } from "../../../models"; +import { handleSSOUserTokenFlow } from "./helpers"; + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const GitLabStrategy = require("passport-gitlab2").Strategy; + +export const initializeGitLabStrategy = async () => { + const urlGitLab = await getUrlGitLabLogin(); + const clientIdGitLabLogin = await getClientIdGitLabLogin(); + const clientSecretGitLabLogin = await getClientSecretGitLabLogin(); + + 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; + + const { isUserCompleted, providerAuthToken } = await handleSSOUserTokenFlow({ + email, + firstName: profile.displayName, + lastName: "", + authMethod: AuthMethod.GITLAB, + callbackPort: req.query.state as string + }); + + req.isUserCompleted = isUserCompleted; + req.providerAuthToken = providerAuthToken; + return done(null, profile); + }) + ); + } +} \ No newline at end of file diff --git a/backend/src/utils/auth/passport/google.ts b/backend/src/utils/auth/passport/google.ts new file mode 100644 index 000000000..126f2f9fb --- /dev/null +++ b/backend/src/utils/auth/passport/google.ts @@ -0,0 +1,48 @@ +import express from "express"; +import passport from "passport"; +import { getClientIdGoogleLogin, getClientSecretGoogleLogin } from "../../../config"; +import { AuthMethod } from "../../../models"; + +import { handleSSOUserTokenFlow } from "./helpers"; + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const GoogleStrategy = require("passport-google-oauth20").Strategy; + +export const initializeGoogleStrategy = async () => { + const clientIdGoogleLogin = await getClientIdGoogleLogin(); + const clientSecretGoogleLogin = await getClientSecretGoogleLogin(); + + if (clientIdGoogleLogin && clientSecretGoogleLogin) { + passport.use(new GoogleStrategy({ + passReqToCallback: true, + clientID: clientIdGoogleLogin, + clientSecret: clientSecretGoogleLogin, + callbackURL: "/api/v1/sso/google", + scope: ["profile", " email"], + }, async ( + req: express.Request, + accessToken: string, + refreshToken: string, + profile: any, + done: any + ) => { + try { + const email = profile.emails[0].value; + + const { isUserCompleted, providerAuthToken } = await handleSSOUserTokenFlow({ + email, + firstName: profile.name.givenName, + lastName: profile.name.familyName, + authMethod: AuthMethod.GOOGLE, + callbackPort: req.query.state as string + }); + + req.isUserCompleted = isUserCompleted; + req.providerAuthToken = providerAuthToken; + done(null, profile); + } catch (err) { + done(null, false); + } + })); + } +} \ No newline at end of file diff --git a/backend/src/utils/auth/passport/helpers.ts b/backend/src/utils/auth/passport/helpers.ts new file mode 100644 index 000000000..e8a21c37d --- /dev/null +++ b/backend/src/utils/auth/passport/helpers.ts @@ -0,0 +1,62 @@ +import { + AuthMethod, + User +} from "../../../models"; +import { createToken } from "../../../helpers/auth"; +import { AuthTokenType } from "../../../variables"; +import { getAuthSecret, getJwtProviderAuthLifetime} from "../../../config"; + +interface SSOUserTokenFlowParams { + email: string; + firstName: string; + lastName: string; + authMethod: AuthMethod; + callbackPort?: string; +} + +export const handleSSOUserTokenFlow = async ({ + email, + firstName, + lastName, + authMethod, + callbackPort +}: SSOUserTokenFlowParams) => { + let user = await User.findOne({ + email + }).select("+publicKey"); + + if (!user) { + user = await new User({ + email, + authMethods: [authMethod], + firstName, + lastName + }).save(); + } + + let isLinkingRequired = false; + if (!user.authMethods.includes(authMethod)) { + isLinkingRequired = true; + } + + const isUserCompleted = !!user.publicKey; + const providerAuthToken = createToken({ + payload: { + authTokenType: AuthTokenType.PROVIDER_TOKEN, + userId: user._id.toString(), + email: user.email, + firstName: user.firstName, + lastName: user.lastName, + authMethod, + isUserCompleted, + isLinkingRequired, + ...(callbackPort ? { + callbackPort + } : {}) + }, + expiresIn: await getJwtProviderAuthLifetime(), + secret: await getAuthSecret(), + }); + + return { isUserCompleted, providerAuthToken }; +} \ No newline at end of file diff --git a/backend/src/utils/auth/passport/index.ts b/backend/src/utils/auth/passport/index.ts new file mode 100644 index 000000000..4346c7d67 --- /dev/null +++ b/backend/src/utils/auth/passport/index.ts @@ -0,0 +1,4 @@ +export { initializeGoogleStrategy } from "./google"; +export { initializeGitHubStrategy } from "./github"; +export { initializeGitLabStrategy } from "./gitlab"; +export { initializeSamlStrategy } from "./saml"; diff --git a/backend/src/utils/auth/passport/saml.ts b/backend/src/utils/auth/passport/saml.ts new file mode 100644 index 000000000..f02e78be1 --- /dev/null +++ b/backend/src/utils/auth/passport/saml.ts @@ -0,0 +1,173 @@ +import passport from "passport"; +import { + getAuthSecret, + getJwtProviderAuthLifetime, + getSiteURL +} from "../../../config"; +import { + AuthMethod, + MembershipOrg, + Organization, + User +} from "../../../models"; +import { + createToken +} from "../../../helpers/auth"; +import { + ACCEPTED, + AuthTokenType, + INVITED, + MEMBER +} from "../../../variables"; +import { Types } from "mongoose"; +import { getSSOConfigHelper } from "../../../ee/helpers/organizations"; +import { InternalServerError, OrganizationNotFoundError } from "../../errors"; + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const { MultiSamlStrategy } = require("@node-saml/passport-saml"); + +export const initializeSamlStrategy = async () => { + passport.use("saml", new MultiSamlStrategy( + { + passReqToCallback: true, + getSamlOptions: async (req: any, done: any) => { + const { ssoIdentifier } = req.params; + + const ssoConfig = await getSSOConfigHelper({ + ssoConfigId: new Types.ObjectId(ssoIdentifier) + }); + + interface ISAMLConfig { + callbackUrl: string; + entryPoint: string; + issuer: string; + cert: string; + audience: string; + wantAuthnResponseSigned?: boolean; + } + + const samlConfig: ISAMLConfig = ({ + callbackUrl: `${await getSiteURL()}/api/v1/sso/saml2/${ssoIdentifier}`, + entryPoint: ssoConfig.entryPoint, + issuer: ssoConfig.issuer, + cert: ssoConfig.cert, + audience: await getSiteURL() + }); + + if (ssoConfig.authProvider.toString() === AuthMethod.JUMPCLOUD_SAML.toString()) { + samlConfig.wantAuthnResponseSigned = false; + } + + if (ssoConfig.authProvider.toString() === AuthMethod.AZURE_SAML.toString()) { + if (req.body.RelayState && JSON.parse(req.body.RelayState).spInitiated) { + samlConfig.audience = `spn:${ssoConfig.issuer}`; + } + } + + req.ssoConfig = ssoConfig; + + done(null, samlConfig); + }, + }, + async (req: any, profile: any, done: any) => { + if (!req.ssoConfig.isActive) return done(InternalServerError()); + + const organization = await Organization.findById(req.ssoConfig.organization); + + if (!organization) return done(OrganizationNotFoundError()); + + const email = profile.email; + const firstName = profile.firstName; + const lastName = profile.lastName; + + let user = await User.findOne({ + email + }).select("+publicKey"); + + if (user) { + // if user does not have SAML enabled then update + const hasSamlEnabled = user.authMethods + .some( + (authMethod: AuthMethod) => [ + AuthMethod.OKTA_SAML, + AuthMethod.AZURE_SAML, + AuthMethod.JUMPCLOUD_SAML + ].includes(authMethod) + ); + + if (!hasSamlEnabled) { + await User.findByIdAndUpdate( + user._id, + { + authMethods: [req.ssoConfig.authProvider] + }, + { + new: true + } + ); + } + + let membershipOrg = await MembershipOrg.findOne( + { + user: user._id, + organization: organization._id + } + ); + + if (!membershipOrg) { + membershipOrg = await new MembershipOrg({ + inviteEmail: email, + user: user._id, + organization: organization._id, + role: MEMBER, + status: ACCEPTED + }).save(); + } + + if (membershipOrg.status === INVITED) { + membershipOrg.status = ACCEPTED; + await membershipOrg.save(); + } + } else { + user = await new User({ + email, + authMethods: [req.ssoConfig.authProvider], + firstName, + lastName + }).save(); + + await new MembershipOrg({ + inviteEmail: email, + user: user._id, + organization: organization._id, + role: MEMBER, + status: INVITED + }).save(); + } + + const isUserCompleted = !!user.publicKey; + const providerAuthToken = createToken({ + payload: { + authTokenType: AuthTokenType.PROVIDER_TOKEN, + userId: user._id.toString(), + email: user.email, + firstName, + lastName, + organizationName: organization?.name, + authMethod: req.ssoConfig.authProvider, + isUserCompleted, + ...(req.body.RelayState ? { + callbackPort: JSON.parse(req.body.RelayState).callbackPort as string + } : {}) + }, + expiresIn: await getJwtProviderAuthLifetime(), + secret: await getAuthSecret(), + }); + + req.isUserCompleted = isUserCompleted; + req.providerAuthToken = providerAuthToken; + + done(null, profile); + } + )); +} \ No newline at end of file diff --git a/backend/src/utils/setup/index.ts b/backend/src/utils/setup/index.ts index 6625bff7b..b07ad8225 100644 --- a/backend/src/utils/setup/index.ts +++ b/backend/src/utils/setup/index.ts @@ -11,7 +11,6 @@ import { backfillBots, backfillEncryptionMetadata, backfillIntegration, - backfillPermission, backfillSecretBlindIndexData, backfillSecretFolders, backfillSecretVersions, @@ -27,7 +26,12 @@ import { reencryptSecretBlindIndexDataSalts } from "./reencryptData"; import { getMongoURL, getNodeEnv, getRedisUrl, getSentryDSN } from "../../config"; -import { initializePassport } from "../auth"; +import { + initializeGitHubStrategy, + initializeGitLabStrategy, + initializeGoogleStrategy, + initializeSamlStrategy +} from "../auth/passport"; /** * Prepare Infisical upon startup. This includes tasks like: @@ -41,6 +45,7 @@ import { initializePassport } from "../auth"; */ export const setup = async () => { if ((await getRedisUrl()) === undefined || (await getRedisUrl()) === "") { + // eslint-disable-next-line no-console console.error( "WARNING: Redis is not yet configured. Infisical may not function as expected without it." ); @@ -55,7 +60,11 @@ export const setup = async () => { // initializing global feature set await EELicenseService.initGlobalFeatureSet(); - await initializePassport(); + // initializing auth strategies + await initializeGoogleStrategy(); + await initializeGitHubStrategy() + await initializeGitLabStrategy(); + await initializeSamlStrategy(); // re-encrypt any data previously encrypted under server hex 128-bit ENCRYPTION_KEY // to base64 256-bit ROOT_ENCRYPTION_KEY