fix(core): Sanitise IdP provided information in SAML test pages (#11171)

Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <aditya@netroy.in>
This commit is contained in:
Val 2024-10-23 12:22:15 +01:00 committed by GitHub
parent 992547baf8
commit 74fc3889b9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 150 additions and 84 deletions

View file

@ -0,0 +1,77 @@
import { type Response } from 'express';
import { mock } from 'jest-mock-extended';
import type { User } from '@/databases/entities/user';
import { UrlService } from '@/services/url.service';
import { mockInstance } from '@test/mocking';
import { SamlService } from '../../saml.service.ee';
import { getServiceProviderConfigTestReturnUrl } from '../../service-provider.ee';
import type { SamlConfiguration } from '../../types/requests';
import type { SamlUserAttributes } from '../../types/saml-user-attributes';
import { SamlController } from '../saml.controller.ee';
const urlService = mockInstance(UrlService);
urlService.getInstanceBaseUrl.mockReturnValue('');
const samlService = mockInstance(SamlService);
const controller = new SamlController(mock(), samlService, mock(), mock());
const user = mock<User>({
id: '123',
password: 'password',
authIdentities: [],
role: 'global:owner',
});
const attributes: SamlUserAttributes = {
email: 'test@example.com',
firstName: 'Test',
lastName: 'User',
userPrincipalName: 'upn:test@example.com',
};
describe('Test views', () => {
test('Should render success with template', async () => {
const req = mock<SamlConfiguration.AcsRequest>();
const res = mock<Response>();
req.body.RelayState = getServiceProviderConfigTestReturnUrl();
samlService.handleSamlLogin.mockResolvedValueOnce({
authenticatedUser: user,
attributes,
onboardingRequired: false,
});
await controller.acsPost(req, res);
expect(res.render).toBeCalledWith('saml-connection-test-success', attributes);
});
test('Should render failure with template', async () => {
const req = mock<SamlConfiguration.AcsRequest>();
const res = mock<Response>();
req.body.RelayState = getServiceProviderConfigTestReturnUrl();
samlService.handleSamlLogin.mockResolvedValueOnce({
authenticatedUser: undefined,
attributes,
onboardingRequired: false,
});
await controller.acsPost(req, res);
expect(res.render).toBeCalledWith('saml-connection-test-failed', { message: '', attributes });
});
test('Should render error with template', async () => {
const req = mock<SamlConfiguration.AcsRequest>();
const res = mock<Response>();
req.body.RelayState = getServiceProviderConfigTestReturnUrl();
samlService.handleSamlLogin.mockRejectedValueOnce(new Error('Test Error'));
await controller.acsPost(req, res);
expect(res.render).toBeCalledWith('saml-connection-test-failed', { message: 'Test Error' });
});
});

View file

@ -10,6 +10,7 @@ import { AuthError } from '@/errors/response-errors/auth.error';
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { EventService } from '@/events/event.service';
import { AuthenticatedRequest } from '@/requests';
import { sendErrorResponse } from '@/response-helper';
import { UrlService } from '@/services/url.service';
import {
@ -26,8 +27,6 @@ import {
import type { SamlLoginBinding } from '../types';
import { SamlConfiguration } from '../types/requests';
import { getInitSSOFormView } from '../views/init-sso-post';
import { getSamlConnectionTestFailedView } from '../views/saml-connection-test-failed';
import { getSamlConnectionTestSuccessView } from '../views/saml-connection-test-success';
@RestController('/sso/saml')
export class SamlController {
@ -92,7 +91,7 @@ export class SamlController {
/**
* Assertion Consumer Service endpoint
*/
@Get('/acs', { middlewares: [samlLicensedMiddleware], skipAuth: true })
@Get('/acs', { middlewares: [samlLicensedMiddleware], skipAuth: true, usesTemplates: true })
async acsGet(req: SamlConfiguration.AcsRequest, res: express.Response) {
return await this.acsHandler(req, res, 'redirect');
}
@ -100,7 +99,7 @@ export class SamlController {
/**
* Assertion Consumer Service endpoint
*/
@Post('/acs', { middlewares: [samlLicensedMiddleware], skipAuth: true })
@Post('/acs', { middlewares: [samlLicensedMiddleware], skipAuth: true, usesTemplates: true })
async acsPost(req: SamlConfiguration.AcsRequest, res: express.Response) {
return await this.acsHandler(req, res, 'post');
}
@ -120,9 +119,12 @@ export class SamlController {
// if RelayState is set to the test connection Url, this is a test connection
if (isConnectionTestRequest(req)) {
if (loginResult.authenticatedUser) {
return res.send(getSamlConnectionTestSuccessView(loginResult.attributes));
return res.render('saml-connection-test-success', loginResult.attributes);
} else {
return res.send(getSamlConnectionTestFailedView('', loginResult.attributes));
return res.render('saml-connection-test-failed', {
message: '',
attributes: loginResult.attributes,
});
}
}
if (loginResult.authenticatedUser) {
@ -148,16 +150,21 @@ export class SamlController {
userEmail: loginResult.attributes.email ?? 'unknown',
authenticationMethod: 'saml',
});
throw new AuthError('SAML Authentication failed');
// Need to manually send the error response since we're using templates
return sendErrorResponse(res, new AuthError('SAML Authentication failed'));
} catch (error) {
if (isConnectionTestRequest(req)) {
return res.send(getSamlConnectionTestFailedView((error as Error).message));
return res.render('saml-connection-test-failed', { message: (error as Error).message });
}
this.eventService.emit('user-login-failed', {
userEmail: 'unknown',
authenticationMethod: 'saml',
});
throw new AuthError('SAML Authentication failed: ' + (error as Error).message);
// Need to manually send the error response since we're using templates
return sendErrorResponse(
res,
new AuthError('SAML Authentication failed: ' + (error as Error).message),
);
}
}

View file

@ -1,42 +0,0 @@
import type { SamlUserAttributes } from '../types/saml-user-attributes';
export function getSamlConnectionTestFailedView(
message: string,
attributes?: SamlUserAttributes,
): string {
return `
<http>
<head>
<title>n8n - SAML Connection Test Result</title>
<style>
body { background: rgb(251,252,254); font-family: 'Open Sans', sans-serif; padding: 10px; margin: auto; width: 500px; top: 40%; position: relative; }
h1 { color: rgb(240, 60, 60); font-size: 16px; font-weight: 400; margin: 0 0 10px 0; }
h2 { color: rgb(0, 0, 0); font-size: 12px; font-weight: 400; margin: 0 0 10px 0; }
button { border: 1px solid rgb(219, 223, 231); background: rgb(255, 255, 255); border-radius: 4px; padding: 10px; }
ul { border: 1px solid rgb(219, 223, 231); border-radius: 4px; padding: 10px; }
li { decoration: none; list-style: none; margin: 0 0 0px 0; color: rgb(125, 125, 125); font-size: 12px;}
</style>
</head>
<body>
<div style="text-align:center">
<h1>SAML Connection Test failed</h1>
<h2>${message ?? 'A common issue could be that no email attribute is set'}</h2>
<button onclick="window.close()">You can close this window now</button>
<p></p>
${
attributes
? `
<h2>Here are the attributes returned by your SAML IdP:</h2>
<ul>
<li><strong>Email:</strong> ${attributes?.email ?? '(n/a)'}</li>
<li><strong>First Name:</strong> ${attributes?.firstName ?? '(n/a)'}</li>
<li><strong>Last Name:</strong> ${attributes?.lastName ?? '(n/a)'}</li>
<li><strong>UPN:</strong> ${attributes?.userPrincipalName ?? '(n/a)'}</li>
</ul>`
: ''
}
</div>
</body>
</http>
`;
}

View file

@ -1,33 +0,0 @@
import type { SamlUserAttributes } from '../types/saml-user-attributes';
export function getSamlConnectionTestSuccessView(attributes: SamlUserAttributes): string {
return `
<http>
<head>
<title>n8n - SAML Connection Test Result</title>
<style>
body { background: rgb(251,252,254); font-family: 'Open Sans', sans-serif; padding: 10px; margin: auto; width: 500px; top: 40%; position: relative; }
h1 { color: rgb(0, 0, 0); font-size: 16px; font-weight: 400; margin: 0 0 10px 0; }
h2 { color: rgb(0, 0, 0); font-size: 12px; font-weight: 400; margin: 0 0 10px 0; }
button { border: 1px solid rgb(219, 223, 231); background: rgb(255, 255, 255); border-radius: 4px; padding: 10px; }
ul { border: 1px solid rgb(219, 223, 231); border-radius: 4px; padding: 10px; }
li { decoration: none; list-style: none; margin: 0 0 0px 0; color: rgb(125, 125, 125); font-size: 12px;}
</style>
</head>
<body>
<div style="text-align:center">
<h1>SAML Connection Test was successful</h1>
<button onclick="window.close()">You can close this window now</button>
<p></p>
<h2>Here are the attributes returned by your SAML IdP:</h2>
<ul>
<li><strong>Email:</strong> ${attributes.email ?? '(n/a)'}</li>
<li><strong>First Name:</strong> ${attributes.firstName ?? '(n/a)'}</li>
<li><strong>Last Name:</strong> ${attributes.lastName ?? '(n/a)'}</li>
<li><strong>UPN:</strong> ${attributes.userPrincipalName ?? '(n/a)'}</li>
</ul>
</div>
</body>
</http>
`;
}

View file

@ -0,0 +1,30 @@
<html>
<head>
<title>n8n - SAML Connection Test Result</title>
<style>
body { background: rgb(251,252,254); font-family: 'Open Sans', sans-serif; padding: 10px; margin: auto; width: 500px; top: 40%; position: relative; }
h1 { color: rgb(240, 60, 60); font-size: 16px; font-weight: 400; margin: 0 0 10px 0; }
h2 { color: rgb(0, 0, 0); font-size: 12px; font-weight: 400; margin: 0 0 10px 0; }
button { border: 1px solid rgb(219, 223, 231); background: rgb(255, 255, 255); border-radius: 4px; padding: 10px; }
ul { border: 1px solid rgb(219, 223, 231); border-radius: 4px; padding: 10px; }
li { decoration: none; list-style: none; margin: 0 0 0px 0; color: rgb(125, 125, 125); font-size: 12px;}
</style>
</head>
<body>
<div style="text-align:center">
<h1>SAML Connection Test failed</h1>
<h2>{{#if message}}{{message}}{{else}}A common issue could be that no email attribute is set{{/if}}</h2>
<button onclick="window.close()">You can close this window now</button>
<p></p>
{{#with attributes}}
<h2>Here are the attributes returned by your SAML IdP:</h2>
<ul>
<li><strong>Email:</strong> {{#if email}}{{email}}{{else}}(n/a){{/if}}</li>
<li><strong>First Name:</strong> {{#if firstName}}{{firstName}}{{else}}(n/a){{/if}}</li>
<li><strong>Last Name:</strong> {{#if lastName}}{{lastName}}{{else}}(n/a){{/if}}</li>
<li><strong>UPN:</strong> {{#if userPrincipalName}}{{userPrincipalName}}{{else}}(n/a){{/if}}</li>
{{/with}}
</ul>
</div>
</body>
</html>

View file

@ -0,0 +1,27 @@
<html>
<head>
<title>n8n - SAML Connection Test Result</title>
<style>
body { background: rgb(251,252,254); font-family: 'Open Sans', sans-serif; padding: 10px; margin: auto; width: 500px; top: 40%; position: relative; }
h1 { color: rgb(0, 0, 0); font-size: 16px; font-weight: 400; margin: 0 0 10px 0; }
h2 { color: rgb(0, 0, 0); font-size: 12px; font-weight: 400; margin: 0 0 10px 0; }
button { border: 1px solid rgb(219, 223, 231); background: rgb(255, 255, 255); border-radius: 4px; padding: 10px; }
ul { border: 1px solid rgb(219, 223, 231); border-radius: 4px; padding: 10px; }
li { decoration: none; list-style: none; margin: 0 0 0px 0; color: rgb(125, 125, 125); font-size: 12px;}
</style>
</head>
<body>
<div style="text-align:center">
<h1>SAML Connection Test was successful</h1>
<button onclick="window.close()">You can close this window now</button>
<p></p>
<h2>Here are the attributes returned by your SAML IdP:</h2>
<ul>
<li><strong>Email:</strong> {{#if email}}{{email}}{{else}}(n/a){{/if}}</li>
<li><strong>First Name:</strong> {{#if firstName}}{{firstName}}{{else}}(n/a){{/if}}</li>
<li><strong>Last Name:</strong> {{#if lastName}}{{lastName}}{{else}}(n/a){{/if}}</li>
<li><strong>UPN:</strong> {{#if userPrincipalName}}{{userPrincipalName}}{{else}}(n/a){{/if}}</li>
</ul>
</div>
</body>
</html>