diff --git a/packages/cli/src/sso.ee/saml/__tests__/saml.service.ee.test.ts b/packages/cli/src/sso.ee/saml/__tests__/saml.service.ee.test.ts index 01741c3725..05ad0d5600 100644 --- a/packages/cli/src/sso.ee/saml/__tests__/saml.service.ee.test.ts +++ b/packages/cli/src/sso.ee/saml/__tests__/saml.service.ee.test.ts @@ -48,6 +48,9 @@ const InvalidSamlSetting: Settings = { }), }; +const SamlMetadataWithoutRedirectBinding = + '\n\n \n \n \n \n MIIC4jCCAcoCCQC33wnybT5QZDANBgkqhkiG9w0BAQsFADAyMQswCQYDVQQGEwJV\nSzEPMA0GA1UECgwGQm94eUhRMRIwEAYDVQQDDAlNb2NrIFNBTUwwIBcNMjIwMjI4\nMjE0NjM4WhgPMzAyMTA3MDEyMTQ2MzhaMDIxCzAJBgNVBAYTAlVLMQ8wDQYDVQQK\nDAZCb3h5SFExEjAQBgNVBAMMCU1vY2sgU0FNTDCCASIwDQYJKoZIhvcNAQEBBQAD\nggEPADCCAQoCggEBALGfYettMsct1T6tVUwTudNJH5Pnb9GGnkXi9Zw/e6x45DD0\nRuRONbFlJ2T4RjAE/uG+AjXxXQ8o2SZfb9+GgmCHuTJFNgHoZ1nFVXCmb/Hg8Hpd\n4vOAGXndixaReOiq3EH5XvpMjMkJ3+8+9VYMzMZOjkgQtAqO36eAFFfNKX7dTj3V\npwLkvz6/KFCq8OAwY+AUi4eZm5J57D31GzjHwfjH9WTeX0MyndmnNB1qV75qQR3b\n2/W5sGHRv+9AarggJkF+ptUkXoLtVA51wcfYm6hILptpde5FQC8RWY1YrswBWAEZ\nNfyrR4JeSweElNHg4NVOs4TwGjOPwWGqzTfgTlECAwEAATANBgkqhkiG9w0BAQsF\nAAOCAQEAAYRlYflSXAWoZpFfwNiCQVE5d9zZ0DPzNdWhAybXcTyMf0z5mDf6FWBW\n5Gyoi9u3EMEDnzLcJNkwJAAc39Apa4I2/tml+Jy29dk8bTyX6m93ngmCgdLh5Za4\nkhuU3AM3L63g7VexCuO7kwkjh/+LqdcIXsVGO6XDfu2QOs1Xpe9zIzLpwm/RNYeX\nUjbSj5ce/jekpAw7qyVVL4xOyh8AtUW1ek3wIw1MJvEgEPt0d16oshWJpoS1OT8L\nr/22SvYEo3EmSGdTVGgk3x3s+A0qWAqTcyjr7Q4s/GKYRFfomGwz0TZ4Iw1ZN99M\nm0eo2USlSRTVl7QHRTuiuSThHpLKQQ==\n \n \n \n urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress\n \n \n'; + const SamlSettingWithInvalidUrl: Settings = { loadOnStartup: true, key: SAML_PREFERENCES_DB_KEY, @@ -342,6 +345,14 @@ describe('SamlService', () => { expect(samlService.samlPreferences.metadataUrl).toBe(metadataUrlTestData); }); + test('does throw `InvalidSamlMetadataError` when a metadata does not contain redirect binding', async () => { + await expect( + samlService.setSamlPreferences({ + metadata: SamlMetadataWithoutRedirectBinding, + }), + ).rejects.toThrowError(InvalidSamlMetadataError); + }); + test('does throw `InvalidSamlMetadataError` when a metadata url is not a valid url', async () => { await expect( samlService.setSamlPreferences({ @@ -396,6 +407,17 @@ describe('SamlService', () => { }); }); + describe('getIdentityProviderInstance', () => { + test('does throw `InvalidSamlMetadataError` when a metadata does not contain redirect binding', async () => { + await samlService.loadPreferencesWithoutValidation({ + metadata: SamlMetadataWithoutRedirectBinding, + }); + expect(() => samlService.getIdentityProviderInstance(true)).toThrowError( + InvalidSamlMetadataError, + ); + }); + }); + describe('reset', () => { test('disables saml login and deletes the saml `features.saml` key in the db', async () => { // ARRANGE diff --git a/packages/cli/src/sso.ee/saml/errors/invalid-saml-metadata.error.ts b/packages/cli/src/sso.ee/saml/errors/invalid-saml-metadata.error.ts index 7c228a71c6..033fd0162a 100644 --- a/packages/cli/src/sso.ee/saml/errors/invalid-saml-metadata.error.ts +++ b/packages/cli/src/sso.ee/saml/errors/invalid-saml-metadata.error.ts @@ -1,7 +1,7 @@ import { UserError } from 'n8n-workflow'; export class InvalidSamlMetadataError extends UserError { - constructor() { - super('Invalid SAML metadata'); + constructor(detail: string = '') { + super(`Invalid SAML metadata${detail ? ': ' + detail : ''}`); } } diff --git a/packages/cli/src/sso.ee/saml/saml-validator.ts b/packages/cli/src/sso.ee/saml/saml-validator.ts index e2deaa6b6d..a34209ed50 100644 --- a/packages/cli/src/sso.ee/saml/saml-validator.ts +++ b/packages/cli/src/sso.ee/saml/saml-validator.ts @@ -1,7 +1,11 @@ import { Service } from '@n8n/di'; import { Logger } from 'n8n-core'; +import { Constants, IdentityProvider } from 'samlify'; +import type { IdentityProviderInstance } from 'samlify'; import type { XMLFileInfo, XMLLintOptions, XMLValidationResult } from 'xmllint-wasm'; +import { InvalidSamlMetadataError } from './errors/invalid-saml-metadata.error'; + @Service() export class SamlValidator { private xmlMetadata: XMLFileInfo; @@ -21,8 +25,24 @@ export class SamlValidator { this.xmllint = await import('xmllint-wasm'); } + validateIdentiyProvider(idp: IdentityProviderInstance) { + const binding = idp.entityMeta.getSingleSignOnService(Constants.wording.binding.redirect); + if (typeof binding !== 'string') { + throw new InvalidSamlMetadataError('only SAML redirect binding is supported.'); + } + } + async validateMetadata(metadata: string): Promise { - return await this.validateXml('metadata', metadata); + const validXML = await this.validateXml('metadata', metadata); + + if (validXML) { + const idp = IdentityProvider({ + metadata, + }); + this.validateIdentiyProvider(idp); + } + + return validXML; } async validateResponse(response: string): Promise { diff --git a/packages/cli/src/sso.ee/saml/saml.service.ee.ts b/packages/cli/src/sso.ee/saml/saml.service.ee.ts index 3db1d23b23..161981ae35 100644 --- a/packages/cli/src/sso.ee/saml/saml.service.ee.ts +++ b/packages/cli/src/sso.ee/saml/saml.service.ee.ts @@ -7,7 +7,7 @@ import type express from 'express'; import https from 'https'; import { Logger } from 'n8n-core'; import { jsonParse, UnexpectedError } from 'n8n-workflow'; -import type { IdentityProviderInstance, ServiceProviderInstance } from 'samlify'; +import { type IdentityProviderInstance, type ServiceProviderInstance } from 'samlify'; import type { BindingContext, PostBindingContext } from 'samlify/types/src/entity'; import { AuthError } from '@/errors/response-errors/auth.error'; @@ -135,6 +135,8 @@ export class SamlService { }); } + this.validator.validateIdentiyProvider(this.identityProviderInstance); + return this.identityProviderInstance; }