fix(core): Improve SAML connection test result views (#5981)

* improve test result views

* refactor

* lint fix
This commit is contained in:
Michael Auerswald 2023-04-14 15:49:10 +02:00 committed by GitHub
parent 18d5156994
commit 4c994faec1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 109 additions and 16 deletions

View file

@ -13,7 +13,7 @@ import { getInitSSOFormView } from '../views/initSsoPost';
import { issueCookie } from '@/auth/jwt'; import { issueCookie } from '@/auth/jwt';
import { validate } from 'class-validator'; import { validate } from 'class-validator';
import type { PostBindingContext } from 'samlify/types/src/entity'; import type { PostBindingContext } from 'samlify/types/src/entity';
import { isSamlLicensedAndEnabled } from '../samlHelpers'; import { isConnectionTestRequest, isSamlLicensedAndEnabled } from '../samlHelpers';
import type { SamlLoginBinding } from '../types'; import type { SamlLoginBinding } from '../types';
import { AuthenticatedRequest } from '@/requests'; import { AuthenticatedRequest } from '@/requests';
import { import {
@ -21,6 +21,8 @@ import {
getServiceProviderEntityId, getServiceProviderEntityId,
getServiceProviderReturnUrl, getServiceProviderReturnUrl,
} from '../serviceProvider.ee'; } from '../serviceProvider.ee';
import { getSamlConnectionTestSuccessView } from '../views/samlConnectionTestSuccess';
import { getSamlConnectionTestFailedView } from '../views/samlConnectionTestFailed';
@RestController('/sso/saml') @RestController('/sso/saml')
export class SamlController { export class SamlController {
@ -106,11 +108,15 @@ export class SamlController {
res: express.Response, res: express.Response,
binding: SamlLoginBinding, binding: SamlLoginBinding,
) { ) {
try {
const loginResult = await this.samlService.handleSamlLogin(req, binding); const loginResult = await this.samlService.handleSamlLogin(req, binding);
if (loginResult) { // if RelayState is set to the test connection Url, this is a test connection
// return attributes if this is a test connection if (isConnectionTestRequest(req)) {
if (req.body.RelayState && req.body.RelayState === getServiceProviderConfigTestReturnUrl()) { if (loginResult.authenticatedUser) {
return res.status(202).send(loginResult.attributes); return res.send(getSamlConnectionTestSuccessView(loginResult.attributes));
} else {
return res.send(getSamlConnectionTestFailedView('', loginResult.attributes));
}
} }
if (loginResult.authenticatedUser) { if (loginResult.authenticatedUser) {
// Only sign in user if SAML is enabled, otherwise treat as test connection // Only sign in user if SAML is enabled, otherwise treat as test connection
@ -125,8 +131,13 @@ export class SamlController {
return res.status(202).send(loginResult.attributes); return res.status(202).send(loginResult.attributes);
} }
} }
}
throw new AuthError('SAML Authentication failed'); throw new AuthError('SAML Authentication failed');
} catch (error) {
if (isConnectionTestRequest(req)) {
return res.send(getSamlConnectionTestFailedView((error as Error).message));
}
throw new AuthError('SAML Authentication failed: ' + (error as Error).message);
}
} }
/** /**

View file

@ -139,14 +139,11 @@ export class SamlService {
async handleSamlLogin( async handleSamlLogin(
req: express.Request, req: express.Request,
binding: SamlLoginBinding, binding: SamlLoginBinding,
): Promise< ): Promise<{
| {
authenticatedUser: User | undefined; authenticatedUser: User | undefined;
attributes: SamlUserAttributes; attributes: SamlUserAttributes;
onboardingRequired: boolean; onboardingRequired: boolean;
} }> {
| undefined
> {
const attributes = await this.getAttributesFromLoginResponse(req, binding); const attributes = await this.getAttributesFromLoginResponse(req, binding);
if (attributes.email) { if (attributes.email) {
const user = await Db.collections.User.findOne({ const user = await Db.collections.User.findOne({
@ -187,7 +184,11 @@ export class SamlService {
} }
} }
} }
return undefined; return {
authenticatedUser: undefined,
attributes,
onboardingRequired: false,
};
} }
async setSamlPreferences(prefs: SamlPreferences): Promise<SamlPreferences | undefined> { async setSamlPreferences(prefs: SamlPreferences): Promise<SamlPreferences | undefined> {

View file

@ -18,6 +18,8 @@ import {
isSamlCurrentAuthenticationMethod, isSamlCurrentAuthenticationMethod,
setCurrentAuthenticationMethod, setCurrentAuthenticationMethod,
} from '../ssoHelpers'; } from '../ssoHelpers';
import { getServiceProviderConfigTestReturnUrl } from './serviceProvider.ee';
import type { SamlConfiguration } from './types/requests';
/** /**
* Check whether the SAML feature is licensed and enabled in the instance * Check whether the SAML feature is licensed and enabled in the instance
*/ */
@ -173,3 +175,7 @@ export function getMappedSamlAttributesFromFlowResult(
} }
return result; return result;
} }
export function isConnectionTestRequest(req: SamlConfiguration.AcsRequest): boolean {
return req.body.RelayState === getServiceProviderConfigTestReturnUrl();
}

View file

@ -0,0 +1,42 @@
import type { SamlUserAttributes } from '../types/samlUserAttributes';
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

@ -0,0 +1,33 @@
import type { SamlUserAttributes } from '../types/samlUserAttributes';
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>
`;
}