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:
committed by
GitHub
parent
d09ca875ec
commit
40a934bbb4
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');
|
||||
}
|
||||
Reference in New Issue
Block a user