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 { 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";
|
||||
|
||||
@@ -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,
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user