This is an automated email from the ASF dual-hosted git repository. coheigea pushed a commit to branch coheigea/saml-get-replay in repository https://gitbox.apache.org/repos/asf/cxf.git
commit c22a43cee9f5f0a151ee2663986ff0dd147aad88 Author: Colm O hEigeartaigh <[email protected]> AuthorDate: Mon Oct 26 08:35:11 2026 +0000 Apply SAML SSO bearer assertion replay detection to all bindings --- .../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.
