mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Modularize SSO provider logic
This commit is contained in:
@@ -49,7 +49,7 @@ import {
|
|||||||
import { TelemetryService } from "../services";
|
import { TelemetryService } from "../services";
|
||||||
import { client, getEncryptionKey, getRootEncryptionKey } from "../config";
|
import { client, getEncryptionKey, getRootEncryptionKey } from "../config";
|
||||||
import { EEAuditLogService, EELogService, EESecretService } from "../ee/services";
|
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 { getFolderByPath, getFolderIdFromServiceToken } from "../services/FolderService";
|
||||||
import picomatch from "picomatch";
|
import picomatch from "picomatch";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
|||||||
@@ -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,
|
|
||||||
}
|
|
||||||
53
backend/src/utils/auth/authDataExtractors/index.ts
Normal file
53
backend/src/utils/auth/authDataExtractors/index.ts
Normal file
@@ -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 };
|
||||||
|
}
|
||||||
|
}
|
||||||
60
backend/src/utils/auth/passport/github.ts
Normal file
60
backend/src/utils/auth/passport/github.ts
Normal file
@@ -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);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
44
backend/src/utils/auth/passport/gitlab.ts
Normal file
44
backend/src/utils/auth/passport/gitlab.ts
Normal file
@@ -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);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
48
backend/src/utils/auth/passport/google.ts
Normal file
48
backend/src/utils/auth/passport/google.ts
Normal file
@@ -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);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
62
backend/src/utils/auth/passport/helpers.ts
Normal file
62
backend/src/utils/auth/passport/helpers.ts
Normal file
@@ -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 };
|
||||||
|
}
|
||||||
4
backend/src/utils/auth/passport/index.ts
Normal file
4
backend/src/utils/auth/passport/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export { initializeGoogleStrategy } from "./google";
|
||||||
|
export { initializeGitHubStrategy } from "./github";
|
||||||
|
export { initializeGitLabStrategy } from "./gitlab";
|
||||||
|
export { initializeSamlStrategy } from "./saml";
|
||||||
173
backend/src/utils/auth/passport/saml.ts
Normal file
173
backend/src/utils/auth/passport/saml.ts
Normal file
@@ -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);
|
||||||
|
}
|
||||||
|
));
|
||||||
|
}
|
||||||
@@ -11,7 +11,6 @@ import {
|
|||||||
backfillBots,
|
backfillBots,
|
||||||
backfillEncryptionMetadata,
|
backfillEncryptionMetadata,
|
||||||
backfillIntegration,
|
backfillIntegration,
|
||||||
backfillPermission,
|
|
||||||
backfillSecretBlindIndexData,
|
backfillSecretBlindIndexData,
|
||||||
backfillSecretFolders,
|
backfillSecretFolders,
|
||||||
backfillSecretVersions,
|
backfillSecretVersions,
|
||||||
@@ -27,7 +26,12 @@ import {
|
|||||||
reencryptSecretBlindIndexDataSalts
|
reencryptSecretBlindIndexDataSalts
|
||||||
} from "./reencryptData";
|
} from "./reencryptData";
|
||||||
import { getMongoURL, getNodeEnv, getRedisUrl, getSentryDSN } from "../../config";
|
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:
|
* Prepare Infisical upon startup. This includes tasks like:
|
||||||
@@ -41,6 +45,7 @@ import { initializePassport } from "../auth";
|
|||||||
*/
|
*/
|
||||||
export const setup = async () => {
|
export const setup = async () => {
|
||||||
if ((await getRedisUrl()) === undefined || (await getRedisUrl()) === "") {
|
if ((await getRedisUrl()) === undefined || (await getRedisUrl()) === "") {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
console.error(
|
console.error(
|
||||||
"WARNING: Redis is not yet configured. Infisical may not function as expected without it."
|
"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
|
// initializing global feature set
|
||||||
await EELicenseService.initGlobalFeatureSet();
|
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
|
// re-encrypt any data previously encrypted under server hex 128-bit ENCRYPTION_KEY
|
||||||
// to base64 256-bit ROOT_ENCRYPTION_KEY
|
// to base64 256-bit ROOT_ENCRYPTION_KEY
|
||||||
|
|||||||
Reference in New Issue
Block a user