diff --git a/core-web/libs/portlets/dot-auth/src/lib/dot-auth-config/components/dot-auth-saml-config.component.html b/core-web/libs/portlets/dot-auth/src/lib/dot-auth-config/components/dot-auth-saml-config.component.html
index b86537d2bfaf..63a534dc3f03 100644
--- a/core-web/libs/portlets/dot-auth/src/lib/dot-auth-config/components/dot-auth-saml-config.component.html
+++ b/core-web/libs/portlets/dot-auth/src/lib/dot-auth-config/components/dot-auth-saml-config.component.html
@@ -144,8 +144,10 @@
tooltipPosition="top">
@@ -158,8 +160,10 @@
tooltipPosition="top">
diff --git a/core-web/libs/portlets/dot-auth/src/lib/dot-auth-config/store/dot-auth-config.mappers.spec.ts b/core-web/libs/portlets/dot-auth/src/lib/dot-auth-config/store/dot-auth-config.mappers.spec.ts
index 3713dfa8d4b2..b9b9c18c28c3 100644
--- a/core-web/libs/portlets/dot-auth/src/lib/dot-auth-config/store/dot-auth-config.mappers.spec.ts
+++ b/core-web/libs/portlets/dot-auth/src/lib/dot-auth-config/store/dot-auth-config.mappers.spec.ts
@@ -1,4 +1,9 @@
-import { DotAuthConfig, DotAuthConfigView, DotAuthDiscoveryView } from '@dotcms/dotcms-models';
+import {
+ DotAuthConfig,
+ DotAuthConfigView,
+ DotAuthDiscoveryView,
+ DotAuthSignatureValidation
+} from '@dotcms/dotcms-models';
import {
DEFAULT_CONFIG,
@@ -323,6 +328,39 @@ describe('dot-auth-config.mappers', () => {
expect(config.saml.wantResponseSigned).toBe(true);
});
+ it('maps single-signature validation types', () => {
+ const assertionOnly = fromView({
+ ...SAML_VIEW,
+ values: { ...SAML_VIEW.values, signatureValidationType: 'assertion' }
+ });
+ expect(assertionOnly.saml.wantAssertionsSigned).toBe(true);
+ expect(assertionOnly.saml.wantResponseSigned).toBe(false);
+
+ const responseOnly = fromView({
+ ...SAML_VIEW,
+ values: { ...SAML_VIEW.values, signatureValidationType: 'response' }
+ });
+ expect(responseOnly.saml.wantAssertionsSigned).toBe(false);
+ expect(responseOnly.saml.wantResponseSigned).toBe(true);
+ });
+
+ it('shows a legacy "none" or unknown validation type as both signatures required', () => {
+ // out-of-range values on purpose: what an old or hand-edited configuration may hold
+ const legacyValues = [
+ 'none',
+ '',
+ 'signature'
+ ] as unknown as DotAuthSignatureValidation[];
+ for (const signatureValidationType of legacyValues) {
+ const config = fromView({
+ ...SAML_VIEW,
+ values: { ...SAML_VIEW.values, signatureValidationType }
+ });
+ expect(config.saml.wantAssertionsSigned).toBe(true);
+ expect(config.saml.wantResponseSigned).toBe(true);
+ }
+ });
+
it('maps SAML claim fields', () => {
const config = fromView(SAML_VIEW);
expect(config.saml.claimEmail).toBe('mail');
@@ -527,13 +565,14 @@ describe('dot-auth-config.mappers', () => {
]
).toBe('response');
+ // never 'none': with neither box checked both signatures are required
config.saml.wantAssertionsSigned = false;
config.saml.wantResponseSigned = false;
expect(
(toPayload(config, 'test-site-id').values as Record)[
'signatureValidationType'
]
- ).toBe('none');
+ ).toBe('responseandassertion');
});
it('round-trips idpName and sPEndpointHostname so a save never wipes a working SP hostname', () => {
diff --git a/core-web/libs/portlets/dot-auth/src/lib/dot-auth-config/store/dot-auth-config.mappers.ts b/core-web/libs/portlets/dot-auth/src/lib/dot-auth-config/store/dot-auth-config.mappers.ts
index f06ba2824c39..b1f838e912a8 100644
--- a/core-web/libs/portlets/dot-auth/src/lib/dot-auth-config/store/dot-auth-config.mappers.ts
+++ b/core-web/libs/portlets/dot-auth/src/lib/dot-auth-config/store/dot-auth-config.mappers.ts
@@ -80,6 +80,17 @@ export const DEFAULT_CONFIG: DotAuthConfig = {
}
};
+/** The SAML signature validation type the backend enforces for a stored value. */
+function samlValidationType(value: unknown): 'response' | 'assertion' | 'responseandassertion' {
+ const normalized = String(value ?? '')
+ .trim()
+ .toLowerCase();
+
+ return normalized === 'response' || normalized === 'assertion'
+ ? normalized
+ : 'responseandassertion';
+}
+
export function fromView(view: DotAuthConfigView): DotAuthConfig {
const config = clone(DEFAULT_CONFIG);
config.protocol =
@@ -112,12 +123,10 @@ export function fromView(view: DotAuthConfigView): DotAuthConfig {
x509cert: String(values.publicCert ?? ''),
privateKey: String(values.privateKey ?? ''),
signRequests: String(values['signRequests'] ?? 'true') === 'true',
- wantAssertionsSigned:
- values.signatureValidationType === 'assertion' ||
- values.signatureValidationType === 'responseandassertion',
- wantResponseSigned:
- values.signatureValidationType === 'response' ||
- values.signatureValidationType === 'responseandassertion',
+ // Anything other than 'response' / 'assertion' (e.g. a legacy 'none') is enforced by
+ // the backend as 'responseandassertion', so show it that way.
+ wantAssertionsSigned: samlValidationType(values.signatureValidationType) !== 'response',
+ wantResponseSigned: samlValidationType(values.signatureValidationType) !== 'assertion',
claimEmail: String(values['attribute.email.name'] ?? config.saml.claimEmail),
claimFirstName: String(
values['attribute.firstname.name'] ?? config.saml.claimFirstName
@@ -180,13 +189,13 @@ export function toPayload(config: DotAuthConfig, siteId: string): DotAuthConfigP
// empty string would be STORED and shadow the host-name fallback downstream.
sPEndpointHostname: config.saml.spEndpointHostname || undefined,
signRequests: String(config.saml.signRequests),
- signatureValidationType: config.saml.wantResponseSigned
- ? config.saml.wantAssertionsSigned
- ? 'responseandassertion'
- : 'response'
- : config.saml.wantAssertionsSigned
- ? 'assertion'
- : 'none',
+ // Never 'none': with neither box checked both signatures are required.
+ signatureValidationType:
+ config.saml.wantResponseSigned && !config.saml.wantAssertionsSigned
+ ? 'response'
+ : config.saml.wantAssertionsSigned && !config.saml.wantResponseSigned
+ ? 'assertion'
+ : 'responseandassertion',
idPMetadataFile: config.saml.metadataUrl,
publicCert: config.saml.x509cert || undefined,
privateKey: config.saml.privateKey || undefined,
diff --git a/dotCMS/src/main/java/com/dotcms/auth/dotAuth/rest/handler/SamlProtocolHandler.java b/dotCMS/src/main/java/com/dotcms/auth/dotAuth/rest/handler/SamlProtocolHandler.java
index dbc57e47960f..73db4b1076ab 100644
--- a/dotCMS/src/main/java/com/dotcms/auth/dotAuth/rest/handler/SamlProtocolHandler.java
+++ b/dotCMS/src/main/java/com/dotcms/auth/dotAuth/rest/handler/SamlProtocolHandler.java
@@ -2,6 +2,7 @@
import com.dotcms.auth.dotAuth.DotAuthConstants;
import com.dotcms.auth.dotAuth.rest.DotAuthProtocol;
+import com.dotcms.saml.DotIdentityProviderConfigurationImpl;
import com.dotcms.saml.DotSamlProxyFactory;
import com.dotcms.security.apps.AppSecrets;
import com.dotmarketing.util.UtilMethods;
@@ -9,6 +10,7 @@
import io.vavr.control.Try;
import java.util.HashMap;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
@@ -40,6 +42,8 @@ public final class SamlProtocolHandler implements ProtocolHandler {
private static final Set BOOLEAN_KEYS = Set.of("enable");
+ private static final String SIGNATURE_VALIDATION_TYPE_KEY = "signatureValidationType";
+
/**
* Extra-key names must be identifier-shaped. Bars whitespace tricks like
* {@code "privateKey "} (trailing space) that would otherwise slip past the
@@ -117,6 +121,8 @@ public AppSecrets buildSecrets(final Map incoming,
if (raw == null) continue;
if (BOOLEAN_KEYS.contains(key)) {
builder.withSecret(key, Boolean.parseBoolean(String.valueOf(raw)));
+ } else if (SIGNATURE_VALIDATION_TYPE_KEY.equals(key)) {
+ builder.withSecret(key, validSignatureValidationType(String.valueOf(raw)));
} else {
builder.withSecret(key, String.valueOf(raw));
if ("publicCert".equals(key) && UtilMethods.isSet(String.valueOf(raw))) {
@@ -175,4 +181,17 @@ public AppSecrets buildSecrets(final Map incoming,
}
return builder.build();
}
+
+ /**
+ * Only "response", "assertion" and "responseandassertion" can be stored: SAML responses
+ * are never accepted without an IdP signature.
+ */
+ private static String validSignatureValidationType(final String value) {
+ final String normalized = value.trim().toLowerCase(Locale.ROOT);
+ if (!normalized.equals(DotIdentityProviderConfigurationImpl.resolveSignatureValidationType(normalized))) {
+ throw new BadRequestException(SIGNATURE_VALIDATION_TYPE_KEY
+ + " must be one of response, assertion or responseandassertion");
+ }
+ return normalized;
+ }
}
diff --git a/dotCMS/src/main/java/com/dotcms/saml/DotAbstractSamlConfigurationServiceImpl.java b/dotCMS/src/main/java/com/dotcms/saml/DotAbstractSamlConfigurationServiceImpl.java
index d6d237e52d43..82081349f1a4 100644
--- a/dotCMS/src/main/java/com/dotcms/saml/DotAbstractSamlConfigurationServiceImpl.java
+++ b/dotCMS/src/main/java/com/dotcms/saml/DotAbstractSamlConfigurationServiceImpl.java
@@ -9,6 +9,7 @@
import java.nio.file.Files;
import java.util.Map;
import java.util.Properties;
+import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Supplier;
@@ -23,6 +24,8 @@ public abstract class DotAbstractSamlConfigurationServiceImpl implements SamlCon
private static final String NULL = "NULL";
private static final String UNABLE_TO_READ_FILE = "File does not exist or unable to read : ";
private static final String NOT_FOUND_ERROR = "Property Name not Found: ";
+ private static final Set SIGNATURE_CHECKS = Set.of(
+ SamlName.DOT_SAML_VERIFY_SIGNATURE_CREDENTIALS, SamlName.DOT_SAML_VERIFY_SIGNATURE_PROFILE);
private AtomicBoolean init = new AtomicBoolean(false);
private final Map defaultProperties = new ConcurrentHashMap<>();
@@ -73,7 +76,17 @@ private synchronized void internalInit(final Map contextMap) {
properties.forEach((key, value) -> {
final SamlName samlName = SamlName.findProperty((String)key);
- if (null != samlName) {
+ if (null == samlName) {
+
+ return;
+ }
+
+ if (SIGNATURE_CHECKS.contains(samlName)) {
+
+ // signature checks can't be switched off: keep the default (true)
+ Logger.warn(this, "Ignoring '" + key + "' in " + dotSamlDefaultPropertiesValue
+ + ": SAML signatures are always verified.");
+ } else {
this.defaultProperties.put(samlName.getPropertyName(), (String)value);
}
diff --git a/dotCMS/src/main/java/com/dotcms/saml/DotIdentityProviderConfigurationImpl.java b/dotCMS/src/main/java/com/dotcms/saml/DotIdentityProviderConfigurationImpl.java
index 50915a6a511b..88e3ca18261f 100644
--- a/dotCMS/src/main/java/com/dotcms/saml/DotIdentityProviderConfigurationImpl.java
+++ b/dotCMS/src/main/java/com/dotcms/saml/DotIdentityProviderConfigurationImpl.java
@@ -7,8 +7,12 @@
import com.dotmarketing.business.APILocator;
import com.dotmarketing.exception.DotDataException;
import com.dotmarketing.exception.DotSecurityException;
+import com.dotmarketing.util.Logger;
+import java.util.Locale;
import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
/**
* Default implementation to retrieve the configuration from apps
@@ -16,17 +20,82 @@
*/
public class DotIdentityProviderConfigurationImpl implements IdentityProviderConfiguration {
+ private static final Set SIGNATURE_VALIDATION_TYPES = Set.of(
+ DotSamlConstants.RESPONSE, DotSamlConstants.ASSERTION, DotSamlConstants.RESPONSE_AND_ASSERTION);
+
+ /**
+ * Extra parameters that used to switch off parts of the signature verification. Stored values are
+ * ignored, so the SAML bundle always uses its default (true).
+ */
+ private static final Set IGNORED_PROPERTIES = Set.of(
+ SamlName.DOT_SAML_VERIFY_SIGNATURE_CREDENTIALS.getPropertyName(),
+ SamlName.DOT_SAML_VERIFY_SIGNATURE_PROFILE.getPropertyName());
+
+ /**
+ * Warnings already logged, keyed by site and setting. A configuration is built for every request
+ * that needs it, so each warning is logged once per site and value rather than on every request.
+ */
+ private static final Set LOGGED_WARNINGS = ConcurrentHashMap.newKeySet();
+
private final AppsAPI appsAPI;
private final Host host;
private final AppSecrets appSecrets;
public DotIdentityProviderConfigurationImpl(final AppsAPI appsAPI, final Host host) throws DotSecurityException, DotDataException {
+ this(appsAPI, host, appsAPI.getSecrets(DotSamlProxyFactory.SAML_APP_CONFIG_KEY,
+ true, host, APILocator.systemUser()).get());
+ }
+
+ DotIdentityProviderConfigurationImpl(final AppsAPI appsAPI, final Host host, final AppSecrets appSecrets) {
+
this.appsAPI = appsAPI;
this.host = host;
- this.appSecrets = this.appsAPI.getSecrets(DotSamlProxyFactory.SAML_APP_CONFIG_KEY,
- true, host, APILocator.systemUser()).get();
+ this.appSecrets = appSecrets;
+
+ this.warnAboutOverriddenSettings();
+ }
+
+ /**
+ * Returns the signature validation type when it is "response", "assertion" or "responseandassertion"
+ * (ignoring case and surrounding blanks). Anything else, including blank and "none", fails closed to
+ * "responseandassertion": signatures can not be switched off through this setting.
+ *
+ * @param configured the stored value, may be null
+ * @return one of the supported validation types
+ */
+ public static String resolveSignatureValidationType(final String configured) {
+
+ final String normalized = null == configured ? "" : configured.trim().toLowerCase(Locale.ROOT);
+ return SIGNATURE_VALIDATION_TYPES.contains(normalized) ? normalized : DotSamlConstants.RESPONSE_AND_ASSERTION;
+ }
+ private void warnAboutOverriddenSettings() {
+
+ final Optional validationType =
+ this.findSecret(SamlName.DOT_SAML_SIGNATURE_VALIDATION_TYPE.getPropertyName());
+ final String configured = validationType.map(Secret::getString).orElse(null);
+ if ((null == configured || !SIGNATURE_VALIDATION_TYPES.contains(configured.trim().toLowerCase(Locale.ROOT)))
+ && firstTime("validationType=" + configured)) {
+
+ Logger.warn(this, "SAML configuration for site '" + this.host.getHostname() + "' has an unsupported "
+ + "signature validation type '" + configured + "'. A signed response and a signed assertion "
+ + "are required until a supported Validation Type is saved.");
+ }
+
+ for (final String ignoredProperty : IGNORED_PROPERTIES) {
+
+ if (this.findSecret(ignoredProperty).isPresent() && firstTime(ignoredProperty)) {
+
+ Logger.warn(this, "SAML configuration for site '" + this.host.getHostname() + "' sets '"
+ + ignoredProperty + "', which is ignored: SAML signatures are always verified.");
+ }
+ }
+ }
+
+ private boolean firstTime(final String warning) {
+
+ return LOGGED_WARNINGS.add(this.host.getIdentifier() + '|' + warning);
}
private Optional findSecret (final String key) {
@@ -78,7 +147,7 @@ public String getSignatureValidationType() {
final String signatureValidationTypeKey = SamlName.DOT_SAML_SIGNATURE_VALIDATION_TYPE.getPropertyName();
final Optional secretOpt = this.findSecret(signatureValidationTypeKey);
- return secretOpt.isPresent()? secretOpt.get().getString(): null;
+ return resolveSignatureValidationType(secretOpt.isPresent()? secretOpt.get().getString(): null);
}
@Override
@@ -111,6 +180,10 @@ public char[] getPrivateKey() {
@Override
public Object getOptionalProperty(final String propertyKey) {
+ if (IGNORED_PROPERTIES.contains(propertyKey)) {
+ return null;
+ }
+
final Optional secretOpt = this.findSecret(propertyKey);
return secretOpt.isPresent()? secretOpt.get().getString(): null;
}
@@ -119,7 +192,7 @@ public Object getOptionalProperty(final String propertyKey) {
@Override
public boolean containsOptionalProperty(final String propertyKey) {
- return this.findSecret(propertyKey).isPresent();
+ return !IGNORED_PROPERTIES.contains(propertyKey) && this.findSecret(propertyKey).isPresent();
}
@Override
diff --git a/dotCMS/src/main/resources/apps/dotsaml-config.yml b/dotCMS/src/main/resources/apps/dotsaml-config.yml
index a784c828dd16..af92a23ec578 100644
--- a/dotCMS/src/main/resources/apps/dotsaml-config.yml
+++ b/dotCMS/src/main/resources/apps/dotsaml-config.yml
@@ -37,13 +37,9 @@ params:
hidden: false
type: "SELECT"
label: "Validation Type"
- hint: "Depends on your IDP Configuration."
+ hint: "Select exactly the parts of the SAML Response your IDP signs. dotCMS rejects a response unless the selected parts carry a valid IDP signature."
required: true
value:
- -
- label: "None"
- value: "none"
- selected: true
-
label: "Only Response"
value: "response"
@@ -53,6 +49,7 @@ params:
-
label: "Response & Assertion"
value: "responseandassertion"
+ selected: true
idPMetadataFile:
value: ""
hidden: false
diff --git a/dotCMS/src/test/java/com/dotcms/auth/dotAuth/rest/handler/SamlProtocolHandlerTest.java b/dotCMS/src/test/java/com/dotcms/auth/dotAuth/rest/handler/SamlProtocolHandlerTest.java
index 15627034b2b9..769f526cfe71 100644
--- a/dotCMS/src/test/java/com/dotcms/auth/dotAuth/rest/handler/SamlProtocolHandlerTest.java
+++ b/dotCMS/src/test/java/com/dotcms/auth/dotAuth/rest/handler/SamlProtocolHandlerTest.java
@@ -211,4 +211,35 @@ public void buildSecrets_omits_previously_stored_undeclared_keys_not_resent() {
assertFalse(result.getSecrets().containsKey("emailAttribute"));
}
+
+ @Test
+ public void buildSecrets_rejects_signature_validation_type_none() {
+ assertThrows(javax.ws.rs.BadRequestException.class, () -> handler.buildSecrets(
+ Map.of("privateKey", "****", "publicCert", "stored-CERT", "signatureValidationType", "none"),
+ Optional.of(existingKeypair())));
+ }
+
+ @Test
+ public void buildSecrets_rejects_unknown_signature_validation_type() {
+ assertThrows(javax.ws.rs.BadRequestException.class, () -> handler.buildSecrets(
+ Map.of("privateKey", "****", "publicCert", "stored-CERT", "signatureValidationType", "signature"),
+ Optional.of(existingKeypair())));
+ }
+
+ @Test
+ public void buildSecrets_stores_supported_signature_validation_type_normalized() {
+ final AppSecrets result = handler.buildSecrets(
+ Map.of("privateKey", "****", "publicCert", "stored-CERT", "signatureValidationType", " Assertion "),
+ Optional.of(existingKeypair()));
+
+ assertEquals("assertion", result.getSecrets().get("signatureValidationType").getString());
+ }
+
+ private static AppSecrets existingKeypair() {
+ return AppSecrets.builder()
+ .withKey(DotSamlProxyFactory.SAML_APP_CONFIG_KEY)
+ .withHiddenSecret("privateKey", "stored-PEM")
+ .withSecret("publicCert", "stored-CERT")
+ .build();
+ }
}
diff --git a/dotCMS/src/test/java/com/dotcms/saml/DotAbstractSamlConfigurationServiceImplTest.java b/dotCMS/src/test/java/com/dotcms/saml/DotAbstractSamlConfigurationServiceImplTest.java
new file mode 100644
index 000000000000..b5d172538ee6
--- /dev/null
+++ b/dotCMS/src/test/java/com/dotcms/saml/DotAbstractSamlConfigurationServiceImplTest.java
@@ -0,0 +1,48 @@
+package com.dotcms.saml;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class DotAbstractSamlConfigurationServiceImplTest {
+
+ @Test
+ public void defaults_file_can_not_turn_signature_checks_off(@TempDir final Path tempDir) throws IOException {
+ final Path defaults = tempDir.resolve("dotcms-saml-default.properties");
+ Files.write(defaults, String.join("\n",
+ SamlName.DOT_SAML_VERIFY_SIGNATURE_CREDENTIALS.getPropertyName() + "=false",
+ SamlName.DOT_SAML_VERIFY_SIGNATURE_PROFILE.getPropertyName() + "=false",
+ SamlName.DOT_SAML_CLOCK_SKEW.getPropertyName() + "=5000",
+ "not.a.saml.property=whatever").getBytes(StandardCharsets.UTF_8));
+
+ final TestConfigurationService service = new TestConfigurationService();
+ service.initService(Map.of(SamlConfigurationService.DOT_SAML_DEFAULT_PROPERTIES_CONTEXT_MAP_KEY,
+ defaults.toString()));
+
+ assertTrue(service.getDefaultBooleanParameter(SamlName.DOT_SAML_VERIFY_SIGNATURE_CREDENTIALS));
+ assertTrue(service.getDefaultBooleanParameter(SamlName.DOT_SAML_VERIFY_SIGNATURE_PROFILE));
+ // other defaults from the file still apply
+ assertEquals("5000", service.getDefaultStringParameter(SamlName.DOT_SAML_CLOCK_SKEW));
+ }
+
+ private static final class TestConfigurationService extends DotAbstractSamlConfigurationServiceImpl {
+
+ @Override
+ public Map createInitialMap() {
+ final Map map = new HashMap<>();
+ map.put(SamlName.DOT_SAML_VERIFY_SIGNATURE_CREDENTIALS.getPropertyName(), "true");
+ map.put(SamlName.DOT_SAML_VERIFY_SIGNATURE_PROFILE.getPropertyName(), "true");
+ map.put(SamlName.DOT_SAML_CLOCK_SKEW.getPropertyName(), "10000");
+ return map;
+ }
+ }
+}
diff --git a/dotCMS/src/test/java/com/dotcms/saml/DotIdentityProviderConfigurationImplTest.java b/dotCMS/src/test/java/com/dotcms/saml/DotIdentityProviderConfigurationImplTest.java
new file mode 100644
index 000000000000..015e6692a97c
--- /dev/null
+++ b/dotCMS/src/test/java/com/dotcms/saml/DotIdentityProviderConfigurationImplTest.java
@@ -0,0 +1,107 @@
+package com.dotcms.saml;
+
+import com.dotcms.security.apps.AppSecrets;
+import com.dotmarketing.beans.Host;
+import org.junit.jupiter.api.Test;
+
+import java.util.UUID;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class DotIdentityProviderConfigurationImplTest {
+
+ @Test
+ public void supported_signature_validation_types_are_kept() {
+ assertEquals(DotSamlConstants.RESPONSE,
+ DotIdentityProviderConfigurationImpl.resolveSignatureValidationType("response"));
+ assertEquals(DotSamlConstants.ASSERTION,
+ DotIdentityProviderConfigurationImpl.resolveSignatureValidationType(" Assertion "));
+ assertEquals(DotSamlConstants.RESPONSE_AND_ASSERTION,
+ DotIdentityProviderConfigurationImpl.resolveSignatureValidationType("responseandassertion"));
+ }
+
+ @Test
+ public void unsupported_signature_validation_types_require_both_signatures() {
+ for (final String configured : new String[]{"none", "NONE", "", " ", null, "signature"}) {
+ assertEquals(DotSamlConstants.RESPONSE_AND_ASSERTION,
+ DotIdentityProviderConfigurationImpl.resolveSignatureValidationType(configured),
+ "type '" + configured + "'");
+ }
+ }
+
+ @Test
+ public void stored_none_is_read_as_response_and_assertion() {
+ final DotIdentityProviderConfigurationImpl config = config(secrets()
+ .withSecret("signatureValidationType", "none")
+ .build());
+
+ assertEquals(DotSamlConstants.RESPONSE_AND_ASSERTION, config.getSignatureValidationType());
+ }
+
+ @Test
+ public void signature_check_overrides_are_hidden_from_the_bundle() {
+ final DotIdentityProviderConfigurationImpl config = config(secrets()
+ .withSecret("signatureValidationType", "assertion")
+ .withSecret(SamlName.DOT_SAML_VERIFY_SIGNATURE_CREDENTIALS.getPropertyName(), "false")
+ .withSecret(SamlName.DOT_SAML_VERIFY_SIGNATURE_PROFILE.getPropertyName(), "false")
+ .build());
+
+ for (final SamlName ignored : new SamlName[]{SamlName.DOT_SAML_VERIFY_SIGNATURE_CREDENTIALS,
+ SamlName.DOT_SAML_VERIFY_SIGNATURE_PROFILE}) {
+ assertFalse(config.containsOptionalProperty(ignored.getPropertyName()), ignored.getPropertyName());
+ assertNull(config.getOptionalProperty(ignored.getPropertyName()), ignored.getPropertyName());
+ }
+ }
+
+ @Test
+ public void other_extra_parameters_still_reach_the_bundle() {
+ final DotIdentityProviderConfigurationImpl config = config(secrets()
+ .withSecret("signatureValidationType", "assertion")
+ .withSecret("allow.unsolicited.responses", "true")
+ .withSecret(SamlName.DOT_SAML_CLOCK_SKEW.getPropertyName(), "5000")
+ .build());
+
+ assertTrue(config.containsOptionalProperty("allow.unsolicited.responses"));
+ assertEquals("true", config.getOptionalProperty("allow.unsolicited.responses"));
+ assertTrue(config.containsOptionalProperty(SamlName.DOT_SAML_CLOCK_SKEW.getPropertyName()));
+ assertEquals("5000", config.getOptionalProperty(SamlName.DOT_SAML_CLOCK_SKEW.getPropertyName()));
+ assertFalse(config.containsOptionalProperty("not.stored"));
+ }
+
+ @Test
+ public void building_the_configuration_repeatedly_still_works_for_legacy_values() {
+ // the configuration is rebuilt per request; the warning is logged once, the behaviour never changes
+ final Host host = host();
+ final AppSecrets legacy = secrets()
+ .withSecret("signatureValidationType", "none")
+ .withSecret(SamlName.DOT_SAML_VERIFY_SIGNATURE_CREDENTIALS.getPropertyName(), "false")
+ .build();
+
+ for (int i = 0; i < 3; i++) {
+ final DotIdentityProviderConfigurationImpl config = new DotIdentityProviderConfigurationImpl(null, host, legacy);
+ assertEquals(DotSamlConstants.RESPONSE_AND_ASSERTION, config.getSignatureValidationType());
+ assertFalse(config.containsOptionalProperty(
+ SamlName.DOT_SAML_VERIFY_SIGNATURE_CREDENTIALS.getPropertyName()));
+ }
+ }
+
+ private static AppSecrets.Builder secrets() {
+ return AppSecrets.builder().withKey(DotSamlProxyFactory.SAML_APP_CONFIG_KEY);
+ }
+
+ private static DotIdentityProviderConfigurationImpl config(final AppSecrets appSecrets) {
+ return new DotIdentityProviderConfigurationImpl(null, host(), appSecrets);
+ }
+
+ private static Host host() {
+ final Host host = mock(Host.class);
+ when(host.getIdentifier()).thenReturn(UUID.randomUUID().toString());
+ when(host.getHostname()).thenReturn("demo.example.com");
+ return host;
+ }
+}