mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-25 20:54:07 -08:00
feat(core): Add SAML login setup (#5515)
* initial commit with sample data * basic saml setup * cleanup console logs * limit saml endpoints through middleware * basic login and token issue * saml service and cleanup * refactor and create user * get/set saml prefs * fix authentication issue * redirect to user details * merge fix * add generated password to saml user * update user from attributes where possible * refactor and fix creating new user * rename saml prefs key * minor cleanup * Update packages/cli/src/config/schema.ts Co-authored-by: Omar Ajoue <krynble@gmail.com> * Update packages/cli/src/config/schema.ts Co-authored-by: Omar Ajoue <krynble@gmail.com> * Update packages/cli/src/controllers/auth.controller.ts Co-authored-by: Omar Ajoue <krynble@gmail.com> * code review changes * fix default saml enabled * remove console.log * fix isSamlLicensed --------- Co-authored-by: Omar Ajoue <krynble@gmail.com>
This commit is contained in:
parent
d09ca875ec
commit
40a934bbb4
|
@ -76,8 +76,8 @@
|
||||||
"@types/json-diff": "^0.5.1",
|
"@types/json-diff": "^0.5.1",
|
||||||
"@types/jsonwebtoken": "^9.0.1",
|
"@types/jsonwebtoken": "^9.0.1",
|
||||||
"@types/localtunnel": "^1.9.0",
|
"@types/localtunnel": "^1.9.0",
|
||||||
"@types/lodash.get": "^4.4.6",
|
|
||||||
"@types/lodash.debounce": "^4.0.7",
|
"@types/lodash.debounce": "^4.0.7",
|
||||||
|
"@types/lodash.get": "^4.4.6",
|
||||||
"@types/lodash.intersection": "^4.4.7",
|
"@types/lodash.intersection": "^4.4.7",
|
||||||
"@types/lodash.iteratee": "^4.7.7",
|
"@types/lodash.iteratee": "^4.7.7",
|
||||||
"@types/lodash.merge": "^4.6.6",
|
"@types/lodash.merge": "^4.6.6",
|
||||||
|
@ -191,6 +191,7 @@
|
||||||
"psl": "^1.8.0",
|
"psl": "^1.8.0",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"replacestream": "^4.0.3",
|
"replacestream": "^4.0.3",
|
||||||
|
"samlify": "^2.8.9",
|
||||||
"semver": "^7.3.8",
|
"semver": "^7.3.8",
|
||||||
"shelljs": "^0.8.5",
|
"shelljs": "^0.8.5",
|
||||||
"source-map-support": "^0.5.21",
|
"source-map-support": "^0.5.21",
|
||||||
|
|
|
@ -190,7 +190,7 @@ export function send<T, R extends Request, S extends Response>(
|
||||||
try {
|
try {
|
||||||
const data = await processFunction(req, res);
|
const data = await processFunction(req, res);
|
||||||
|
|
||||||
sendSuccessResponse(res, data, raw);
|
if (!res.headersSent) sendSuccessResponse(res, data, raw);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
if (!(error instanceof ResponseError) || error.httpStatusCode > 404) {
|
if (!(error instanceof ResponseError) || error.httpStatusCode > 404) {
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
import { getLicense } from '../License';
|
|
||||||
import { isUserManagementEnabled } from '../UserManagement/UserManagementHelper';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check whether the SAML feature is licensed and enabled in the instance
|
|
||||||
*/
|
|
||||||
export function isSamlEnabled(): boolean {
|
|
||||||
const license = getLicense();
|
|
||||||
return isUserManagementEnabled() && license.isSamlEnabled();
|
|
||||||
}
|
|
|
@ -142,10 +142,13 @@ import { setupBasicAuth } from './middlewares/basicAuth';
|
||||||
import { setupExternalJWTAuth } from './middlewares/externalJWTAuth';
|
import { setupExternalJWTAuth } from './middlewares/externalJWTAuth';
|
||||||
import { PostHogClient } from './posthog';
|
import { PostHogClient } from './posthog';
|
||||||
import { eventBus } from './eventbus';
|
import { eventBus } from './eventbus';
|
||||||
import { isSamlEnabled } from './Saml/helpers';
|
|
||||||
import { Container } from 'typedi';
|
import { Container } from 'typedi';
|
||||||
import { InternalHooks } from './InternalHooks';
|
import { InternalHooks } from './InternalHooks';
|
||||||
import { getStatusUsingPreviousExecutionStatusMethod } from './executions/executionHelpers';
|
import { getStatusUsingPreviousExecutionStatusMethod } from './executions/executionHelpers';
|
||||||
|
import { isSamlLicensed } from './sso/saml/samlHelpers';
|
||||||
|
import { samlControllerPublic } from './sso/saml/routes/saml.controller.public.ee';
|
||||||
|
import { SamlService } from './sso/saml/saml.service.ee';
|
||||||
|
import { samlControllerProtected } from './sso/saml/routes/saml.controller.protected.ee';
|
||||||
|
|
||||||
const exec = promisify(callbackExec);
|
const exec = promisify(callbackExec);
|
||||||
|
|
||||||
|
@ -318,7 +321,7 @@ class Server extends AbstractServer {
|
||||||
sharing: isSharingEnabled(),
|
sharing: isSharingEnabled(),
|
||||||
logStreaming: isLogStreamingEnabled(),
|
logStreaming: isLogStreamingEnabled(),
|
||||||
ldap: isLdapEnabled(),
|
ldap: isLdapEnabled(),
|
||||||
saml: isSamlEnabled(),
|
saml: isSamlLicensed(),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isLdapEnabled()) {
|
if (isLdapEnabled()) {
|
||||||
|
@ -495,6 +498,19 @@ class Server extends AbstractServer {
|
||||||
this.app.use(`/${this.restEndpoint}/ldap`, ldapController);
|
this.app.use(`/${this.restEndpoint}/ldap`, ldapController);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------
|
||||||
|
// SAML
|
||||||
|
// ----------------------------------------
|
||||||
|
|
||||||
|
// initialize SamlService
|
||||||
|
await SamlService.getInstance().init();
|
||||||
|
|
||||||
|
// public SAML endpoints
|
||||||
|
this.app.use(`/${this.restEndpoint}/sso/saml`, samlControllerPublic);
|
||||||
|
this.app.use(`/${this.restEndpoint}/sso/saml`, samlControllerProtected);
|
||||||
|
|
||||||
|
// ----------------------------------------
|
||||||
|
|
||||||
// Returns parameter values which normally get loaded from an external API or
|
// Returns parameter values which normally get loaded from an external API or
|
||||||
// get generated dynamically
|
// get generated dynamically
|
||||||
this.app.get(
|
this.app.get(
|
||||||
|
|
|
@ -237,7 +237,7 @@ export class Start extends BaseCommand {
|
||||||
// Load settings from database and set them to config.
|
// Load settings from database and set them to config.
|
||||||
const databaseSettings = await Db.collections.Settings.findBy({ loadOnStartup: true });
|
const databaseSettings = await Db.collections.Settings.findBy({ loadOnStartup: true });
|
||||||
databaseSettings.forEach((setting) => {
|
databaseSettings.forEach((setting) => {
|
||||||
config.set(setting.key, jsonParse(setting.value));
|
config.set(setting.key, jsonParse(setting.value, { fallbackValue: setting.value }));
|
||||||
});
|
});
|
||||||
|
|
||||||
config.set('nodes.packagesMissing', '');
|
config.set('nodes.packagesMissing', '');
|
||||||
|
|
|
@ -813,6 +813,11 @@ export const schema = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
authenticationMethod: {
|
||||||
|
doc: 'How to authenticate users (e.g. "email", "ldap", "saml")',
|
||||||
|
format: ['email', 'ldap', 'saml'] as const,
|
||||||
|
default: 'email',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
externalFrontendHooksUrls: {
|
externalFrontendHooksUrls: {
|
||||||
|
@ -1006,6 +1011,27 @@ export const schema = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
sso: {
|
||||||
|
justInTimeProvisioning: {
|
||||||
|
format: Boolean,
|
||||||
|
default: true,
|
||||||
|
doc: 'Whether to automatically create users when they login via SSO.',
|
||||||
|
},
|
||||||
|
redirectLoginToSso: {
|
||||||
|
format: Boolean,
|
||||||
|
default: true,
|
||||||
|
doc: 'Whether to automatically redirect users from login dialog to initialize SSO flow.',
|
||||||
|
},
|
||||||
|
saml: {
|
||||||
|
enabled: {
|
||||||
|
format: Boolean,
|
||||||
|
default: false,
|
||||||
|
doc: 'Whether to enable SAML SSO.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// TODO: move into sso settings
|
||||||
ldap: {
|
ldap: {
|
||||||
loginEnabled: {
|
loginEnabled: {
|
||||||
format: Boolean,
|
format: Boolean,
|
||||||
|
|
|
@ -19,6 +19,8 @@ import type {
|
||||||
} from '@/Interfaces';
|
} from '@/Interfaces';
|
||||||
import { handleEmailLogin, handleLdapLogin } from '@/auth';
|
import { handleEmailLogin, handleLdapLogin } from '@/auth';
|
||||||
import type { PostHogClient } from '@/posthog';
|
import type { PostHogClient } from '@/posthog';
|
||||||
|
import { isSamlCurrentAuthenticationMethod } from '../sso/ssoHelpers';
|
||||||
|
import { SamlUrls } from '../sso/saml/constants';
|
||||||
|
|
||||||
@RestController()
|
@RestController()
|
||||||
export class AuthController {
|
export class AuthController {
|
||||||
|
@ -57,14 +59,34 @@ export class AuthController {
|
||||||
* Authless endpoint.
|
* Authless endpoint.
|
||||||
*/
|
*/
|
||||||
@Post('/login')
|
@Post('/login')
|
||||||
async login(req: LoginRequest, res: Response): Promise<PublicUser> {
|
async login(req: LoginRequest, res: Response): Promise<PublicUser | undefined> {
|
||||||
const { email, password } = req.body;
|
const { email, password } = req.body;
|
||||||
if (!email) throw new Error('Email is required to log in');
|
if (!email) throw new Error('Email is required to log in');
|
||||||
if (!password) throw new Error('Password is required to log in');
|
if (!password) throw new Error('Password is required to log in');
|
||||||
|
|
||||||
const user =
|
let user: User | undefined;
|
||||||
(await handleLdapLogin(email, password)) ?? (await handleEmailLogin(email, password));
|
|
||||||
|
|
||||||
|
if (isSamlCurrentAuthenticationMethod()) {
|
||||||
|
// attempt to fetch user data with the credentials, but don't log in yet
|
||||||
|
const preliminaryUser = await handleEmailLogin(email, password);
|
||||||
|
// if the user is an owner, continue with the login
|
||||||
|
if (preliminaryUser?.globalRole?.name === 'owner') {
|
||||||
|
user = preliminaryUser;
|
||||||
|
} else {
|
||||||
|
// TODO:SAML - uncomment this block when we have a way to redirect users to the SSO flow
|
||||||
|
// if (doRedirectUsersFromLoginToSsoFlow()) {
|
||||||
|
res.redirect(SamlUrls.restInitSSO);
|
||||||
|
return;
|
||||||
|
// return withFeatureFlags(this.postHog, sanitizeUser(preliminaryUser));
|
||||||
|
// } else {
|
||||||
|
// throw new AuthError(
|
||||||
|
// 'Login with username and password is disabled due to SAML being the default authentication method. Please use SAML to log in.',
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
user = (await handleLdapLogin(email, password)) ?? (await handleEmailLogin(email, password));
|
||||||
|
}
|
||||||
if (user) {
|
if (user) {
|
||||||
await issueCookie(res, user);
|
await issueCookie(res, user);
|
||||||
return withFeatureFlags(this.postHog, sanitizeUser(user));
|
return withFeatureFlags(this.postHog, sanitizeUser(user));
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { Column, Entity, ManyToOne, PrimaryColumn, Unique } from 'typeorm';
|
||||||
import { AbstractEntity } from './AbstractEntity';
|
import { AbstractEntity } from './AbstractEntity';
|
||||||
import { User } from './User';
|
import { User } from './User';
|
||||||
|
|
||||||
export type AuthProviderType = 'ldap' | 'email'; //| 'saml' | 'google';
|
export type AuthProviderType = 'ldap' | 'email' | 'saml'; // | 'google';
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
@Unique(['providerId', 'providerType'])
|
@Unique(['providerId', 'providerType'])
|
||||||
|
|
|
@ -18,6 +18,7 @@ import {
|
||||||
} from '@/UserManagement/UserManagementHelper';
|
} from '@/UserManagement/UserManagementHelper';
|
||||||
import type { Repository } from 'typeorm';
|
import type { Repository } from 'typeorm';
|
||||||
import type { User } from '@db/entities/User';
|
import type { User } from '@db/entities/User';
|
||||||
|
import { SamlUrls } from '../sso/saml/constants';
|
||||||
|
|
||||||
const jwtFromRequest = (req: Request) => {
|
const jwtFromRequest = (req: Request) => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||||
|
@ -95,6 +96,9 @@ export const setupAuthMiddlewares = (
|
||||||
req.url.startsWith(`/${restEndpoint}/change-password`) ||
|
req.url.startsWith(`/${restEndpoint}/change-password`) ||
|
||||||
req.url.startsWith(`/${restEndpoint}/oauth2-credential/callback`) ||
|
req.url.startsWith(`/${restEndpoint}/oauth2-credential/callback`) ||
|
||||||
req.url.startsWith(`/${restEndpoint}/oauth1-credential/callback`) ||
|
req.url.startsWith(`/${restEndpoint}/oauth1-credential/callback`) ||
|
||||||
|
req.url.startsWith(`/${restEndpoint}/sso/saml${SamlUrls.metadata}`) ||
|
||||||
|
req.url.startsWith(`/${restEndpoint}/sso/saml${SamlUrls.initSSO}`) ||
|
||||||
|
req.url.startsWith(`/${restEndpoint}/sso/saml${SamlUrls.acs}`) ||
|
||||||
isAuthExcluded(req.url, ignoredEndpoints)
|
isAuthExcluded(req.url, ignoredEndpoints)
|
||||||
) {
|
) {
|
||||||
return next();
|
return next();
|
||||||
|
|
25
packages/cli/src/sso/saml/constants.ts
Normal file
25
packages/cli/src/sso/saml/constants.ts
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
export class SamlUrls {
|
||||||
|
static readonly samlRESTRoot = '/rest/sso/saml';
|
||||||
|
|
||||||
|
static readonly initSSO = '/initsso';
|
||||||
|
|
||||||
|
static readonly restInitSSO = this.samlRESTRoot + this.initSSO;
|
||||||
|
|
||||||
|
static readonly acs = '/acs';
|
||||||
|
|
||||||
|
static readonly restAcs = this.samlRESTRoot + this.acs;
|
||||||
|
|
||||||
|
static readonly metadata = '/metadata';
|
||||||
|
|
||||||
|
static readonly restMetadata = this.samlRESTRoot + this.metadata;
|
||||||
|
|
||||||
|
static readonly config = '/config';
|
||||||
|
|
||||||
|
static readonly restConfig = this.samlRESTRoot + this.config;
|
||||||
|
|
||||||
|
static readonly defaultRedirect = '/';
|
||||||
|
|
||||||
|
static readonly samlOnboarding = '/settings/personal'; // TODO:SAML: implement signup page
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SAML_PREFERENCES_DB_KEY = 'features.saml';
|
|
@ -0,0 +1,24 @@
|
||||||
|
import type { RequestHandler } from 'express';
|
||||||
|
import type { AuthenticatedRequest } from '../../../requests';
|
||||||
|
import { isSamlCurrentAuthenticationMethod } from '../../ssoHelpers';
|
||||||
|
import { isSamlEnabled, isSamlLicensed } from '../samlHelpers';
|
||||||
|
|
||||||
|
export const samlLicensedOwnerMiddleware: RequestHandler = (
|
||||||
|
req: AuthenticatedRequest,
|
||||||
|
res,
|
||||||
|
next,
|
||||||
|
) => {
|
||||||
|
if (isSamlLicensed() && req.user?.globalRole.name === 'owner') {
|
||||||
|
next();
|
||||||
|
} else {
|
||||||
|
res.status(401).json({ status: 'error', message: 'Unauthorized' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const samlLicensedAndEnabledMiddleware: RequestHandler = (req, res, next) => {
|
||||||
|
if (isSamlEnabled() && isSamlLicensed() && isSamlCurrentAuthenticationMethod()) {
|
||||||
|
next();
|
||||||
|
} else {
|
||||||
|
res.status(401).json({ status: 'error', message: 'Unauthorized' });
|
||||||
|
}
|
||||||
|
};
|
105
packages/cli/src/sso/saml/routes/saml.controller.protected.ee.ts
Normal file
105
packages/cli/src/sso/saml/routes/saml.controller.protected.ee.ts
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
import express from 'express';
|
||||||
|
import {
|
||||||
|
samlLicensedAndEnabledMiddleware,
|
||||||
|
samlLicensedOwnerMiddleware,
|
||||||
|
} from '../middleware/samlEnabledMiddleware';
|
||||||
|
import { SamlService } from '../saml.service.ee';
|
||||||
|
import { SamlUrls } from '../constants';
|
||||||
|
import type { SamlConfiguration } from '../types/requests';
|
||||||
|
import { AuthError } from '../../../ResponseHelper';
|
||||||
|
import { issueCookie } from '../../../auth/jwt';
|
||||||
|
|
||||||
|
export const samlControllerProtected = express.Router();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /sso/saml/config
|
||||||
|
* Return SAML config
|
||||||
|
*/
|
||||||
|
samlControllerProtected.get(
|
||||||
|
SamlUrls.config,
|
||||||
|
samlLicensedOwnerMiddleware,
|
||||||
|
async (req: SamlConfiguration.Read, res: express.Response) => {
|
||||||
|
const prefs = await SamlService.getInstance().getSamlPreferences();
|
||||||
|
return res.send(prefs);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /sso/saml/config
|
||||||
|
* Return SAML config
|
||||||
|
*/
|
||||||
|
samlControllerProtected.post(
|
||||||
|
SamlUrls.config,
|
||||||
|
samlLicensedOwnerMiddleware,
|
||||||
|
async (req: SamlConfiguration.Update, res: express.Response) => {
|
||||||
|
const result = await SamlService.getInstance().setSamlPreferences({
|
||||||
|
metadata: req.body.metadata,
|
||||||
|
mapping: req.body.mapping,
|
||||||
|
});
|
||||||
|
return res.send(result);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /sso/saml/acs
|
||||||
|
* Assertion Consumer Service endpoint
|
||||||
|
*/
|
||||||
|
samlControllerProtected.get(
|
||||||
|
SamlUrls.acs,
|
||||||
|
samlLicensedAndEnabledMiddleware,
|
||||||
|
async (req: express.Request, res: express.Response) => {
|
||||||
|
const loginResult = await SamlService.getInstance().handleSamlLogin(req, 'redirect');
|
||||||
|
if (loginResult) {
|
||||||
|
if (loginResult.authenticatedUser) {
|
||||||
|
await issueCookie(res, loginResult.authenticatedUser);
|
||||||
|
if (loginResult.onboardingRequired) {
|
||||||
|
return res.redirect(SamlUrls.samlOnboarding);
|
||||||
|
} else {
|
||||||
|
return res.redirect(SamlUrls.defaultRedirect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new AuthError('SAML Authentication failed');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /sso/saml/acs
|
||||||
|
* Assertion Consumer Service endpoint
|
||||||
|
*/
|
||||||
|
samlControllerProtected.post(
|
||||||
|
SamlUrls.acs,
|
||||||
|
samlLicensedAndEnabledMiddleware,
|
||||||
|
async (req: express.Request, res: express.Response) => {
|
||||||
|
const loginResult = await SamlService.getInstance().handleSamlLogin(req, 'post');
|
||||||
|
if (loginResult) {
|
||||||
|
if (loginResult.authenticatedUser) {
|
||||||
|
await issueCookie(res, loginResult.authenticatedUser);
|
||||||
|
if (loginResult.onboardingRequired) {
|
||||||
|
return res.redirect(SamlUrls.samlOnboarding);
|
||||||
|
} else {
|
||||||
|
return res.redirect(SamlUrls.defaultRedirect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new AuthError('SAML Authentication failed');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /sso/saml/initsso
|
||||||
|
* Access URL for implementing SP-init SSO
|
||||||
|
*/
|
||||||
|
samlControllerProtected.get(
|
||||||
|
SamlUrls.initSSO,
|
||||||
|
samlLicensedAndEnabledMiddleware,
|
||||||
|
async (req: express.Request, res: express.Response) => {
|
||||||
|
const url = SamlService.getInstance().getRedirectLoginRequestUrl();
|
||||||
|
if (url) {
|
||||||
|
// TODO:SAML: redirect to the URL on the client side
|
||||||
|
return res.status(301).send(url);
|
||||||
|
} else {
|
||||||
|
throw new AuthError('SAML redirect failed, please check your SAML configuration.');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
|
@ -0,0 +1,17 @@
|
||||||
|
import express from 'express';
|
||||||
|
import { SamlUrls } from '../constants';
|
||||||
|
import { getServiceProviderInstance } from '../serviceProvider.ee';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SSO Endpoints that are public
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const samlControllerPublic = express.Router();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /sso/saml/metadata
|
||||||
|
* Return Service Provider metadata
|
||||||
|
*/
|
||||||
|
samlControllerPublic.get(SamlUrls.metadata, async (req: express.Request, res: express.Response) => {
|
||||||
|
return res.header('Content-Type', 'text/xml').send(getServiceProviderInstance().getMetadata());
|
||||||
|
});
|
228
packages/cli/src/sso/saml/saml.service.ee.ts
Normal file
228
packages/cli/src/sso/saml/saml.service.ee.ts
Normal file
|
@ -0,0 +1,228 @@
|
||||||
|
import type express from 'express';
|
||||||
|
import * as Db from '@/Db';
|
||||||
|
import type { User } from '@/databases/entities/User';
|
||||||
|
import { jsonParse, LoggerProxy } from 'n8n-workflow';
|
||||||
|
import { AuthError } from '@/ResponseHelper';
|
||||||
|
import { getServiceProviderInstance } from './serviceProvider.ee';
|
||||||
|
import type { SamlUserAttributes } from './types/samlUserAttributes';
|
||||||
|
import type { SamlAttributeMapping } from './types/samlAttributeMapping';
|
||||||
|
import { isSsoJustInTimeProvisioningEnabled } from '../ssoHelpers';
|
||||||
|
import type { SamlPreferences } from './types/samlPreferences';
|
||||||
|
import { SAML_PREFERENCES_DB_KEY } from './constants';
|
||||||
|
import type { IdentityProviderInstance } from 'samlify';
|
||||||
|
import { IdentityProvider } from 'samlify';
|
||||||
|
import {
|
||||||
|
createUserFromSamlAttributes,
|
||||||
|
getMappedSamlAttributesFromFlowResult,
|
||||||
|
updateUserFromSamlAttributes,
|
||||||
|
} from './samlHelpers';
|
||||||
|
|
||||||
|
export class SamlService {
|
||||||
|
private static instance: SamlService;
|
||||||
|
|
||||||
|
private identityProviderInstance: IdentityProviderInstance | undefined;
|
||||||
|
|
||||||
|
private _attributeMapping: SamlAttributeMapping = {
|
||||||
|
email: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress',
|
||||||
|
firstName: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/firstname',
|
||||||
|
lastName: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/lastname',
|
||||||
|
userPrincipalName: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn',
|
||||||
|
};
|
||||||
|
|
||||||
|
public get attributeMapping(): SamlAttributeMapping {
|
||||||
|
return this._attributeMapping;
|
||||||
|
}
|
||||||
|
|
||||||
|
public set attributeMapping(mapping: SamlAttributeMapping) {
|
||||||
|
// TODO:SAML: add validation
|
||||||
|
this._attributeMapping = mapping;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _metadata = '';
|
||||||
|
|
||||||
|
public get metadata(): string {
|
||||||
|
return this._metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
public set metadata(metadata: string) {
|
||||||
|
this._metadata = metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.loadSamlPreferences()
|
||||||
|
.then(() => {
|
||||||
|
LoggerProxy.debug('Initializing SAML service');
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
LoggerProxy.error('Error initializing SAML service');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static getInstance(): SamlService {
|
||||||
|
if (!SamlService.instance) {
|
||||||
|
SamlService.instance = new SamlService();
|
||||||
|
}
|
||||||
|
return SamlService.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
async init(): Promise<void> {
|
||||||
|
await this.loadSamlPreferences();
|
||||||
|
}
|
||||||
|
|
||||||
|
getIdentityProviderInstance(forceRecreate = false): IdentityProviderInstance {
|
||||||
|
if (this.identityProviderInstance === undefined || forceRecreate) {
|
||||||
|
this.identityProviderInstance = IdentityProvider({
|
||||||
|
metadata: this.metadata,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.identityProviderInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
getRedirectLoginRequestUrl(): string {
|
||||||
|
const loginRequest = getServiceProviderInstance().createLoginRequest(
|
||||||
|
this.getIdentityProviderInstance(),
|
||||||
|
'redirect',
|
||||||
|
);
|
||||||
|
//TODO:SAML: debug logging
|
||||||
|
LoggerProxy.debug(loginRequest.context);
|
||||||
|
return loginRequest.context;
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleSamlLogin(
|
||||||
|
req: express.Request,
|
||||||
|
binding: 'post' | 'redirect',
|
||||||
|
): Promise<
|
||||||
|
| {
|
||||||
|
authenticatedUser: User | undefined;
|
||||||
|
attributes: SamlUserAttributes;
|
||||||
|
onboardingRequired: boolean;
|
||||||
|
}
|
||||||
|
| undefined
|
||||||
|
> {
|
||||||
|
const attributes = await this.getAttributesFromLoginResponse(req, binding);
|
||||||
|
if (attributes.email) {
|
||||||
|
const user = await Db.collections.User.findOne({
|
||||||
|
where: { email: attributes.email },
|
||||||
|
relations: ['globalRole', 'authIdentities'],
|
||||||
|
});
|
||||||
|
if (user) {
|
||||||
|
// Login path for existing users that are fully set up
|
||||||
|
if (
|
||||||
|
user.authIdentities.find(
|
||||||
|
(e) => e.providerType === 'saml' && e.providerId === attributes.userPrincipalName,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
authenticatedUser: user,
|
||||||
|
attributes,
|
||||||
|
onboardingRequired: false,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// Login path for existing users that are NOT fully set up for SAML
|
||||||
|
const updatedUser = await updateUserFromSamlAttributes(user, attributes);
|
||||||
|
return {
|
||||||
|
authenticatedUser: updatedUser,
|
||||||
|
attributes,
|
||||||
|
onboardingRequired: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// New users to be created JIT based on SAML attributes
|
||||||
|
if (isSsoJustInTimeProvisioningEnabled()) {
|
||||||
|
const newUser = await createUserFromSamlAttributes(attributes);
|
||||||
|
return {
|
||||||
|
authenticatedUser: newUser,
|
||||||
|
attributes,
|
||||||
|
onboardingRequired: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSamlPreferences(): Promise<SamlPreferences> {
|
||||||
|
return {
|
||||||
|
mapping: this.attributeMapping,
|
||||||
|
metadata: this.metadata,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async setSamlPreferences(prefs: SamlPreferences): Promise<void> {
|
||||||
|
this.attributeMapping = prefs.mapping;
|
||||||
|
this.metadata = prefs.metadata;
|
||||||
|
this.getIdentityProviderInstance(true);
|
||||||
|
await this.saveSamlPreferences();
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadSamlPreferences(): Promise<SamlPreferences | undefined> {
|
||||||
|
const samlPreferences = await Db.collections.Settings.findOne({
|
||||||
|
where: { key: SAML_PREFERENCES_DB_KEY },
|
||||||
|
});
|
||||||
|
if (samlPreferences) {
|
||||||
|
const prefs = jsonParse<SamlPreferences>(samlPreferences.value);
|
||||||
|
if (prefs) {
|
||||||
|
this.attributeMapping = prefs.mapping;
|
||||||
|
this.metadata = prefs.metadata;
|
||||||
|
}
|
||||||
|
return prefs;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveSamlPreferences(): Promise<void> {
|
||||||
|
const samlPreferences = await Db.collections.Settings.findOne({
|
||||||
|
where: { key: SAML_PREFERENCES_DB_KEY },
|
||||||
|
});
|
||||||
|
if (samlPreferences) {
|
||||||
|
samlPreferences.value = JSON.stringify({
|
||||||
|
mapping: this.attributeMapping,
|
||||||
|
metadata: this.metadata,
|
||||||
|
});
|
||||||
|
samlPreferences.loadOnStartup = true;
|
||||||
|
await Db.collections.Settings.save(samlPreferences);
|
||||||
|
} else {
|
||||||
|
await Db.collections.Settings.save({
|
||||||
|
key: SAML_PREFERENCES_DB_KEY,
|
||||||
|
value: JSON.stringify({
|
||||||
|
mapping: this.attributeMapping,
|
||||||
|
metadata: this.metadata,
|
||||||
|
}),
|
||||||
|
loadOnStartup: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAttributesFromLoginResponse(
|
||||||
|
req: express.Request,
|
||||||
|
binding: 'post' | 'redirect',
|
||||||
|
): Promise<SamlUserAttributes> {
|
||||||
|
let parsedSamlResponse;
|
||||||
|
try {
|
||||||
|
parsedSamlResponse = await getServiceProviderInstance().parseLoginResponse(
|
||||||
|
this.getIdentityProviderInstance(),
|
||||||
|
binding,
|
||||||
|
req,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
// throw new AuthError('SAML Authentication failed. Could not parse SAML response.');
|
||||||
|
}
|
||||||
|
const { attributes, missingAttributes } = getMappedSamlAttributesFromFlowResult(
|
||||||
|
parsedSamlResponse,
|
||||||
|
this.attributeMapping,
|
||||||
|
);
|
||||||
|
if (!attributes) {
|
||||||
|
throw new AuthError('SAML Authentication failed. Invalid SAML response.');
|
||||||
|
}
|
||||||
|
if (!attributes.email && missingAttributes.length > 0) {
|
||||||
|
throw new AuthError(
|
||||||
|
`SAML Authentication failed. Invalid SAML response (missing attributes: ${missingAttributes.join(
|
||||||
|
', ',
|
||||||
|
)}).`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return attributes;
|
||||||
|
}
|
||||||
|
}
|
136
packages/cli/src/sso/saml/samlHelpers.ts
Normal file
136
packages/cli/src/sso/saml/samlHelpers.ts
Normal file
|
@ -0,0 +1,136 @@
|
||||||
|
import config from '@/config';
|
||||||
|
import * as Db from '@/Db';
|
||||||
|
import { AuthIdentity } from '../../databases/entities/AuthIdentity';
|
||||||
|
import { User } from '../../databases/entities/User';
|
||||||
|
import { getLicense } from '../../License';
|
||||||
|
import { AuthError } from '../../ResponseHelper';
|
||||||
|
import { hashPassword, isUserManagementEnabled } from '../../UserManagement/UserManagementHelper';
|
||||||
|
import type { SamlPreferences } from './types/samlPreferences';
|
||||||
|
import type { SamlUserAttributes } from './types/samlUserAttributes';
|
||||||
|
import type { FlowResult } from 'samlify/types/src/flow';
|
||||||
|
import type { SamlAttributeMapping } from './types/samlAttributeMapping';
|
||||||
|
/**
|
||||||
|
* Check whether the SAML feature is licensed and enabled in the instance
|
||||||
|
*/
|
||||||
|
export function isSamlEnabled(): boolean {
|
||||||
|
return config.getEnv('sso.saml.enabled');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isSamlLicensed(): boolean {
|
||||||
|
const license = getLicense();
|
||||||
|
return (
|
||||||
|
isUserManagementEnabled() &&
|
||||||
|
(license.isSamlEnabled() || config.getEnv('enterprise.features.saml'))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isSamlPreferences = (candidate: unknown): candidate is SamlPreferences => {
|
||||||
|
const o = candidate as SamlPreferences;
|
||||||
|
return typeof o === 'object' && typeof o.metadata === 'string' && typeof o.mapping === 'object';
|
||||||
|
};
|
||||||
|
|
||||||
|
export function generatePassword(): string {
|
||||||
|
const length = 18;
|
||||||
|
const charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||||
|
const charsetNoNumbers = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||||
|
const randomNumber = Math.floor(Math.random() * 10);
|
||||||
|
const randomUpper = charset.charAt(Math.floor(Math.random() * charsetNoNumbers.length));
|
||||||
|
const randomNumberPosition = Math.floor(Math.random() * length);
|
||||||
|
const randomUpperPosition = Math.floor(Math.random() * length);
|
||||||
|
let password = '';
|
||||||
|
for (let i = 0, n = charset.length; i < length; ++i) {
|
||||||
|
password += charset.charAt(Math.floor(Math.random() * n));
|
||||||
|
}
|
||||||
|
password =
|
||||||
|
password.substring(0, randomNumberPosition) +
|
||||||
|
randomNumber.toString() +
|
||||||
|
password.substring(randomNumberPosition);
|
||||||
|
password =
|
||||||
|
password.substring(0, randomUpperPosition) +
|
||||||
|
randomUpper +
|
||||||
|
password.substring(randomUpperPosition);
|
||||||
|
return password;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createUserFromSamlAttributes(attributes: SamlUserAttributes): Promise<User> {
|
||||||
|
const user = new User();
|
||||||
|
const authIdentity = new AuthIdentity();
|
||||||
|
user.email = attributes.email;
|
||||||
|
user.firstName = attributes.firstName;
|
||||||
|
user.lastName = attributes.lastName;
|
||||||
|
user.globalRole = await Db.collections.Role.findOneOrFail({
|
||||||
|
where: { name: 'member', scope: 'global' },
|
||||||
|
});
|
||||||
|
// generates a password that is not used or known to the user
|
||||||
|
user.password = await hashPassword(generatePassword());
|
||||||
|
authIdentity.providerId = attributes.userPrincipalName;
|
||||||
|
authIdentity.providerType = 'saml';
|
||||||
|
authIdentity.user = user;
|
||||||
|
const resultAuthIdentity = await Db.collections.AuthIdentity.save(authIdentity);
|
||||||
|
if (!resultAuthIdentity) throw new AuthError('Could not create AuthIdentity');
|
||||||
|
user.authIdentities = [authIdentity];
|
||||||
|
const resultUser = await Db.collections.User.save(user);
|
||||||
|
if (!resultUser) throw new AuthError('Could not create User');
|
||||||
|
return resultUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateUserFromSamlAttributes(
|
||||||
|
user: User,
|
||||||
|
attributes: SamlUserAttributes,
|
||||||
|
): Promise<User> {
|
||||||
|
if (!attributes.email) throw new AuthError('Email is required to update user');
|
||||||
|
if (!user) throw new AuthError('User not found');
|
||||||
|
let samlAuthIdentity = user?.authIdentities.find((e) => e.providerType === 'saml');
|
||||||
|
if (!samlAuthIdentity) {
|
||||||
|
samlAuthIdentity = new AuthIdentity();
|
||||||
|
samlAuthIdentity.providerId = attributes.userPrincipalName;
|
||||||
|
samlAuthIdentity.providerType = 'saml';
|
||||||
|
samlAuthIdentity.user = user;
|
||||||
|
user.authIdentities.push(samlAuthIdentity);
|
||||||
|
} else {
|
||||||
|
samlAuthIdentity.providerId = attributes.userPrincipalName;
|
||||||
|
}
|
||||||
|
await Db.collections.AuthIdentity.save(samlAuthIdentity);
|
||||||
|
user.firstName = attributes.firstName;
|
||||||
|
user.lastName = attributes.lastName;
|
||||||
|
const resultUser = await Db.collections.User.save(user);
|
||||||
|
if (!resultUser) throw new AuthError('Could not create User');
|
||||||
|
return resultUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetMappedSamlReturn = {
|
||||||
|
attributes: SamlUserAttributes | undefined;
|
||||||
|
missingAttributes: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getMappedSamlAttributesFromFlowResult(
|
||||||
|
flowResult: FlowResult,
|
||||||
|
attributeMapping: SamlAttributeMapping,
|
||||||
|
): GetMappedSamlReturn {
|
||||||
|
const result: GetMappedSamlReturn = {
|
||||||
|
attributes: undefined,
|
||||||
|
missingAttributes: [] as string[],
|
||||||
|
};
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||||
|
if (flowResult?.extract?.attributes) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||||
|
const attributes = flowResult.extract.attributes as { [key: string]: string };
|
||||||
|
// TODO:SAML: fetch mapped attributes from flowResult.extract.attributes and create or login user
|
||||||
|
const email = attributes[attributeMapping.email];
|
||||||
|
const firstName = attributes[attributeMapping.firstName];
|
||||||
|
const lastName = attributes[attributeMapping.lastName];
|
||||||
|
const userPrincipalName = attributes[attributeMapping.userPrincipalName];
|
||||||
|
|
||||||
|
result.attributes = {
|
||||||
|
email,
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
userPrincipalName,
|
||||||
|
};
|
||||||
|
if (!email) result.missingAttributes.push(attributeMapping.email);
|
||||||
|
if (!userPrincipalName) result.missingAttributes.push(attributeMapping.userPrincipalName);
|
||||||
|
if (!firstName) result.missingAttributes.push(attributeMapping.firstName);
|
||||||
|
if (!lastName) result.missingAttributes.push(attributeMapping.lastName);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
39
packages/cli/src/sso/saml/serviceProvider.ee.ts
Normal file
39
packages/cli/src/sso/saml/serviceProvider.ee.ts
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import { getInstanceBaseUrl } from '@/UserManagement/UserManagementHelper';
|
||||||
|
import type { ServiceProviderInstance } from 'samlify';
|
||||||
|
import { ServiceProvider, setSchemaValidator } from 'samlify';
|
||||||
|
import { SamlUrls } from './constants';
|
||||||
|
|
||||||
|
let serviceProviderInstance: ServiceProviderInstance | undefined;
|
||||||
|
|
||||||
|
setSchemaValidator({
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
validate: async (response: string) => {
|
||||||
|
// TODO:SAML: implment validation
|
||||||
|
return Promise.resolve('skipped');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const metadata = `
|
||||||
|
<EntityDescriptor
|
||||||
|
xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata"
|
||||||
|
xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
|
||||||
|
xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
|
||||||
|
entityID="${getInstanceBaseUrl() + SamlUrls.restMetadata}">
|
||||||
|
<SPSSODescriptor WantAssertionsSigned="true" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
|
||||||
|
<NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</NameIDFormat>
|
||||||
|
<AssertionConsumerService isDefault="true" index="0" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="${
|
||||||
|
getInstanceBaseUrl() + SamlUrls.restAcs
|
||||||
|
}"/>
|
||||||
|
</SPSSODescriptor>
|
||||||
|
</EntityDescriptor>
|
||||||
|
`;
|
||||||
|
|
||||||
|
export function getServiceProviderInstance(): ServiceProviderInstance {
|
||||||
|
if (serviceProviderInstance === undefined) {
|
||||||
|
serviceProviderInstance = ServiceProvider({
|
||||||
|
metadata,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return serviceProviderInstance;
|
||||||
|
}
|
7
packages/cli/src/sso/saml/types/requests.ts
Normal file
7
packages/cli/src/sso/saml/types/requests.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import type { AuthenticatedRequest } from '../../../requests';
|
||||||
|
import type { SamlPreferences } from './samlPreferences';
|
||||||
|
|
||||||
|
export declare namespace SamlConfiguration {
|
||||||
|
type Read = AuthenticatedRequest<{}, {}, {}, {}>;
|
||||||
|
type Update = AuthenticatedRequest<{}, {}, SamlPreferences, {}>;
|
||||||
|
}
|
6
packages/cli/src/sso/saml/types/samlAttributeMapping.ts
Normal file
6
packages/cli/src/sso/saml/types/samlAttributeMapping.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
export interface SamlAttributeMapping {
|
||||||
|
email: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
userPrincipalName: string;
|
||||||
|
}
|
7
packages/cli/src/sso/saml/types/samlPreferences.ts
Normal file
7
packages/cli/src/sso/saml/types/samlPreferences.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import type { SamlAttributeMapping } from './samlAttributeMapping';
|
||||||
|
|
||||||
|
export interface SamlPreferences {
|
||||||
|
mapping: SamlAttributeMapping;
|
||||||
|
metadata: string;
|
||||||
|
//TODO:SAML: add fields for separate SAML settins to generate metadata from
|
||||||
|
}
|
6
packages/cli/src/sso/saml/types/samlUserAttributes.ts
Normal file
6
packages/cli/src/sso/saml/types/samlUserAttributes.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
export interface SamlUserAttributes {
|
||||||
|
email: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
userPrincipalName: string;
|
||||||
|
}
|
13
packages/cli/src/sso/ssoHelpers.ts
Normal file
13
packages/cli/src/sso/ssoHelpers.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import config from '@/config';
|
||||||
|
|
||||||
|
export function isSamlCurrentAuthenticationMethod(): boolean {
|
||||||
|
return config.getEnv('userManagement.authenticationMethod') === 'saml';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isSsoJustInTimeProvisioningEnabled(): boolean {
|
||||||
|
return config.getEnv('sso.justInTimeProvisioning');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function doRedirectUsersFromLoginToSsoFlow(): boolean {
|
||||||
|
return config.getEnv('sso.redirectLoginToSso');
|
||||||
|
}
|
1
packages/workflow/src/Authentication.ts
Normal file
1
packages/workflow/src/Authentication.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export type AuthenticationMethod = 'none' | 'email' | 'ldap' | 'saml';
|
|
@ -4,6 +4,7 @@ import * as NodeHelpers from './NodeHelpers';
|
||||||
import * as ObservableObject from './ObservableObject';
|
import * as ObservableObject from './ObservableObject';
|
||||||
import * as TelemetryHelpers from './TelemetryHelpers';
|
import * as TelemetryHelpers from './TelemetryHelpers';
|
||||||
|
|
||||||
|
export * from './Authentication';
|
||||||
export * from './Cron';
|
export * from './Cron';
|
||||||
export * from './DeferredPromise';
|
export * from './DeferredPromise';
|
||||||
export * from './Interfaces';
|
export * from './Interfaces';
|
||||||
|
|
|
@ -233,6 +233,7 @@ importers:
|
||||||
reflect-metadata: ^0.1.13
|
reflect-metadata: ^0.1.13
|
||||||
replacestream: ^4.0.3
|
replacestream: ^4.0.3
|
||||||
run-script-os: ^1.0.7
|
run-script-os: ^1.0.7
|
||||||
|
samlify: ^2.8.9
|
||||||
semver: ^7.3.8
|
semver: ^7.3.8
|
||||||
shelljs: ^0.8.5
|
shelljs: ^0.8.5
|
||||||
source-map-support: ^0.5.21
|
source-map-support: ^0.5.21
|
||||||
|
@ -328,6 +329,7 @@ importers:
|
||||||
psl: 1.9.0
|
psl: 1.9.0
|
||||||
reflect-metadata: 0.1.13
|
reflect-metadata: 0.1.13
|
||||||
replacestream: 4.0.3
|
replacestream: 4.0.3
|
||||||
|
samlify: 2.8.9
|
||||||
semver: 7.3.8
|
semver: 7.3.8
|
||||||
shelljs: 0.8.5
|
shelljs: 0.8.5
|
||||||
source-map-support: 0.5.21
|
source-map-support: 0.5.21
|
||||||
|
@ -1215,6 +1217,15 @@ packages:
|
||||||
z-schema: 4.2.4
|
z-schema: 4.2.4
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@authenio/xml-encryption/2.0.2:
|
||||||
|
resolution: {integrity: sha512-cTlrKttbrRHEw3W+0/I609A2Matj5JQaRvfLtEIGZvlN0RaPi+3ANsMeqAyCAVlH/lUIW2tmtBlSMni74lcXeg==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
dependencies:
|
||||||
|
'@xmldom/xmldom': 0.8.6
|
||||||
|
escape-html: 1.0.3
|
||||||
|
xpath: 0.0.32
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@aw-web-design/x-default-browser/1.4.88:
|
/@aw-web-design/x-default-browser/1.4.88:
|
||||||
resolution: {integrity: sha512-AkEmF0wcwYC2QkhK703Y83fxWARttIWXDmQN8+cof8FmFZ5BRhnNXGymeb1S73bOCLfWjYELxtujL56idCN/XA==}
|
resolution: {integrity: sha512-AkEmF0wcwYC2QkhK703Y83fxWARttIWXDmQN8+cof8FmFZ5BRhnNXGymeb1S73bOCLfWjYELxtujL56idCN/XA==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
@ -6886,6 +6897,11 @@ packages:
|
||||||
'@xtuc/long': 4.2.2
|
'@xtuc/long': 4.2.2
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@xmldom/xmldom/0.8.6:
|
||||||
|
resolution: {integrity: sha512-uRjjusqpoqfmRkTaNuLJ2VohVr67Q5YwDATW3VU7PfzTj6IRaihGrYI7zckGZjxQPBIp63nfvJbM+Yu5ICh0Bg==}
|
||||||
|
engines: {node: '>=10.0.0'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@xtuc/ieee754/1.2.0:
|
/@xtuc/ieee754/1.2.0:
|
||||||
resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==}
|
resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==}
|
||||||
dev: true
|
dev: true
|
||||||
|
@ -8242,7 +8258,6 @@ packages:
|
||||||
/camelcase/6.3.0:
|
/camelcase/6.3.0:
|
||||||
resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==}
|
resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/caniuse-lite/1.0.30001445:
|
/caniuse-lite/1.0.30001445:
|
||||||
resolution: {integrity: sha512-8sdQIdMztYmzfTMO6KfLny878Ln9c2M0fc7EH60IjlP4Dc4PiCy7K2Vl3ITmWgOyPgVQKa5x+UP/KqFsxj4mBg==}
|
resolution: {integrity: sha512-8sdQIdMztYmzfTMO6KfLny878Ln9c2M0fc7EH60IjlP4Dc4PiCy7K2Vl3ITmWgOyPgVQKa5x+UP/KqFsxj4mBg==}
|
||||||
|
@ -15539,7 +15554,6 @@ packages:
|
||||||
/node-forge/1.3.1:
|
/node-forge/1.3.1:
|
||||||
resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==}
|
resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==}
|
||||||
engines: {node: '>= 6.13.0'}
|
engines: {node: '>= 6.13.0'}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/node-gyp-build-optional-packages/5.0.3:
|
/node-gyp-build-optional-packages/5.0.3:
|
||||||
resolution: {integrity: sha512-k75jcVzk5wnnc/FMxsf4udAoTEUv2jY3ycfdSd3yWu6Cnd1oee6/CfZJApyscA4FJOmdoixWwiwOyf16RzD5JA==}
|
resolution: {integrity: sha512-k75jcVzk5wnnc/FMxsf4udAoTEUv2jY3ycfdSd3yWu6Cnd1oee6/CfZJApyscA4FJOmdoixWwiwOyf16RzD5JA==}
|
||||||
|
@ -16132,6 +16146,10 @@ packages:
|
||||||
resolution: {integrity: sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==}
|
resolution: {integrity: sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/pako/1.0.11:
|
||||||
|
resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/param-case/3.0.4:
|
/param-case/3.0.4:
|
||||||
resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==}
|
resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -17947,6 +17965,21 @@ packages:
|
||||||
/safer-buffer/2.1.2:
|
/safer-buffer/2.1.2:
|
||||||
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
||||||
|
|
||||||
|
/samlify/2.8.9:
|
||||||
|
resolution: {integrity: sha512-+HHxkBweHwWEEiFWelGhTTX2Zv/7Tjh6xbZPYUURe7JWp1N9cO2jUOiSb13gTzCEXtffye+Ld7M/f2gCU5+B2Q==}
|
||||||
|
dependencies:
|
||||||
|
'@authenio/xml-encryption': 2.0.2
|
||||||
|
'@xmldom/xmldom': 0.8.6
|
||||||
|
camelcase: 6.3.0
|
||||||
|
node-forge: 1.3.1
|
||||||
|
node-rsa: 1.1.1
|
||||||
|
pako: 1.0.11
|
||||||
|
uuid: 8.3.2
|
||||||
|
xml: 1.0.1
|
||||||
|
xml-crypto: 3.0.1
|
||||||
|
xpath: 0.0.32
|
||||||
|
dev: false
|
||||||
|
|
||||||
/sanitize-html/2.9.0:
|
/sanitize-html/2.9.0:
|
||||||
resolution: {integrity: sha512-KY1hpSbqFNcpoLf+nP7iStbP5JfQZ2Nd19ZEE7qFsQqRdp+sO5yX/e5+HoG9puFAcSTEpzQuihfKUltDcLlQjg==}
|
resolution: {integrity: sha512-KY1hpSbqFNcpoLf+nP7iStbP5JfQZ2Nd19ZEE7qFsQqRdp+sO5yX/e5+HoG9puFAcSTEpzQuihfKUltDcLlQjg==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -21568,11 +21601,23 @@ packages:
|
||||||
word: 0.3.0
|
word: 0.3.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/xml-crypto/3.0.1:
|
||||||
|
resolution: {integrity: sha512-7XrwB3ujd95KCO6+u9fidb8ajvRJvIfGNWD0XLJoTWlBKz+tFpUzEYxsN+Il/6/gHtEs1RgRh2RH+TzhcWBZUw==}
|
||||||
|
engines: {node: '>=0.4.0'}
|
||||||
|
dependencies:
|
||||||
|
'@xmldom/xmldom': 0.8.6
|
||||||
|
xpath: 0.0.32
|
||||||
|
dev: false
|
||||||
|
|
||||||
/xml-name-validator/4.0.0:
|
/xml-name-validator/4.0.0:
|
||||||
resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==}
|
resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/xml/1.0.1:
|
||||||
|
resolution: {integrity: sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/xml2js/0.4.19:
|
/xml2js/0.4.19:
|
||||||
resolution: {integrity: sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==}
|
resolution: {integrity: sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -21602,6 +21647,11 @@ packages:
|
||||||
resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
|
resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/xpath/0.0.32:
|
||||||
|
resolution: {integrity: sha512-rxMJhSIoiO8vXcWvSifKqhvV96GjiD5wYb8/QHdoRyQvraTpp4IEv944nhGausZZ3u7dhQXteZuZbaqfpB7uYw==}
|
||||||
|
engines: {node: '>=0.6.0'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/xregexp/2.0.0:
|
/xregexp/2.0.0:
|
||||||
resolution: {integrity: sha512-xl/50/Cf32VsGq/1R8jJE5ajH1yMCQkpmoS10QbFZWl2Oor4H0Me64Pu2yxvsRWK3m6soJbmGfzSR7BYmDcWAA==}
|
resolution: {integrity: sha512-xl/50/Cf32VsGq/1R8jJE5ajH1yMCQkpmoS10QbFZWl2Oor4H0Me64Pu2yxvsRWK3m6soJbmGfzSR7BYmDcWAA==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
Loading…
Reference in a new issue