This is an automated email from the ASF dual-hosted git repository.
reta pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/cxf.git
The following commit(s) were added to refs/heads/main by this push:
new dd9af5c9ae4 match saml sso issuer exactly instead of by prefix (#3281)
dd9af5c9ae4 is described below
commit dd9af5c9ae419ac410bd308c49a3e09706232a30
Author: Javid Khan <[email protected]>
AuthorDate: Tue Jul 28 17:00:16 2026 +0530
match saml sso issuer exactly instead of by prefix (#3281)
* match saml sso issuer exactly instead of by prefix
* restrict saml sso issuer prefix match to same origin
keep prefix compatibility for the entityID-as-endpoint case but require
url-based issuer values to share the scheme, host and port of the
configured issuer idp, and add an enforceStrictIssuerMatch flag for
deployments that want exact matching only
Signed-off-by: Javid Khan <[email protected]>
---------
Signed-off-by: Javid Khan <[email protected]>
---
.../saml/sso/SAMLSSOResponseValidator.java | 68 +++++++++++++++++-
.../saml/sso/SAMLSSOResponseValidatorTest.java | 82 ++++++++++++++++++++++
2 files changed, 148 insertions(+), 2 deletions(-)
diff --git
a/rt/rs/security/sso/saml/src/main/java/org/apache/cxf/rs/security/saml/sso/SAMLSSOResponseValidator.java
b/rt/rs/security/sso/saml/src/main/java/org/apache/cxf/rs/security/saml/sso/SAMLSSOResponseValidator.java
index 85c0e0ede96..62da52cfe95 100644
---
a/rt/rs/security/sso/saml/src/main/java/org/apache/cxf/rs/security/saml/sso/SAMLSSOResponseValidator.java
+++
b/rt/rs/security/sso/saml/src/main/java/org/apache/cxf/rs/security/saml/sso/SAMLSSOResponseValidator.java
@@ -18,6 +18,7 @@
*/
package org.apache.cxf.rs.security.saml.sso;
+import java.net.URI;
import java.time.Instant;
import java.util.List;
import java.util.logging.Logger;
@@ -47,6 +48,7 @@ public class SAMLSSOResponseValidator {
private boolean enforceResponseSigned;
private boolean enforceAssertionsSigned = true;
private boolean enforceKnownIssuer = true;
+ private boolean enforceStrictIssuerMatch;
private TokenReplayCache<String> replayCache;
/**
@@ -64,6 +66,15 @@ public class SAMLSSOResponseValidator {
this.enforceKnownIssuer = enforceKnownIssuer;
}
+ /**
+ * Require the Issuer of the received Response/Assertion to match the
configured Issuer IDP
+ * exactly, rather than allowing a same-origin prefix. The default is
false, which keeps
+ * backwards compatibility for deployments whose entityID is a prefix of
the configured value.
+ */
+ public void setEnforceStrictIssuerMatch(boolean enforceStrictIssuerMatch) {
+ this.enforceStrictIssuerMatch = enforceStrictIssuerMatch;
+ }
+
/**
* Validate a SAML 2 Protocol Response
* @param samlResponse
@@ -169,8 +180,8 @@ public class SAMLSSOResponseValidator {
return;
}
- // Issuer value must match (be contained in) Issuer IDP
- if (enforceKnownIssuer && (issuer.getValue() == null ||
!issuerIDP.startsWith(issuer.getValue()))) {
+ // Issuer value must match the configured Issuer IDP
+ if (enforceKnownIssuer && !matchesKnownIssuer(issuer.getValue())) {
LOG.warning("Issuer value: " + issuer.getValue() + " does not
match issuer IDP: "
+ issuerIDP);
throw new
WSSecurityException(WSSecurityException.ErrorCode.FAILURE,
"invalidSAMLsecurity");
@@ -185,6 +196,59 @@ public class SAMLSSOResponseValidator {
}
}
+ private boolean matchesKnownIssuer(String issuerValue) {
+ if (issuerValue == null || issuerIDP == null) {
+ return false;
+ }
+
+ if (issuerIDP.equals(issuerValue)) {
+ return true;
+ }
+
+ // Strict matching only accepts an exact match with the configured
Issuer IDP
+ if (enforceStrictIssuerMatch || !issuerIDP.startsWith(issuerValue)) {
+ return false;
+ }
+
+ // Keep prefix compatibility, but for URL-based IdP values require a
URL-based issuer on
+ // the same scheme/host/port to avoid accepting arbitrary short
prefixes.
+ URI issuerIdpUri = toUri(issuerIDP);
+ if (isHierarchicalAbsoluteUri(issuerIdpUri)) {
+ URI issuerUri = toUri(issuerValue);
+ return isHierarchicalAbsoluteUri(issuerUri)
+ &&
issuerIdpUri.getScheme().equalsIgnoreCase(issuerUri.getScheme())
+ && issuerIdpUri.getHost().equalsIgnoreCase(issuerUri.getHost())
+ && getEffectivePort(issuerIdpUri) ==
getEffectivePort(issuerUri);
+ }
+
+ return true;
+ }
+
+ private URI toUri(String value) {
+ try {
+ return URI.create(value);
+ } catch (IllegalArgumentException ex) {
+ return null;
+ }
+ }
+
+ private boolean isHierarchicalAbsoluteUri(URI uri) {
+ return uri != null && uri.isAbsolute() && uri.getHost() != null;
+ }
+
+ private int getEffectivePort(URI uri) {
+ if (uri.getPort() != -1) {
+ return uri.getPort();
+ }
+ if ("http".equalsIgnoreCase(uri.getScheme())) {
+ return 80;
+ }
+ if ("https".equalsIgnoreCase(uri.getScheme())) {
+ return 443;
+ }
+ return -1;
+ }
+
/**
* Validate the Subject (of an Authentication Statement).
*/
diff --git
a/rt/rs/security/sso/saml/src/test/java/org/apache/cxf/rs/security/saml/sso/SAMLSSOResponseValidatorTest.java
b/rt/rs/security/sso/saml/src/test/java/org/apache/cxf/rs/security/saml/sso/SAMLSSOResponseValidatorTest.java
index 1bf6b656c03..9bba9c4e189 100644
---
a/rt/rs/security/sso/saml/src/test/java/org/apache/cxf/rs/security/saml/sso/SAMLSSOResponseValidatorTest.java
+++
b/rt/rs/security/sso/saml/src/test/java/org/apache/cxf/rs/security/saml/sso/SAMLSSOResponseValidatorTest.java
@@ -324,6 +324,88 @@ public class SAMLSSOResponseValidatorTest {
}
}
+ @org.junit.Test
+ public void testResponseIssuerShortPrefixOfConfiguredIssuer() throws
Exception {
+ SubjectConfirmationDataBean subjectConfirmationData = new
SubjectConfirmationDataBean();
+ subjectConfirmationData.setAddress("http://apache.org");
+ subjectConfirmationData.setInResponseTo("12345");
+
subjectConfirmationData.setNotAfter(Instant.now().plus(Duration.ofMinutes(5)));
+ subjectConfirmationData.setRecipient("http://recipient.apache.org");
+
+ Response response = createResponse(subjectConfirmationData);
+ // An arbitrary short prefix on a different host must not be accepted
+
response.setIssuer(SAML2PResponseComponentBuilder.createIssuer("http://cxf"));
+
+ // Validate the Response
+ SAMLSSOResponseValidator validator = new SAMLSSOResponseValidator();
+ validator.setEnforceAssertionsSigned(false);
+ validator.setIssuerIDP("http://cxf.apache.org/issuer");
+ validator.setAssertionConsumerURL("http://recipient.apache.org");
+ validator.setClientAddress("http://apache.org");
+ validator.setRequestId("12345");
+ validator.setSpIdentifier("http://service.apache.org");
+ try {
+ validator.validateSamlResponse(response, false);
+ fail("Expected failure on issuer that only matches a short prefix
of the configured issuer");
+ } catch (WSSecurityException ex) {
+ // expected
+ }
+ }
+
+ @org.junit.Test
+ public void testResponseIssuerSameOriginPrefixAccepted() throws Exception {
+ SubjectConfirmationDataBean subjectConfirmationData = new
SubjectConfirmationDataBean();
+ subjectConfirmationData.setAddress("http://apache.org");
+ subjectConfirmationData.setInResponseTo("12345");
+
subjectConfirmationData.setNotAfter(Instant.now().plus(Duration.ofMinutes(5)));
+ subjectConfirmationData.setRecipient("http://recipient.apache.org");
+
+ Response response = createResponse(subjectConfirmationData);
+ // A prefix on the same scheme/host/port is the legitimate
entityID-of-endpoint case
+
response.setIssuer(SAML2PResponseComponentBuilder.createIssuer("http://cxf.apache.org"));
+
+ // Validate the Response
+ SAMLSSOResponseValidator validator = new SAMLSSOResponseValidator();
+ validator.setEnforceAssertionsSigned(false);
+ validator.setIssuerIDP("http://cxf.apache.org/issuer");
+ validator.setAssertionConsumerURL("http://recipient.apache.org");
+ validator.setClientAddress("http://apache.org");
+ validator.setRequestId("12345");
+ validator.setSpIdentifier("http://service.apache.org");
+
+ SSOValidatorResponse validateSamlResponse =
validator.validateSamlResponse(response, false);
+ assertEquals(response.getID(), validateSamlResponse.getResponseId());
+ }
+
+ @org.junit.Test
+ public void testResponseIssuerStrictMatchRejectsPrefix() throws Exception {
+ SubjectConfirmationDataBean subjectConfirmationData = new
SubjectConfirmationDataBean();
+ subjectConfirmationData.setAddress("http://apache.org");
+ subjectConfirmationData.setInResponseTo("12345");
+
subjectConfirmationData.setNotAfter(Instant.now().plus(Duration.ofMinutes(5)));
+ subjectConfirmationData.setRecipient("http://recipient.apache.org");
+
+ Response response = createResponse(subjectConfirmationData);
+ // Same-origin prefix, but strict matching must require an exact match
+
response.setIssuer(SAML2PResponseComponentBuilder.createIssuer("http://cxf.apache.org"));
+
+ // Validate the Response
+ SAMLSSOResponseValidator validator = new SAMLSSOResponseValidator();
+ validator.setEnforceAssertionsSigned(false);
+ validator.setEnforceStrictIssuerMatch(true);
+ validator.setIssuerIDP("http://cxf.apache.org/issuer");
+ validator.setAssertionConsumerURL("http://recipient.apache.org");
+ validator.setClientAddress("http://apache.org");
+ validator.setRequestId("12345");
+ validator.setSpIdentifier("http://service.apache.org");
+ try {
+ validator.validateSamlResponse(response, false);
+ fail("Expected failure on prefix issuer when strict matching is
enforced");
+ } catch (WSSecurityException ex) {
+ // expected
+ }
+ }
+
@org.junit.Test
public void testMissingAuthnStatement() throws Exception {
SubjectConfirmationDataBean subjectConfirmationData = new
SubjectConfirmationDataBean();