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

coheigea pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ws-wss4j.git


The following commit(s) were added to refs/heads/master by this push:
     new 10fad7d67 WSS-713 - Support SubjectConfirmationData for SAML-SV v1 
(#747)
10fad7d67 is described below

commit 10fad7d673f66e2890c2c7c5ebb83312c04b44aa
Author: Colm O hEigeartaigh <[email protected]>
AuthorDate: Mon Oct 12 15:12:41 2026 +0100

    WSS-713 - Support SubjectConfirmationData for SAML-SV v1 (#747)
---
 .../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)

Reply via email to