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.