feat(core): Convert saml controller to decorator style (#5653) (no-changelog)

This commit is contained in:
Michael Auerswald 2023-03-10 19:19:52 +01:00 committed by GitHub
parent 90afa5e55f
commit c6ba0bd8de
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 180 additions and 173 deletions

View file

@ -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

View 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);
}
}

View file

@ -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);
},
);

View file

@ -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());
});

View file

@ -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 =

View file

@ -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 {

View file

@ -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: [
{

View file

@ -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',
},
};
}

View file

@ -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');
}