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)