This is an automated email from the ASF dual-hosted git repository.

coheigea 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 6308498b8d9 Apply SAML SSO bearer assertion replay detection to all 
bindings (#3631)
6308498b8d9 is described below

commit 6308498b8d94cbbb3b282c2e83010a434e7b5edd
Author: Colm O hEigeartaigh <[email protected]>
AuthorDate: Mon Oct 26 17:01:57 2026 +0000

    Apply SAML SSO bearer assertion replay detection to all bindings (#3631)
---
 .../AbstractRequestAssertionConsumerHandler.java   |  4 +--
 .../saml/sso/SAMLSSOResponseValidator.java         | 16 ++++++----
 .../saml/sso/SAMLSSOResponseValidatorTest.java     | 37 ++++++++++++++++++++++
 3 files changed, 47 insertions(+), 10 deletions(-)

diff --git 
a/rt/rs/security/sso/saml/src/main/java/org/apache/cxf/rs/security/saml/sso/AbstractRequestAssertionConsumerHandler.java
 
b/rt/rs/security/sso/saml/src/main/java/org/apache/cxf/rs/security/saml/sso/AbstractRequestAssertionConsumerHandler.java
index 25a2731f517..aa0fde871e7 100644
--- 
a/rt/rs/security/sso/saml/src/main/java/org/apache/cxf/rs/security/saml/sso/AbstractRequestAssertionConsumerHandler.java
+++ 
b/rt/rs/security/sso/saml/src/main/java/org/apache/cxf/rs/security/saml/sso/AbstractRequestAssertionConsumerHandler.java
@@ -356,9 +356,7 @@ public abstract class 
AbstractRequestAssertionConsumerHandler extends AbstractSS
             
ssoResponseValidator.setEnforceAssertionsSigned(enforceAssertionsSigned);
             
ssoResponseValidator.setEnforceResponseSigned(enforceResponseSigned);
             ssoResponseValidator.setEnforceKnownIssuer(enforceKnownIssuer);
-            if (postBinding) {
-                ssoResponseValidator.setReplayCache(getReplayCache());
-            }
+            ssoResponseValidator.setReplayCache(getReplayCache());
 
             return ssoResponseValidator.validateSamlResponse(samlResponse, 
postBinding);
         } catch (Exception ex) {
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 598f29f04ff..889539e1aa0 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
@@ -78,7 +78,8 @@ public class SAMLSSOResponseValidator {
     /**
      * Validate a SAML 2 Protocol Response
      * @param samlResponse
-     * @param postBinding
+     * @param postBinding whether the Response was received via the POST 
binding. Replay detection
+     *                    of bearer assertions applies to all bindings, if a 
replay cache is configured.
      * @return a SSOValidatorResponse object
      * @throws WSSecurityException
      */
@@ -130,7 +131,7 @@ public class SAMLSSOResponseValidator {
                 && !assertion.getAuthnStatements().isEmpty()) {
                 org.opensaml.saml.saml2.core.Subject subject = 
assertion.getSubject();
                 org.opensaml.saml.saml2.core.SubjectConfirmation subjectConf =
-                    validateAuthenticationSubject(subject, assertion.getID(), 
postBinding);
+                    validateAuthenticationSubject(subject, assertion.getID());
                 if (subjectConf != null) {
                     
validateAudienceRestrictionCondition(assertion.getConditions());
                     validAssertion = assertion;
@@ -253,7 +254,7 @@ public class SAMLSSOResponseValidator {
      * Validate the Subject (of an Authentication Statement).
      */
     private org.opensaml.saml.saml2.core.SubjectConfirmation 
validateAuthenticationSubject(
-        org.opensaml.saml.saml2.core.Subject subject, String id, boolean 
postBinding
+        org.opensaml.saml.saml2.core.Subject subject, String id
     ) throws WSSecurityException {
         if (subject.getSubjectConfirmations() == null) {
             return null;
@@ -264,7 +265,7 @@ public class SAMLSSOResponseValidator {
         for (org.opensaml.saml.saml2.core.SubjectConfirmation subjectConf
             : subject.getSubjectConfirmations()) {
             if (SAML2Constants.CONF_BEARER.equals(subjectConf.getMethod())) {
-                
validateSubjectConfirmation(subjectConf.getSubjectConfirmationData(), id, 
postBinding);
+                
validateSubjectConfirmation(subjectConf.getSubjectConfirmationData(), id);
                 validSubjectConf = subjectConf;
             }
         }
@@ -276,7 +277,7 @@ public class SAMLSSOResponseValidator {
      * Validate a (Bearer) Subject Confirmation
      */
     private void validateSubjectConfirmation(
-        org.opensaml.saml.saml2.core.SubjectConfirmationData subjectConfData, 
String id, boolean postBinding
+        org.opensaml.saml.saml2.core.SubjectConfirmationData subjectConfData, 
String id
     ) throws WSSecurityException {
         if (subjectConfData == null) {
             LOG.warning("Subject Confirmation Data of a Bearer Subject 
Confirmation is null");
@@ -298,8 +299,9 @@ public class SAMLSSOResponseValidator {
             throw new 
WSSecurityException(WSSecurityException.ErrorCode.FAILURE, 
"invalidSAMLsecurity");
         }
 
-        // Need to keep bearer assertion IDs based on NotOnOrAfter to detect 
replay attacks
-        if (postBinding && replayCache != null) {
+        // Need to keep bearer assertion IDs based on NotOnOrAfter to detect 
replay attacks. This
+        // applies to every binding, not just POST, as a Response received via 
GET can be replayed too
+        if (replayCache != null) {
             Instant expires = subjectConfData.getNotOnOrAfter();
             if (!replayCache.putIdIfAbsent(id, expires)) {
                 LOG.warning("Replay attack with token id: " + id);
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 447c7459edc..5a033a1e8f3 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
@@ -701,6 +701,43 @@ public class SAMLSSOResponseValidatorTest {
         }
     }
 
+    @org.junit.Test
+    public void testReplayedRedirectBindingResponseIsRejected() 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);
+
+        TokenReplayCache<String> replayCache = new EHCacheTokenReplayCache();
+        try {
+            SAMLSSOResponseValidator validator = 
createReplayValidator(replayCache);
+
+            SSOValidatorResponse validateSamlResponse = 
validator.validateSamlResponse(response, false);
+            assertEquals(response.getID(), 
validateSamlResponse.getResponseId());
+
+            // The same (redirect binding) response is replayed
+            try {
+                validator.validateSamlResponse(copyResponse(response), false);
+                fail("Expected failure on replayed response");
+            } catch (WSSecurityException ex) {
+                // expected
+            }
+
+            // The same response is replayed via the POST binding
+            try {
+                validator.validateSamlResponse(copyResponse(response), true);
+                fail("Expected failure on replayed response");
+            } catch (WSSecurityException ex) {
+                // expected
+            }
+        } finally {
+            replayCache.close();
+        }
+    }
+
     // Two copies of the same response are validated concurrently, with the 
replay cache arranged so
     // that both threads look the assertion id up before either of them has 
stored it. Only one of
     // them may be accepted.

Reply via email to