This is an automated email from the ASF dual-hosted git repository. coheigea pushed a commit to branch coheigea/WSS-713 in repository https://gitbox.apache.org/repos/asf/ws-wss4j.git
commit 1fe85b5276c85febd0032d1838ce74e0edf9e3d6 Author: Colm O hEigeartaigh <[email protected]> AuthorDate: Mon Oct 12 14:13:29 2026 +0100 WSS-713 - Support SubjectConfirmationData for SAML-SV v1 --- .../org/apache/wss4j/common/saml/OpenSAMLUtil.java | 33 +++++++ .../saml/bean/SubjectConfirmationDataBean.java | 2 +- .../common/saml/builder/SAML1ComponentBuilder.java | 66 +++++++++++++ .../common/saml/builder/SAML2ComponentBuilder.java | 2 +- .../wss4j/dom/common/SAML1CallbackHandler.java | 1 + .../org/apache/wss4j/dom/saml/SamlTokenTest.java | 110 +++++++++++++++++++++ 6 files changed, 212 insertions(+), 2 deletions(-) diff --git a/ws-security-common/src/main/java/org/apache/wss4j/common/saml/OpenSAMLUtil.java b/ws-security-common/src/main/java/org/apache/wss4j/common/saml/OpenSAMLUtil.java index 4fca170bd..368d3bac4 100644 --- a/ws-security-common/src/main/java/org/apache/wss4j/common/saml/OpenSAMLUtil.java +++ b/ws-security-common/src/main/java/org/apache/wss4j/common/saml/OpenSAMLUtil.java @@ -41,6 +41,7 @@ import org.opensaml.core.xml.io.MarshallingException; import org.opensaml.core.xml.io.Unmarshaller; import org.opensaml.core.xml.io.UnmarshallerFactory; import org.opensaml.core.xml.io.UnmarshallingException; +import org.opensaml.core.xml.util.XMLObjectSupport; import org.opensaml.saml.common.SignableSAMLObject; import org.opensaml.saml.config.SAMLConfiguration; import org.opensaml.xmlsec.config.DecryptionParserPool; @@ -162,6 +163,38 @@ public final class OpenSAMLUtil { } } + /** + * An XMLObject can only have one parent. Return the given XMLObject if it has no parent, + * otherwise return a copy of it, for example if the same bean is used to create more than + * one SAML assertion. Elements without a registered unmarshaller are copied using the + * default provider (XSAny). + * + * @param xmlObject the XMLObject to add to a new parent + * @return the XMLObject, or a copy of it if it already has a parent + * @throws WSSecurityException if the XMLObject could not be copied + */ + public static XMLObject cloneIfParented(XMLObject xmlObject) throws WSSecurityException { + if (xmlObject.getParent() == null) { + return xmlObject; + } + try { + Element element = XMLObjectSupport.marshall(xmlObject); + UnmarshallerFactory factory = XMLObjectProviderRegistrySupport.getUnmarshallerFactory(); + Unmarshaller unmarshaller = factory.getUnmarshaller(element); + if (unmarshaller == null) { + unmarshaller = factory.getUnmarshaller(XMLObjectProviderRegistrySupport.getDefaultProviderQName()); + } + XMLObject clone = unmarshaller.unmarshall(element); + // Detach the copy from the DOM of the original object + clone.releaseDOM(); + clone.releaseChildrenDOM(true); + return clone; + } catch (MarshallingException | UnmarshallingException ex) { + throw new WSSecurityException(WSSecurityException.ErrorCode.FAILURE, ex, "empty", + new Object[] {"Error copying an XMLObject"}); + } + } + /** * Convert a SAML Assertion from a XMLObject to a DOM Element * diff --git a/ws-security-common/src/main/java/org/apache/wss4j/common/saml/bean/SubjectConfirmationDataBean.java b/ws-security-common/src/main/java/org/apache/wss4j/common/saml/bean/SubjectConfirmationDataBean.java index 746a6343c..85056ab96 100644 --- a/ws-security-common/src/main/java/org/apache/wss4j/common/saml/bean/SubjectConfirmationDataBean.java +++ b/ws-security-common/src/main/java/org/apache/wss4j/common/saml/bean/SubjectConfirmationDataBean.java @@ -25,7 +25,7 @@ import java.util.Date; import java.util.List; /** - * Class SubjectConfirmationDataBean represents a SAML (2) SubjectConfirmationData. Please note that + * Class SubjectConfirmationDataBean represents a SAML SubjectConfirmationData. Please note that * KeyInfo functionality is in SubjectBean for backwards compatibility reasons. */ public class SubjectConfirmationDataBean { diff --git a/ws-security-common/src/main/java/org/apache/wss4j/common/saml/builder/SAML1ComponentBuilder.java b/ws-security-common/src/main/java/org/apache/wss4j/common/saml/builder/SAML1ComponentBuilder.java index 44f24ff9e..9b728d96c 100644 --- a/ws-security-common/src/main/java/org/apache/wss4j/common/saml/builder/SAML1ComponentBuilder.java +++ b/ws-security-common/src/main/java/org/apache/wss4j/common/saml/builder/SAML1ComponentBuilder.java @@ -36,15 +36,19 @@ import org.apache.wss4j.common.saml.bean.AuthenticationStatementBean; import org.apache.wss4j.common.saml.bean.ConditionsBean; import org.apache.wss4j.common.saml.bean.KeyInfoBean; import org.apache.wss4j.common.saml.bean.SubjectBean; +import org.apache.wss4j.common.saml.bean.SubjectConfirmationDataBean; import org.apache.wss4j.common.saml.bean.SubjectLocalityBean; import org.apache.xml.security.stax.impl.util.IDGenerator; import org.opensaml.core.xml.XMLObject; import org.opensaml.core.xml.XMLObjectBuilderFactory; import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport; +import org.opensaml.core.xml.schema.XSAny; import org.opensaml.core.xml.schema.XSString; +import org.opensaml.core.xml.schema.impl.XSAnyBuilder; import org.opensaml.core.xml.schema.impl.XSStringBuilder; import org.opensaml.saml.common.SAMLObjectBuilder; import org.opensaml.saml.common.SAMLVersion; +import org.opensaml.saml.common.xml.SAMLConstants; import org.opensaml.saml.saml1.core.Action; import org.opensaml.saml.saml1.core.Advice; import org.opensaml.saml.saml1.core.Assertion; @@ -71,12 +75,20 @@ import org.opensaml.xmlsec.keyinfo.impl.X509KeyInfoGeneratorFactory; import org.opensaml.xmlsec.signature.KeyInfo; import org.w3c.dom.Element; +import javax.xml.namespace.QName; + /** * Class SAML1ComponentBuilder provides builder methods that can be used * to construct SAML v1.1 statements using the OpenSaml library. */ public final class SAML1ComponentBuilder { + private static final org.slf4j.Logger LOG = + org.slf4j.LoggerFactory.getLogger(SAML1ComponentBuilder.class); + + private static final QName SUBJECT_CONFIRMATION_DATA_ELEMENT_NAME = + new QName(SAMLConstants.SAML1_NS, "SubjectConfirmationData", SAMLConstants.SAML1_PREFIX); + private static volatile SAMLObjectBuilder<Assertion> assertionV1Builder; private static volatile SAMLObjectBuilder<Conditions> conditionsV1Builder; @@ -107,6 +119,8 @@ public final class SAML1ComponentBuilder { private static volatile XSStringBuilder stringBuilder; + private static volatile XSAnyBuilder xsAnyBuilder; + private static volatile SAMLObjectBuilder<AuthorizationDecisionStatement> authorizationDecisionStatementV1Builder; @@ -199,12 +213,64 @@ public final class SAML1ComponentBuilder { KeyInfo keyInfo = createKeyInfo(subjectBean.getKeyInfo()); subjectConfirmation.setKeyInfo(keyInfo); } + if (subjectBean.getSubjectConfirmationData() != null) { + subjectConfirmation.setSubjectConfirmationData( + createSubjectConfirmationData(subjectBean.getSubjectConfirmationData()) + ); + } subject.setNameIdentifier(nameIdentifier); subject.setSubjectConfirmation(subjectConfirmation); return subject; } + /** + * Create a SAML 1.1 SubjectConfirmationData object. In SAML 1.1 the SubjectConfirmationData + * element is of type xs:anyType and has no defined attributes, so only the additional + * XMLObjects of the bean ("any") are added as child elements. The SAML 2.0 specific values + * (Recipient, Address, InResponseTo, NotBefore, NotOnOrAfter) are not supported and ignored. + * + * @param subjectConfirmationDataBean of type SubjectConfirmationDataBean + * @return a SubjectConfirmationData object, or null if there is no content to add + * @throws WSSecurityException if an XMLObject could not be cloned + */ + public static XSAny createSubjectConfirmationData( + SubjectConfirmationDataBean subjectConfirmationDataBean + ) throws WSSecurityException { + if (subjectConfirmationDataBean.getInResponseTo() != null + || subjectConfirmationDataBean.getRecipient() != null + || subjectConfirmationDataBean.getAddress() != null + || subjectConfirmationDataBean.getNotBefore() != null + || subjectConfirmationDataBean.getNotAfter() != null) { + LOG.warn("Ignore the SAML 2.0 specific values of the SubjectConfirmationData for a SAML 1.1 token"); + } + + List<XMLObject> unknownObjects = new ArrayList<>(); + if (subjectConfirmationDataBean.getAny() != null) { + for (Object obj : subjectConfirmationDataBean.getAny()) { + if (obj == null) { + LOG.warn("Ignore <null> object in SubjectConfirmationData.any"); + } else if (obj instanceof XMLObject) { + unknownObjects.add(OpenSAMLUtil.cloneIfParented((XMLObject) obj)); + } else { + LOG.warn("Ignore object of the unsupported type {} in SubjectConfirmationData.any", + obj.getClass()); + } + } + } + if (unknownObjects.isEmpty()) { + return null; + } + + if (xsAnyBuilder == null) { + xsAnyBuilder = new XSAnyBuilder(); + } + XSAny subjectConfirmationData = xsAnyBuilder.buildObject(SUBJECT_CONFIRMATION_DATA_ELEMENT_NAME); + subjectConfirmationData.getUnknownXMLObjects().addAll(unknownObjects); + + return subjectConfirmationData; + } + /** * Create an Opensaml KeyInfo object from the parameters * @param keyInfo the KeyInfo bean from which to extract security credentials diff --git a/ws-security-common/src/main/java/org/apache/wss4j/common/saml/builder/SAML2ComponentBuilder.java b/ws-security-common/src/main/java/org/apache/wss4j/common/saml/builder/SAML2ComponentBuilder.java index bff03d14d..b146982a2 100644 --- a/ws-security-common/src/main/java/org/apache/wss4j/common/saml/builder/SAML2ComponentBuilder.java +++ b/ws-security-common/src/main/java/org/apache/wss4j/common/saml/builder/SAML2ComponentBuilder.java @@ -658,7 +658,7 @@ public final class SAML2ComponentBuilder { if (obj == null) { LOG.warn("Ignore <null> object in SubjectConfirmationData.any"); } else if (obj instanceof XMLObject) { - unknownObjects.add((XMLObject) obj); + unknownObjects.add(OpenSAMLUtil.cloneIfParented((XMLObject) obj)); } else if (obj instanceof AttributeStatementBean) { unknownObjects.addAll(createAttributeStatement(Collections.singletonList((AttributeStatementBean) obj))); } else { diff --git a/ws-security-dom/src/test/java/org/apache/wss4j/dom/common/SAML1CallbackHandler.java b/ws-security-dom/src/test/java/org/apache/wss4j/dom/common/SAML1CallbackHandler.java index 03daec925..f41563e22 100644 --- a/ws-security-dom/src/test/java/org/apache/wss4j/dom/common/SAML1CallbackHandler.java +++ b/ws-security-dom/src/test/java/org/apache/wss4j/dom/common/SAML1CallbackHandler.java @@ -81,6 +81,7 @@ public class SAML1CallbackHandler extends AbstractSAMLCallbackHandler { if (subjectNameIDFormat != null) { subjectBean.setSubjectNameIDFormat(subjectNameIDFormat); } + subjectBean.setSubjectConfirmationData(subjectConfirmationData); if (SAML1Constants.CONF_HOLDER_KEY.equals(confirmationMethod)) { try { KeyInfoBean keyInfo = createKeyInfo(); diff --git a/ws-security-dom/src/test/java/org/apache/wss4j/dom/saml/SamlTokenTest.java b/ws-security-dom/src/test/java/org/apache/wss4j/dom/saml/SamlTokenTest.java index 5acea98e1..4e3069cfb 100644 --- a/ws-security-dom/src/test/java/org/apache/wss4j/dom/saml/SamlTokenTest.java +++ b/ws-security-dom/src/test/java/org/apache/wss4j/dom/saml/SamlTokenTest.java @@ -33,6 +33,7 @@ import java.util.List; import javax.crypto.KeyGenerator; import javax.crypto.SecretKey; import javax.security.auth.callback.CallbackHandler; +import javax.xml.namespace.QName; import javax.xml.parsers.DocumentBuilderFactory; import org.apache.commons.xml.secure.SecureDocumentBuilderFactory; @@ -671,6 +672,61 @@ public class SamlTokenTest { assertFalse(receivedSamlAssertion.isSigned()); } + /** + * Test that creates, sends and processes an unsigned SAML 1.1 authentication assertion with + * SubjectConfirmationData information. + */ + @Test + public void testSAML1SubjectConfirmationData() throws Exception { + SAML1CallbackHandler callbackHandler = new SAML1CallbackHandler(); + callbackHandler.setStatement(SAML1CallbackHandler.Statement.AUTHN); + callbackHandler.setIssuer("www.example.com"); + + XMLObjectBuilderFactory builderFactory = XMLObjectProviderRegistrySupport.getBuilderFactory(); + @SuppressWarnings("unchecked") + XMLObjectBuilder<XSAny> xsAnyBuilder = + (XMLObjectBuilder<XSAny>)builderFactory.getBuilder(XSAny.TYPE_NAME); + XSAny customData = xsAnyBuilder.buildObject(new QName("urn:example:custom", "CustomData", "ex")); + customData.setTextContent("custom-confirmation-data"); + + SubjectConfirmationDataBean subjectConfirmationData = new SubjectConfirmationDataBean(); + // SAML 2.0 specific values must not be written to a SAML 1.1 token + subjectConfirmationData.setRecipient("http://recipient.apache.org"); + subjectConfirmationData.addAny(customData); + callbackHandler.setSubjectConfirmationData(subjectConfirmationData); + + SAMLCallback samlCallback = new SAMLCallback(); + SAMLUtil.doSAMLCallback(callbackHandler, samlCallback); + SamlAssertionWrapper samlAssertion = new SamlAssertionWrapper(samlCallback); + + 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); + + String outputString = + XMLUtils.prettyDocumentToString(unsignedDoc); + if (LOG.isDebugEnabled()) { + LOG.debug("SAML 1.1 Authn Assertion (sender vouches):"); + LOG.debug(outputString); + } + assertTrue(outputString.contains("SubjectConfirmationData")); + assertTrue(outputString.contains("custom-confirmation-data")); + assertFalse(outputString.contains("http://recipient.apache.org")); + + WSHandlerResult results = createAndVerifyMessage(callbackHandler, true); + WSSecurityEngineResult actionResult = + results.getActionResults().get(WSConstants.ST_UNSIGNED).get(0); + + SamlAssertionWrapper receivedSamlAssertion = + (SamlAssertionWrapper) actionResult.get(WSSecurityEngineResult.TAG_SAML_ASSERTION); + assertNotNull(receivedSamlAssertion); + assertFalse(receivedSamlAssertion.isSigned()); + } + /** * Test that creates, sends and processes an unsigned SAML 2 authentication assertion with * SubjectConfirmationData information. @@ -718,6 +774,60 @@ public class SamlTokenTest { assertFalse(receivedSamlAssertion.isSigned()); } + /** + * Test that creates, sends and processes an unsigned SAML 2 authentication assertion with + * SubjectConfirmationData containing an additional element, where the same bean is used to + * create more than one assertion. + */ + @Test + public void testSAML2SubjectConfirmationDataAny() throws Exception { + SAML2CallbackHandler callbackHandler = new SAML2CallbackHandler(); + callbackHandler.setStatement(SAML2CallbackHandler.Statement.AUTHN); + callbackHandler.setIssuer("www.example.com"); + + XMLObjectBuilderFactory builderFactory = XMLObjectProviderRegistrySupport.getBuilderFactory(); + @SuppressWarnings("unchecked") + XMLObjectBuilder<XSAny> xsAnyBuilder = + (XMLObjectBuilder<XSAny>)builderFactory.getBuilder(XSAny.TYPE_NAME); + XSAny customData = xsAnyBuilder.buildObject(new QName("urn:example:custom", "CustomData", "ex")); + customData.setTextContent("custom-confirmation-data"); + + SubjectConfirmationDataBean subjectConfirmationData = new SubjectConfirmationDataBean(); + subjectConfirmationData.setRecipient("http://recipient.apache.org"); + subjectConfirmationData.addAny(customData); + callbackHandler.setSubjectConfirmationData(subjectConfirmationData); + + SAMLCallback samlCallback = new SAMLCallback(); + SAMLUtil.doSAMLCallback(callbackHandler, samlCallback); + SamlAssertionWrapper samlAssertion = new SamlAssertionWrapper(samlCallback); + + 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); + + String outputString = + XMLUtils.prettyDocumentToString(unsignedDoc); + if (LOG.isDebugEnabled()) { + LOG.debug("SAML 2 Authn Assertion (sender vouches):"); + LOG.debug(outputString); + } + assertTrue(outputString.contains("custom-confirmation-data")); + + // The same SubjectConfirmationDataBean is used again to create a second assertion + WSHandlerResult results = createAndVerifyMessage(callbackHandler, true); + WSSecurityEngineResult actionResult = + results.getActionResults().get(WSConstants.ST_UNSIGNED).get(0); + + SamlAssertionWrapper receivedSamlAssertion = + (SamlAssertionWrapper) actionResult.get(WSSecurityEngineResult.TAG_SAML_ASSERTION); + assertNotNull(receivedSamlAssertion); + assertFalse(receivedSamlAssertion.isSigned()); + } + /** * Test that creates, sends and processes an unsigned SAML 2 authentication assertion with * a NameID in the Subject (see https://issues.apache.org/jira/browse/WSS-622)
