This is an automated email from the ASF dual-hosted git repository. coheigea pushed a commit to branch 2_4_x-fixes in repository https://gitbox.apache.org/repos/asf/ws-wss4j.git
commit dc72a6adb77e6e55f6a38ef6387cbfe43139d0c4 Author: Colm O hEigeartaigh <[email protected]> AuthorDate: Mon Sep 21 11:46:08 2026 +0100 Fix SAML Conditions parsing (#721) --- .../wss4j/common/saml/SamlAssertionWrapper.java | 17 ++-- .../apache/wss4j/dom/saml/SamlConditionsTest.java | 104 +++++++++++++++++++++ 2 files changed, 115 insertions(+), 6 deletions(-) diff --git a/ws-security-common/src/main/java/org/apache/wss4j/common/saml/SamlAssertionWrapper.java b/ws-security-common/src/main/java/org/apache/wss4j/common/saml/SamlAssertionWrapper.java index c1f3471f6..4a3037cad 100644 --- a/ws-security-common/src/main/java/org/apache/wss4j/common/saml/SamlAssertionWrapper.java +++ b/ws-security-common/src/main/java/org/apache/wss4j/common/saml/SamlAssertionWrapper.java @@ -840,14 +840,19 @@ public class SamlAssertionWrapper { DateTime issueInstant = null; DateTime validTill = null; - if (getSamlVersion().equals(SAMLVersion.VERSION_20) - && getSaml2().getConditions() != null) { - validTill = getSaml2().getConditions().getNotOnOrAfter(); + // The IssueInstant is read whether or not the assertion carries any Conditions at all. + // It is the fallback bound on the assertion's lifetime, so reading it only when there + // are Conditions would skip that bound in precisely the case it exists for. + if (getSamlVersion().equals(SAMLVersion.VERSION_20)) { issueInstant = getSaml2().getIssueInstant(); - } else if (getSamlVersion().equals(SAMLVersion.VERSION_11) - && getSaml1().getConditions() != null) { - validTill = getSaml1().getConditions().getNotOnOrAfter(); + if (getSaml2().getConditions() != null) { + validTill = getSaml2().getConditions().getNotOnOrAfter(); + } + } else if (getSamlVersion().equals(SAMLVersion.VERSION_11)) { issueInstant = getSaml1().getIssueInstant(); + if (getSaml1().getConditions() != null) { + validTill = getSaml1().getConditions().getNotOnOrAfter(); + } } // Check the IssueInstant is not in the future, subject to the future TTL diff --git a/ws-security-dom/src/test/java/org/apache/wss4j/dom/saml/SamlConditionsTest.java b/ws-security-dom/src/test/java/org/apache/wss4j/dom/saml/SamlConditionsTest.java index 6a23fd2ee..0fdb11f87 100644 --- a/ws-security-dom/src/test/java/org/apache/wss4j/dom/saml/SamlConditionsTest.java +++ b/ws-security-dom/src/test/java/org/apache/wss4j/dom/saml/SamlConditionsTest.java @@ -283,6 +283,76 @@ public class SamlConditionsTest { verify(unsignedDoc); } + /** + * An assertion with no Conditions element at all has no NotOnOrAfter, so the TTL on the + * IssueInstant is the only bound on its lifetime that exists. It has to be applied, or such + * an assertion is good forever. + */ + @Test + public void testSAML2StaleIssueInstantWithNoConditions() throws Exception { + SAML2CallbackHandler callbackHandler = new SAML2CallbackHandler(); + callbackHandler.setStatement(SAML2CallbackHandler.Statement.AUTHN); + callbackHandler.setIssuer("www.example.com"); + + SAMLCallback samlCallback = new SAMLCallback(); + SAMLUtil.doSAMLCallback(callbackHandler, samlCallback); + SamlAssertionWrapper samlAssertion = new SamlAssertionWrapper(samlCallback); + + DateTime issueInstant = new DateTime().minusMinutes(31); + samlAssertion.getSaml2().setIssueInstant(issueInstant); + samlAssertion.getSaml2().setConditions(null); + + Document doc = SOAPUtil.toSOAPPart(SOAPUtil.SAMPLE_SOAP_MSG); + WSSecHeader secHeader = new WSSecHeader(doc); + secHeader.insertSecurityHeader(); + + WSSecSAMLToken wsSign = new WSSecSAMLToken(secHeader); + + Document unsignedDoc = wsSign.build(samlAssertion); + + if (LOG.isDebugEnabled()) { + LOG.debug("SAML 2 Authn Assertion (sender vouches):"); + String outputString = + XMLUtils.prettyDocumentToString(unsignedDoc); + LOG.debug(outputString); + } + + try { + verify(unsignedDoc); + fail("Failure expected in processing a stale SAML Assertion"); + } catch (WSSecurityException ex) { + assertTrue(ex.getMessage().contains("SAML token security failure")); + } + } + + /** + * The same assertion within the TTL is accepted: an assertion is not required to carry + * Conditions, only to be recent when it does not. + */ + @Test + public void testSAML2FreshIssueInstantWithNoConditions() throws Exception { + SAML2CallbackHandler callbackHandler = new SAML2CallbackHandler(); + callbackHandler.setStatement(SAML2CallbackHandler.Statement.AUTHN); + callbackHandler.setIssuer("www.example.com"); + + SAMLCallback samlCallback = new SAMLCallback(); + SAMLUtil.doSAMLCallback(callbackHandler, samlCallback); + SamlAssertionWrapper samlAssertion = new SamlAssertionWrapper(samlCallback); + + samlAssertion.getSaml2().setIssueInstant(new DateTime().minusSeconds(5)); + samlAssertion.getSaml2().setConditions(null); + + Document doc = SOAPUtil.toSOAPPart(SOAPUtil.SAMPLE_SOAP_MSG); + WSSecHeader secHeader = new WSSecHeader(doc); + secHeader.insertSecurityHeader(); + + WSSecSAMLToken wsSign = new WSSecSAMLToken(secHeader); + + Document unsignedDoc = wsSign.build(samlAssertion); + + verify(unsignedDoc); + } + @Test public void testSAML1StaleIssueInstant() throws Exception { SAML1CallbackHandler callbackHandler = new SAML1CallbackHandler(); @@ -321,6 +391,40 @@ public class SamlConditionsTest { } } + /** + * The SAML 1.1 counterpart: no Conditions element, so the IssueInstant TTL is the only + * bound there is. + */ + @Test + public void testSAML1StaleIssueInstantWithNoConditions() throws Exception { + SAML1CallbackHandler callbackHandler = new SAML1CallbackHandler(); + callbackHandler.setStatement(SAML1CallbackHandler.Statement.AUTHN); + callbackHandler.setIssuer("www.example.com"); + + SAMLCallback samlCallback = new SAMLCallback(); + SAMLUtil.doSAMLCallback(callbackHandler, samlCallback); + SamlAssertionWrapper samlAssertion = new SamlAssertionWrapper(samlCallback); + + DateTime issueInstant = new DateTime().minusMinutes(31); + samlAssertion.getSaml1().setIssueInstant(issueInstant); + samlAssertion.getSaml1().setConditions(null); + + Document doc = SOAPUtil.toSOAPPart(SOAPUtil.SAMPLE_SOAP_MSG); + WSSecHeader secHeader = new WSSecHeader(doc); + secHeader.insertSecurityHeader(); + + WSSecSAMLToken wsSign = new WSSecSAMLToken(secHeader); + + Document unsignedDoc = wsSign.build(samlAssertion); + + try { + verify(unsignedDoc); + fail("Failure expected in processing a stale SAML Assertion"); + } catch (WSSecurityException ex) { + assertTrue(ex.getMessage().contains("SAML token security failure")); + } + } + /** * Test that creates, sends and processes an unsigned SAML 2 authentication assertion * with an (invalid) custom Conditions statement.
