diff --git a/packages/cli/src/sso/saml/routes/saml.controller.ee.ts b/packages/cli/src/sso/saml/routes/saml.controller.ee.ts index 7ced030b0..86e7b75cf 100644 --- a/packages/cli/src/sso/saml/routes/saml.controller.ee.ts +++ b/packages/cli/src/sso/saml/routes/saml.controller.ee.ts @@ -13,7 +13,7 @@ import { getInitSSOFormView } from '../views/initSsoPost'; import { issueCookie } from '@/auth/jwt'; import { validate } from 'class-validator'; import type { PostBindingContext } from 'samlify/types/src/entity'; -import { isSamlLicensedAndEnabled } from '../samlHelpers'; +import { isConnectionTestRequest, isSamlLicensedAndEnabled } from '../samlHelpers'; import type { SamlLoginBinding } from '../types'; import { AuthenticatedRequest } from '@/requests'; import { @@ -21,6 +21,8 @@ import { getServiceProviderEntityId, getServiceProviderReturnUrl, } from '../serviceProvider.ee'; +import { getSamlConnectionTestSuccessView } from '../views/samlConnectionTestSuccess'; +import { getSamlConnectionTestFailedView } from '../views/samlConnectionTestFailed'; @RestController('/sso/saml') export class SamlController { @@ -106,11 +108,15 @@ export class SamlController { res: express.Response, binding: SamlLoginBinding, ) { - const loginResult = await this.samlService.handleSamlLogin(req, binding); - if (loginResult) { - // return attributes if this is a test connection - if (req.body.RelayState && req.body.RelayState === getServiceProviderConfigTestReturnUrl()) { - return res.status(202).send(loginResult.attributes); + try { + const loginResult = await this.samlService.handleSamlLogin(req, binding); + // 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)); + } else { + return res.send(getSamlConnectionTestFailedView('', loginResult.attributes)); + } } if (loginResult.authenticatedUser) { // 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); } } + 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); } - throw new AuthError('SAML Authentication failed'); } /** diff --git a/packages/cli/src/sso/saml/saml.service.ee.ts b/packages/cli/src/sso/saml/saml.service.ee.ts index 22909f46c..846c7541b 100644 --- a/packages/cli/src/sso/saml/saml.service.ee.ts +++ b/packages/cli/src/sso/saml/saml.service.ee.ts @@ -139,14 +139,11 @@ export class SamlService { async handleSamlLogin( req: express.Request, binding: SamlLoginBinding, - ): Promise< - | { - authenticatedUser: User | undefined; - attributes: SamlUserAttributes; - onboardingRequired: boolean; - } - | undefined - > { + ): Promise<{ + authenticatedUser: User | undefined; + attributes: SamlUserAttributes; + onboardingRequired: boolean; + }> { const attributes = await this.getAttributesFromLoginResponse(req, binding); if (attributes.email) { 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 { diff --git a/packages/cli/src/sso/saml/samlHelpers.ts b/packages/cli/src/sso/saml/samlHelpers.ts index 55efbedd6..0672965de 100644 --- a/packages/cli/src/sso/saml/samlHelpers.ts +++ b/packages/cli/src/sso/saml/samlHelpers.ts @@ -18,6 +18,8 @@ import { isSamlCurrentAuthenticationMethod, setCurrentAuthenticationMethod, } 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 */ @@ -173,3 +175,7 @@ export function getMappedSamlAttributesFromFlowResult( } return result; } + +export function isConnectionTestRequest(req: SamlConfiguration.AcsRequest): boolean { + return req.body.RelayState === getServiceProviderConfigTestReturnUrl(); +} diff --git a/packages/cli/src/sso/saml/views/samlConnectionTestFailed.ts b/packages/cli/src/sso/saml/views/samlConnectionTestFailed.ts new file mode 100644 index 000000000..8d9a3578f --- /dev/null +++ b/packages/cli/src/sso/saml/views/samlConnectionTestFailed.ts @@ -0,0 +1,42 @@ +import type { SamlUserAttributes } from '../types/samlUserAttributes'; + +export function getSamlConnectionTestFailedView( + message: string, + attributes?: SamlUserAttributes, +): string { + return ` + + + n8n - SAML Connection Test Result + + + +
+

SAML Connection Test failed

+

${message ?? 'A common issue could be that no email attribute is set'}

+ +

+ ${ + attributes + ? ` +

Here are the attributes returned by your SAML IdP:

+
    +
  • Email: ${attributes?.email ?? '(n/a)'}
  • +
  • First Name: ${attributes?.firstName ?? '(n/a)'}
  • +
  • Last Name: ${attributes?.lastName ?? '(n/a)'}
  • +
  • UPN: ${attributes?.userPrincipalName ?? '(n/a)'}
  • +
` + : '' + } +
+ +
+ `; +} diff --git a/packages/cli/src/sso/saml/views/samlConnectionTestSuccess.ts b/packages/cli/src/sso/saml/views/samlConnectionTestSuccess.ts new file mode 100644 index 000000000..59e6aed26 --- /dev/null +++ b/packages/cli/src/sso/saml/views/samlConnectionTestSuccess.ts @@ -0,0 +1,33 @@ +import type { SamlUserAttributes } from '../types/samlUserAttributes'; + +export function getSamlConnectionTestSuccessView(attributes: SamlUserAttributes): string { + return ` + + + n8n - SAML Connection Test Result + + + +
+

SAML Connection Test was successful

+ +

+

Here are the attributes returned by your SAML IdP:

+
    +
  • Email: ${attributes.email ?? '(n/a)'}
  • +
  • First Name: ${attributes.firstName ?? '(n/a)'}
  • +
  • Last Name: ${attributes.lastName ?? '(n/a)'}
  • +
  • UPN: ${attributes.userPrincipalName ?? '(n/a)'}
  • +
+
+ +
+ `; +}