mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
feat(core): Convert saml controller to decorator style (#5653) (no-changelog)
This commit is contained in:
parent
90afa5e55f
commit
c6ba0bd8de
|
@ -153,9 +153,8 @@ import {
|
|||
isAdvancedExecutionFiltersEnabled,
|
||||
} from './executions/executionHelpers';
|
||||
import { getSamlLoginLabel, isSamlLoginEnabled, isSamlLicensed } from './sso/saml/samlHelpers';
|
||||
import { samlControllerPublic } from './sso/saml/routes/saml.controller.public.ee';
|
||||
import { SamlController } from './sso/saml/routes/saml.controller.ee';
|
||||
import { SamlService } from './sso/saml/saml.service.ee';
|
||||
import { samlControllerProtected } from './sso/saml/routes/saml.controller.protected.ee';
|
||||
import { LdapManager } from './Ldap/LdapManager.ee';
|
||||
|
||||
const exec = promisify(callbackExec);
|
||||
|
@ -370,6 +369,7 @@ class Server extends AbstractServer {
|
|||
const internalHooks = Container.get(InternalHooks);
|
||||
const mailer = getMailerInstance();
|
||||
const postHog = this.postHog;
|
||||
const samlService = SamlService.getInstance();
|
||||
|
||||
const controllers: object[] = [
|
||||
new AuthController({ config, internalHooks, repositories, logger, postHog }),
|
||||
|
@ -389,6 +389,7 @@ class Server extends AbstractServer {
|
|||
logger,
|
||||
postHog,
|
||||
}),
|
||||
new SamlController(samlService),
|
||||
];
|
||||
|
||||
if (isLdapEnabled()) {
|
||||
|
@ -514,9 +515,6 @@ class Server extends AbstractServer {
|
|||
}
|
||||
}
|
||||
|
||||
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
|
||||
// get generated dynamically
|
||||
|
|
137
packages/cli/src/sso/saml/routes/saml.controller.ee.ts
Normal file
137
packages/cli/src/sso/saml/routes/saml.controller.ee.ts
Normal file
|
@ -0,0 +1,137 @@
|
|||
import express from 'express';
|
||||
import { Get, Post, RestController } from '../../../decorators';
|
||||
import { SamlUrls } from '../constants';
|
||||
import {
|
||||
samlLicensedAndEnabledMiddleware,
|
||||
samlLicensedOwnerMiddleware,
|
||||
} from '../middleware/samlEnabledMiddleware';
|
||||
import { SamlService } from '../saml.service.ee';
|
||||
import { SamlConfiguration } from '../types/requests';
|
||||
import { AuthError, BadRequestError } from '../../../ResponseHelper';
|
||||
import { getInitSSOFormView } from '../views/initSsoPost';
|
||||
import { getInitSSOPostView } from '../views/initSsoRedirect';
|
||||
import { issueCookie } from '../../../auth/jwt';
|
||||
import { validate } from 'class-validator';
|
||||
import type { PostBindingContext } from 'samlify/types/src/entity';
|
||||
|
||||
@RestController('/sso/saml')
|
||||
export class SamlController {
|
||||
constructor(private samlService: SamlService) {}
|
||||
|
||||
@Get(SamlUrls.metadata)
|
||||
async getServiceProviderMetadata(req: express.Request, res: express.Response) {
|
||||
return res
|
||||
.header('Content-Type', 'text/xml')
|
||||
.send(this.samlService.getServiceProviderInstance().getMetadata());
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /sso/saml/config
|
||||
* Return SAML config
|
||||
*/
|
||||
@Get(SamlUrls.config, { middlewares: [samlLicensedOwnerMiddleware] })
|
||||
async configGet(req: SamlConfiguration.Read, res: express.Response) {
|
||||
const prefs = this.samlService.samlPreferences;
|
||||
return res.send(prefs);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /sso/saml/config
|
||||
* Set SAML config
|
||||
*/
|
||||
@Post(SamlUrls.config, { middlewares: [samlLicensedOwnerMiddleware] })
|
||||
async configPost(req: SamlConfiguration.Update, res: express.Response) {
|
||||
const validationResult = await validate(req.body);
|
||||
if (validationResult.length === 0) {
|
||||
const result = await this.samlService.setSamlPreferences(req.body);
|
||||
return res.send(result);
|
||||
} else {
|
||||
throw new BadRequestError(
|
||||
'Body is not a valid SamlPreferences object: ' +
|
||||
validationResult.map((e) => e.toString()).join(','),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /sso/saml/config/toggle
|
||||
* Set SAML config
|
||||
*/
|
||||
@Post(SamlUrls.configToggleEnabled, { middlewares: [samlLicensedOwnerMiddleware] })
|
||||
async toggleEnabledPost(req: SamlConfiguration.Toggle, res: express.Response) {
|
||||
if (req.body.loginEnabled === undefined) {
|
||||
throw new BadRequestError('Body should contain a boolean "loginEnabled" property');
|
||||
}
|
||||
await this.samlService.setSamlPreferences({ loginEnabled: req.body.loginEnabled });
|
||||
return res.sendStatus(200);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /sso/saml/acs
|
||||
* Assertion Consumer Service endpoint
|
||||
*/
|
||||
@Get(SamlUrls.acs, { middlewares: [samlLicensedAndEnabledMiddleware] })
|
||||
async acsGet(req: express.Request, res: express.Response) {
|
||||
const loginResult = await this.samlService.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
|
||||
*/
|
||||
@Post(SamlUrls.acs, { middlewares: [samlLicensedAndEnabledMiddleware] })
|
||||
async acsPost(req: express.Request, res: express.Response) {
|
||||
const loginResult = await this.samlService.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
|
||||
*/
|
||||
@Get(SamlUrls.initSSO, { middlewares: [samlLicensedAndEnabledMiddleware] })
|
||||
async initSsoGet(req: express.Request, res: express.Response) {
|
||||
const result = this.samlService.getLoginRequestUrl();
|
||||
if (result?.binding === 'redirect') {
|
||||
// forced client side redirect through the use of a javascript redirect
|
||||
return res.send(getInitSSOPostView(result.context));
|
||||
// TODO:SAML: If we want the frontend to handle the redirect, we will send the redirect URL instead:
|
||||
// return res.status(301).send(result.context.context);
|
||||
} else if (result?.binding === 'post') {
|
||||
return res.send(getInitSSOFormView(result.context as PostBindingContext));
|
||||
} else {
|
||||
throw new AuthError('SAML redirect failed, please check your SAML configuration.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /sso/saml/config/test
|
||||
* Test SAML config
|
||||
*/
|
||||
@Get(SamlUrls.configTest, { middlewares: [samlLicensedOwnerMiddleware] })
|
||||
async configTestGet(req: express.Request, res: express.Response) {
|
||||
const testResult = await this.samlService.testSamlConnection();
|
||||
return res.send(testResult);
|
||||
}
|
||||
}
|
|
@ -1,147 +0,0 @@
|
|||
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, BadRequestError } from '@/ResponseHelper';
|
||||
import { issueCookie } from '../../../auth/jwt';
|
||||
import { validate } from 'class-validator';
|
||||
import type { PostBindingContext } from 'samlify/types/src/entity';
|
||||
import { getInitSSOFormView } from '../views/initSsoPost';
|
||||
import { getInitSSOPostView } from '../views/initSsoRedirect';
|
||||
|
||||
export const samlControllerProtected = express.Router();
|
||||
|
||||
/**
|
||||
* GET /sso/saml/config
|
||||
* Return SAML config
|
||||
*/
|
||||
samlControllerProtected.get(
|
||||
SamlUrls.config,
|
||||
samlLicensedOwnerMiddleware,
|
||||
(req: SamlConfiguration.Read, res: express.Response) => {
|
||||
const prefs = SamlService.getInstance().samlPreferences;
|
||||
return res.send(prefs);
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* POST /sso/saml/config
|
||||
* Set SAML config
|
||||
*/
|
||||
samlControllerProtected.post(
|
||||
SamlUrls.config,
|
||||
samlLicensedOwnerMiddleware,
|
||||
async (req: SamlConfiguration.Update, res: express.Response) => {
|
||||
const validationResult = await validate(req.body);
|
||||
if (validationResult.length === 0) {
|
||||
const result = await SamlService.getInstance().setSamlPreferences(req.body);
|
||||
return res.send(result);
|
||||
} else {
|
||||
throw new BadRequestError(
|
||||
'Body is not a valid SamlPreferences object: ' +
|
||||
validationResult.map((e) => e.toString()).join(','),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* POST /sso/saml/config/toggle
|
||||
* Set SAML config
|
||||
*/
|
||||
samlControllerProtected.post(
|
||||
SamlUrls.configToggleEnabled,
|
||||
samlLicensedOwnerMiddleware,
|
||||
async (req: SamlConfiguration.Toggle, res: express.Response) => {
|
||||
if (req.body.loginEnabled === undefined) {
|
||||
throw new BadRequestError('Body should contain a boolean "loginEnabled" property');
|
||||
}
|
||||
await SamlService.getInstance().setSamlPreferences({ loginEnabled: req.body.loginEnabled });
|
||||
res.sendStatus(200);
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* 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 result = SamlService.getInstance().getLoginRequestUrl();
|
||||
if (result?.binding === 'redirect') {
|
||||
// forced client side redirect through the use of a javascript redirect
|
||||
return res.send(getInitSSOPostView(result.context));
|
||||
// TODO:SAML: If we want the frontend to handle the redirect, we will send the redirect URL instead:
|
||||
// return res.status(301).send(result.context.context);
|
||||
} else if (result?.binding === 'post') {
|
||||
return res.send(getInitSSOFormView(result.context as PostBindingContext));
|
||||
} else {
|
||||
throw new AuthError('SAML redirect failed, please check your SAML configuration.');
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* GET /sso/saml/config/test
|
||||
* Test SAML config
|
||||
*/
|
||||
samlControllerProtected.get(
|
||||
SamlUrls.configTest,
|
||||
samlLicensedOwnerMiddleware,
|
||||
async (req: express.Request, res: express.Response) => {
|
||||
const testResult = await SamlService.getInstance().testSamlConnection();
|
||||
return res.send(testResult);
|
||||
},
|
||||
);
|
|
@ -1,19 +0,0 @@
|
|||
import express from 'express';
|
||||
import { SamlUrls } from '../constants';
|
||||
import { SamlService } from '../saml.service.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(SamlService.getInstance().getServiceProviderInstance().getMetadata());
|
||||
});
|
|
@ -48,6 +48,13 @@ export class SamlService {
|
|||
loginLabel: 'SAML',
|
||||
wantAssertionsSigned: true,
|
||||
wantMessageSigned: true,
|
||||
signatureConfig: {
|
||||
prefix: 'ds',
|
||||
location: {
|
||||
reference: '/samlp:Response/saml:Issuer',
|
||||
action: 'after',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
public get samlPreferences(): SamlPreferences {
|
||||
|
@ -189,6 +196,8 @@ export class SamlService {
|
|||
this._samlPreferences.mapping = prefs.mapping ?? this._samlPreferences.mapping;
|
||||
this._samlPreferences.ignoreSSL = prefs.ignoreSSL ?? this._samlPreferences.ignoreSSL;
|
||||
this._samlPreferences.acsBinding = prefs.acsBinding ?? this._samlPreferences.acsBinding;
|
||||
this._samlPreferences.signatureConfig =
|
||||
prefs.signatureConfig ?? this._samlPreferences.signatureConfig;
|
||||
this._samlPreferences.authnRequestsSigned =
|
||||
prefs.authnRequestsSigned ?? this._samlPreferences.authnRequestsSigned;
|
||||
this._samlPreferences.wantAssertionsSigned =
|
||||
|
|
|
@ -10,7 +10,11 @@ import type { SamlUserAttributes } from './types/samlUserAttributes';
|
|||
import type { FlowResult } from 'samlify/types/src/flow';
|
||||
import type { SamlAttributeMapping } from './types/samlAttributeMapping';
|
||||
import { SAML_ENTERPRISE_FEATURE_ENABLED, SAML_LOGIN_ENABLED, SAML_LOGIN_LABEL } from './constants';
|
||||
import { isSamlCurrentAuthenticationMethod } from '../ssoHelpers';
|
||||
import {
|
||||
isEmailCurrentAuthenticationMethod,
|
||||
isSamlCurrentAuthenticationMethod,
|
||||
setCurrentAuthenticationMethod,
|
||||
} from '../ssoHelpers';
|
||||
/**
|
||||
* Check whether the SAML feature is licensed and enabled in the instance
|
||||
*/
|
||||
|
@ -22,8 +26,17 @@ export function getSamlLoginLabel(): string {
|
|||
return config.getEnv(SAML_LOGIN_LABEL);
|
||||
}
|
||||
|
||||
// can only toggle between email and saml, not directly to e.g. ldap
|
||||
export function setSamlLoginEnabled(enabled: boolean): void {
|
||||
config.set(SAML_LOGIN_ENABLED, enabled);
|
||||
if (enabled) {
|
||||
if (isEmailCurrentAuthenticationMethod()) {
|
||||
config.set(SAML_LOGIN_ENABLED, true);
|
||||
setCurrentAuthenticationMethod('saml');
|
||||
}
|
||||
} else {
|
||||
config.set(SAML_LOGIN_ENABLED, false);
|
||||
setCurrentAuthenticationMethod('email');
|
||||
}
|
||||
}
|
||||
|
||||
export function setSamlLoginLabel(label: string): void {
|
||||
|
|
|
@ -15,6 +15,7 @@ export function getServiceProviderInstance(prefs: SamlPreferences): ServiceProvi
|
|||
authnRequestsSigned: prefs.authnRequestsSigned,
|
||||
wantAssertionsSigned: prefs.wantAssertionsSigned,
|
||||
wantMessageSigned: prefs.wantMessageSigned,
|
||||
signatureConfig: prefs.signatureConfig,
|
||||
nameIDFormat: ['urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'],
|
||||
assertionConsumerService: [
|
||||
{
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { IsBoolean, IsObject, IsOptional, IsString } from 'class-validator';
|
||||
import { SignatureConfig } from 'samlify/types/src/types';
|
||||
import { SamlLoginBinding } from '.';
|
||||
import { SamlAttributeMapping } from './samlAttributeMapping';
|
||||
|
||||
|
@ -46,4 +47,14 @@ export class SamlPreferences {
|
|||
@IsString()
|
||||
@IsOptional()
|
||||
acsBinding?: SamlLoginBinding = 'post';
|
||||
|
||||
@IsObject()
|
||||
@IsOptional()
|
||||
signatureConfig?: SignatureConfig = {
|
||||
prefix: 'ds',
|
||||
location: {
|
||||
reference: '/samlp:Response/saml:Issuer',
|
||||
action: 'after',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -5,6 +5,10 @@ export function isSamlCurrentAuthenticationMethod(): boolean {
|
|||
return config.getEnv('userManagement.authenticationMethod') === 'saml';
|
||||
}
|
||||
|
||||
export function isEmailCurrentAuthenticationMethod(): boolean {
|
||||
return config.getEnv('userManagement.authenticationMethod') === 'email';
|
||||
}
|
||||
|
||||
export function isSsoJustInTimeProvisioningEnabled(): boolean {
|
||||
return config.getEnv('sso.justInTimeProvisioning');
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue