[java-shib-attribute] branch main updated: JSATTR-6: SAML AttributeQuery DataConnector

Brent Putman putmanb at georgetown.edu
Tue Jun 3 04:50:39 UTC 2025


This is an automated email from the git hooks/post-receive script.

putmanb pushed a commit to branch main
in repository java-shib-attribute.

View the commit online:
http://git.shibboleth.net/view/?p=java-shib-attribute.git;a=commit;h=7e5878092955d7ec547e74deea914d36aae5f92e

The following commit(s) were added to refs/heads/main by this push:
     new 7e5878092 JSATTR-6: SAML AttributeQuery DataConnector
7e5878092 is described below

commit 7e5878092955d7ec547e74deea914d36aae5f92e
Author: Brent Putman <putmanb at georgetown.edu>
AuthorDate: Thu Sep 19 23:24:21 2024 -0400

    JSATTR-6: SAML AttributeQuery DataConnector
    
    Initial commit of SAML DataConnector components.
---
 pom.xml                                            |   1 +
 shib-attribute-resolver-api/pom.xml                |   4 +
 .../resolver/dc/saml/ExecutableQuery.java          |  44 ++
 .../attribute/resolver/dc/saml/ResponseData.java   | 112 ++++
 .../resolver/dc/saml/ResponseMappingStrategy.java  |  27 +
 .../attribute/resolver/dc/saml/package-info.java   |  21 +
 shib-attribute-resolver-impl/pom.xml               |  23 +-
 .../impl/AttributeAuthorityEntityIDResolver.java   |  37 ++
 .../dc/saml/impl/BasicResponseMappingStrategy.java | 458 ++++++++++++++
 .../dc/saml/impl/ExecutableQueryBuilder.java       | 658 +++++++++++++++++++++
 .../resolver/dc/saml/impl/SAMLDataConnector.java   | 253 ++++++++
 .../dc/saml/impl/SelfEntityIDResolver.java         |  40 ++
 .../impl/SimpleAggregationSAMLDataConnector.java   | 203 +++++++
 .../resolver/dc/saml/impl/SubjectResolver.java     |  41 ++
 .../resolver/dc/saml/impl/package-info.java        |  22 +
 .../impl/ChainingAuthorityEntityIDResolver.java    | 106 ++++
 .../plugin/impl/ChainingSelfEntityIDResolver.java  | 108 ++++
 .../saml/plugin/impl/ChainingSubjectResolver.java  | 108 ++++
 .../impl/ContextAuthorityEntityIDResolver.java     |  50 ++
 .../CriteriaDecryptionConfigurationLookup.java     | 124 ++++
 .../plugin/impl/CriteriaSelfEntityIDResolver.java  | 101 ++++
 .../impl/DependencyAttributeSubjectResolver.java   | 249 ++++++++
 .../resolver/dc/saml/plugin/impl/package-info.java |  21 +
 .../util/impl/AssertionValidationProcessor.java    | 222 +++++++
 .../impl/AttributeAuthorityEntityIDContext.java    |  53 ++
 .../impl/AttributeAuthorityEntityIDReference.java  |  34 ++
 .../impl/AttributeAuthorityEntityIDSource.java     |  49 ++
 .../util/impl/AttributeAuthorityEntityIDValue.java |  34 ++
 .../dc/saml/util/impl/DecryptionProcessor.java     | 367 ++++++++++++
 .../dc/saml/util/impl/SubjectMatchProcessor.java   |  89 +++
 .../resolver/dc/saml/util/impl/package-info.java   |  21 +
 31 files changed, 3670 insertions(+), 10 deletions(-)

diff --git a/pom.xml b/pom.xml
index 4ecdd2bdd..7d3a8624d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -48,6 +48,7 @@
         <opensaml.version>5.2.0-SNAPSHOT</opensaml.version>
         <shib-metadata.groupId>net.shibboleth</shib-metadata.groupId>
         <shib-metadata.version>5.2.0-SNAPSHOT</shib-metadata.version>
+
         <checkstyle.configLocation>${project.basedir}/resources/checkstyle/checkstyle.xml</checkstyle.configLocation>
     </properties>
 
diff --git a/shib-attribute-resolver-api/pom.xml b/shib-attribute-resolver-api/pom.xml
index 25620aef7..5834d6bc1 100644
--- a/shib-attribute-resolver-api/pom.xml
+++ b/shib-attribute-resolver-api/pom.xml
@@ -32,6 +32,10 @@
             <groupId>${opensaml.groupId}</groupId>
             <artifactId>opensaml-messaging-api</artifactId>
         </dependency>
+        <dependency>
+            <groupId>${opensaml.groupId}</groupId>
+            <artifactId>opensaml-saml-api</artifactId>
+        </dependency>
         <dependency>
             <groupId>${opensaml.groupId}</groupId>
             <artifactId>opensaml-profile-api</artifactId>
diff --git a/shib-attribute-resolver-api/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/ExecutableQuery.java b/shib-attribute-resolver-api/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/ExecutableQuery.java
new file mode 100644
index 000000000..7bf3adddf
--- /dev/null
+++ b/shib-attribute-resolver-api/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/ExecutableQuery.java
@@ -0,0 +1,44 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.attribute.resolver.dc.saml;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.saml.common.SAMLException;
+import org.opensaml.security.SecurityException;
+import org.opensaml.soap.client.SOAPClient;
+import org.opensaml.soap.common.SOAPException;
+
+import net.shibboleth.idp.attribute.resolver.dc.ExecutableSearch;
+
+/** A query that can be executed against a SAML 2 AttributeAuthority to fetch results. */
+public interface ExecutableQuery extends ExecutableSearch {
+    
+    /**
+     * Executes the query and returns the results.
+     * 
+     * @param soapClient SOAP client to process the query
+     * 
+     * @return the result of the executed query
+     * 
+     * @throws SAMLException if there is a SAML error during resolution
+     * @throws SOAPException  if there is a SOAP error during resolution
+     * @throws SecurityException if there there is a security-related error during resolution
+     * 
+     */
+    @Nonnull ResponseData execute(@Nonnull SOAPClient soapClient)
+            throws SAMLException, SOAPException, SecurityException;
+
+}
\ No newline at end of file
diff --git a/shib-attribute-resolver-api/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/ResponseData.java b/shib-attribute-resolver-api/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/ResponseData.java
new file mode 100644
index 000000000..9c81e40c5
--- /dev/null
+++ b/shib-attribute-resolver-api/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/ResponseData.java
@@ -0,0 +1,112 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.attribute.resolver.dc.saml;
+
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.messaging.context.InOutOperationContext;
+import org.opensaml.saml.saml2.core.Response;
+import org.opensaml.saml.saml2.metadata.RoleDescriptor;
+
+import net.shibboleth.idp.attribute.IdPAttributeValue;
+import net.shibboleth.idp.attribute.resolver.context.AttributeResolutionContext;
+import net.shibboleth.shared.logic.Constraint;
+
+/**
+ * A class which encapsulates the SAML 2 {@link Response} to an attribute query and associated data
+ * used in processing it within a data connector.
+ */
+public class ResponseData {
+    
+    /** Response. */
+    @Nonnull private Response response;
+    
+    /** SOAP client operation context. */
+    @Nonnull private InOutOperationContext soapClientContext;
+    
+    /** Attribute resolution context. */
+    @Nonnull private AttributeResolutionContext attributeResolutionContext;
+    
+    /** Dependency attributes. */
+    @Nonnull private Map<String, List<IdPAttributeValue>> dependencyAttributes;
+
+    /** Attribute authority role descriptor. */
+    @Nonnull private RoleDescriptor attributeAuthorityRoleDescriptor;
+    
+
+    /**
+     * Constructor.
+     *
+     * @param message SAML 2 Response message
+     * @param soapContext SOAP client operation context
+     * @param attributeContext attribute resolution context
+     * @param dependencies resolution dependency attributes
+     * @param roleDescriptor attribute authority role descriptor
+     */
+    public ResponseData(@Nonnull final Response message, @Nonnull final InOutOperationContext soapContext,
+            @Nonnull final AttributeResolutionContext attributeContext,
+            @Nonnull final Map<String, List<IdPAttributeValue>> dependencies,
+            @Nonnull final RoleDescriptor roleDescriptor) {
+        response = Constraint.isNotNull(message, "Response message was null");
+        soapClientContext = Constraint.isNotNull(soapContext, "SOAP client operation context was null");
+        attributeResolutionContext = Constraint.isNotNull(attributeContext, "Attribute resolution context was null");
+        dependencyAttributes = Constraint.isNotNull(dependencies, "Dependency attributes were null");
+        attributeAuthorityRoleDescriptor = Constraint.isNotNull(roleDescriptor, "RoleDescriptor was null");
+    }
+
+    /**
+     * Get the SAML 2 {@link Response}.
+     * 
+     * @return the SAML 2 Response message
+     */
+    @Nonnull public Response getResponse() {
+        return response;
+    }
+
+    /**
+     * Get the SOAP client {@link InOutOperationContext}.
+     * 
+     * @return the SOAP client operation context
+     */
+    @Nonnull public InOutOperationContext getSOAPClientContext() {
+        return soapClientContext;
+    }
+
+    /**
+     * Get the {@link AttributeResolutionContext}.
+     * 
+     * @return the attribute resolution context
+     */
+    @Nonnull public AttributeResolutionContext getAttributeResolutionContext() {
+        return attributeResolutionContext;
+    }
+
+    /**
+     * Get the resolution dependency attributes. 
+     * 
+     * @return Returns the dependency attributes
+     */
+    @Nonnull public Map<String, List<IdPAttributeValue>> getDependencyAttributes() {
+        return dependencyAttributes;
+    }
+    
+    @Nonnull public RoleDescriptor getAttributeAuthorityRoleDescriptor() {
+        return attributeAuthorityRoleDescriptor;
+    }
+
+}
diff --git a/shib-attribute-resolver-api/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/ResponseMappingStrategy.java b/shib-attribute-resolver-api/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/ResponseMappingStrategy.java
new file mode 100644
index 000000000..b4034f083
--- /dev/null
+++ b/shib-attribute-resolver-api/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/ResponseMappingStrategy.java
@@ -0,0 +1,27 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.attribute.resolver.dc.saml;
+
+import org.opensaml.saml.saml2.core.Response;
+
+import net.shibboleth.idp.attribute.resolver.dc.MappingStrategy;
+
+/**
+ * Strategy for mapping from a {@link Response} to a collection of
+ * {@link net.shibboleth.idp.attribute.IdPAttribute}s.
+ */
+public interface ResponseMappingStrategy extends MappingStrategy<ResponseData> {
+    
+}
\ No newline at end of file
diff --git a/shib-attribute-resolver-api/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/package-info.java b/shib-attribute-resolver-api/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/package-info.java
new file mode 100644
index 000000000..fa8117d5f
--- /dev/null
+++ b/shib-attribute-resolver-api/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Package for SAML data connector configuration.
+ */
+ at NonnullElements
+package net.shibboleth.idp.attribute.resolver.dc.saml;
+
+import net.shibboleth.shared.annotation.constraint.NonnullElements;
diff --git a/shib-attribute-resolver-impl/pom.xml b/shib-attribute-resolver-impl/pom.xml
index 2dfe2746d..cf7d2d144 100644
--- a/shib-attribute-resolver-impl/pom.xml
+++ b/shib-attribute-resolver-impl/pom.xml
@@ -32,7 +32,16 @@
             <artifactId>shib-attribute-resolver-api</artifactId>
             <version>${project.version}</version>
         </dependency>
+        <dependency>
+            <groupId>${project.groupId}</groupId>
+            <artifactId>shib-attribute-filter-api</artifactId>
+            <version>${project.version}</version>
+        </dependency>
 
+        <dependency>
+            <groupId>${opensaml.groupId}</groupId>
+            <artifactId>opensaml-core-api</artifactId>
+        </dependency>
         <dependency>
             <groupId>${opensaml.groupId}</groupId>
             <artifactId>opensaml-messaging-api</artifactId>
@@ -41,6 +50,10 @@
             <groupId>${opensaml.groupId}</groupId>
             <artifactId>opensaml-profile-api</artifactId>
         </dependency>
+        <dependency>
+            <groupId>${opensaml.groupId}</groupId>
+            <artifactId>opensaml-saml-api</artifactId>
+        </dependency>
         <dependency>
             <groupId>${opensaml.groupId}</groupId>
             <artifactId>opensaml-security-api</artifactId>
@@ -110,16 +123,6 @@
             <scope>test</scope>
         </dependency>
 
-        <dependency>
-            <groupId>${opensaml.groupId}</groupId>
-            <artifactId>opensaml-core-api</artifactId>
-            <scope>test</scope>
-        </dependency>
-        <dependency>
-            <groupId>${opensaml.groupId}</groupId>
-            <artifactId>opensaml-saml-api</artifactId>
-            <scope>test</scope>
-        </dependency>
         <dependency>
             <!-- We need this even if mvn dependency:analyze says we don't - this 
              provides the implementations of the SAML interfaces and by definition cannot 
diff --git a/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/impl/AttributeAuthorityEntityIDResolver.java b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/impl/AttributeAuthorityEntityIDResolver.java
new file mode 100644
index 000000000..8345be352
--- /dev/null
+++ b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/impl/AttributeAuthorityEntityIDResolver.java
@@ -0,0 +1,37 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.attribute.resolver.dc.saml.impl;
+
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import net.shibboleth.idp.attribute.IdPAttributeValue;
+import net.shibboleth.idp.attribute.resolver.ResolutionException;
+import net.shibboleth.idp.attribute.resolver.context.AttributeResolutionContext;
+
+/**
+ * Interface for a component which resolves the entityID of an AttributeAuthority
+ * against which to query for attributes.
+ */
+public interface AttributeAuthorityEntityIDResolver {
+    
+    @Nullable
+    String resolve(@Nonnull final AttributeResolutionContext resolutionContext,
+            @Nonnull final Map<String, List<IdPAttributeValue>> dependencyAttributes) throws ResolutionException;
+
+}
diff --git a/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/impl/BasicResponseMappingStrategy.java b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/impl/BasicResponseMappingStrategy.java
new file mode 100644
index 000000000..e494be73e
--- /dev/null
+++ b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/impl/BasicResponseMappingStrategy.java
@@ -0,0 +1,458 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.attribute.resolver.dc.saml.impl;
+
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.core.criterion.EntityIdCriterion;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.saml.common.messaging.context.SAMLMetadataContext;
+import org.opensaml.saml.common.messaging.context.SAMLPeerEntityContext;
+import org.opensaml.saml.common.messaging.context.SAMLSelfEntityContext;
+import org.opensaml.saml.common.xml.SAMLConstants;
+import org.opensaml.saml.criterion.EntityRoleCriterion;
+import org.opensaml.saml.criterion.ProtocolCriterion;
+import org.opensaml.saml.metadata.resolver.MetadataResolver;
+import org.opensaml.saml.metadata.resolver.RoleDescriptorResolver;
+import org.opensaml.saml.saml2.core.Assertion;
+import org.opensaml.saml.saml2.core.Attribute;
+import org.opensaml.saml.saml2.core.AttributeStatement;
+import org.opensaml.saml.saml2.core.Issuer;
+import org.opensaml.saml.saml2.metadata.AttributeAuthorityDescriptor;
+import org.opensaml.saml.saml2.metadata.EntityDescriptor;
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.attribute.IdPAttribute;
+import net.shibboleth.idp.attribute.IdPAttributeSupport;
+import net.shibboleth.idp.attribute.filter.AttributeFilter;
+import net.shibboleth.idp.attribute.filter.AttributeFilterException;
+import net.shibboleth.idp.attribute.filter.context.AttributeFilterContext;
+import net.shibboleth.idp.attribute.filter.context.AttributeFilterContext.Direction;
+import net.shibboleth.idp.attribute.resolver.ResolutionException;
+import net.shibboleth.idp.attribute.resolver.dc.saml.ResponseData;
+import net.shibboleth.idp.attribute.resolver.dc.saml.ResponseMappingStrategy;
+import net.shibboleth.idp.attribute.transcoding.AttributeTranscoder;
+import net.shibboleth.idp.attribute.transcoding.AttributeTranscoderRegistry;
+import net.shibboleth.idp.attribute.transcoding.TranscoderSupport;
+import net.shibboleth.idp.attribute.transcoding.TranscodingRule;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.component.AbstractInitializableComponent;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.resolver.CriteriaSet;
+import net.shibboleth.shared.resolver.ResolverException;
+import net.shibboleth.shared.service.ReloadableService;
+import net.shibboleth.shared.service.ServiceException;
+import net.shibboleth.shared.service.ServiceableComponent;
+
+/**
+ * Basic SAML 2 Response strategy to map to data connector output requirements.
+ */
+public class BasicResponseMappingStrategy extends AbstractInitializableComponent implements ResponseMappingStrategy {
+    
+    /** Logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(BasicResponseMappingStrategy.class);
+    
+    /** Transcoder registry. */
+    @NonnullAfterInit private AttributeTranscoderRegistry transcoderRegistry;
+    
+    /** MetadataResolver. */
+    @NonnullAfterInit private MetadataResolver metadataResolver;
+    
+    /** Role descriptor resolver. */
+    @NonnullAfterInit private RoleDescriptorResolver roleDescriptorResolver;
+    
+    /** AttributeFilter service. */
+    @NonnullAfterInit private ReloadableService<AttributeFilter> filterService;
+    
+    /** Flag indicating whether to filter attributes. */
+    private boolean filterAttributes = true;
+
+    /**
+     * Set the instance of {@link AttributeTranscoderRegistry} to use.
+     * 
+     * @param registry the transcoder registry
+     */
+    public void setTranscoderRegistry(final @Nullable AttributeTranscoderRegistry registry) {
+        checkSetterPreconditions();
+        transcoderRegistry = registry;
+    }
+    
+    /**
+     * Set the instance of {@link MetadataResolver} to use.
+     * 
+     * @param resolver the metadata resolver
+     */
+    public void setMetadataResolver(final @Nullable MetadataResolver resolver) {
+        checkSetterPreconditions();
+        metadataResolver = resolver;
+    }
+    
+    /**
+     * Set the role descriptor resolver.
+     * 
+     * @param resolver the role descriptor resolver
+     */
+    public void setRoleDescriptorResolver(@Nullable final RoleDescriptorResolver resolver) {
+        checkSetterPreconditions();
+        roleDescriptorResolver = resolver;
+    }
+    
+    /**
+     * Set the instance of {@link AttributeFilter} service to use.
+     * 
+     * @param service the attribute filter service
+     */
+    public void setAttributeFilterService(final @Nullable ReloadableService<AttributeFilter> service) {
+        checkSetterPreconditions();
+        filterService = service;
+    }
+    
+    /**
+     * Set the flag indicating whether to filter attributes.
+     * 
+     * @param flag true if attributes should be filter, otherwise false
+     */
+    public void setFilterAttributes(final boolean flag) {
+        filterAttributes = flag;
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        
+        if (transcoderRegistry == null) {
+            throw new ComponentInitializationException("AttributeTranscoderRegistry was null");
+        }
+        if (metadataResolver == null) {
+            throw new ComponentInitializationException("MetadataResolver was null");
+        }
+        if (roleDescriptorResolver == null) {
+            throw new ComponentInitializationException("RoleDescriptorResolver was null");
+        }
+        if (filterService == null) {
+            throw new ComponentInitializationException("AttributeFilter service was null");
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    @Nullable
+    public Map<String, IdPAttribute> map(@Nonnull final ResponseData responseData) throws ResolutionException {
+        checkComponentActive();
+
+        final List<IdPAttribute> aggregateResults = new LinkedList<>();
+        
+        final ProfileRequestContext profileContext = responseData.getAttributeResolutionContext()
+                .getProfileRequestContextLookupStrategy()
+                .apply(responseData.getAttributeResolutionContext());
+        if (profileContext != null) {
+            log.trace("For transcoding successfully resolved ProfileRequestContext");
+        } else {
+            log.trace("For transcoding failed to resolve ProfileRequestContext, some features may be unavailable");
+        }
+
+        for (final Assertion assertion : responseData.getResponse().getAssertions()) {
+            String assertionIssuerID = null;
+            final Issuer assertionIssuer = assertion.getIssuer();
+            if (assertionIssuer != null) {
+                assertionIssuerID = assertionIssuer.getValue();
+            }
+
+            log.debug("Processing attributes from Assertion '{}' issued by '{}'",
+                    assertion.getID(), assertionIssuerID != null ? assertionIssuerID : "<unknown>");
+
+            final List<IdPAttribute> assertionResults = new LinkedList<>();
+
+            for (final AttributeStatement statement : assertion.getAttributeStatements()) {
+                for (final Attribute samlAttribute : statement.getAttributes()) {
+                    assert samlAttribute != null;
+                    assertionResults.addAll(transcodeAttribute(samlAttribute, profileContext));
+                }
+            }
+
+            aggregateResults.addAll(processAssertionFiltering(assertion, assertionResults, assertionIssuerID,
+                    profileContext, responseData));
+        }
+        
+        final Map<String,IdPAttribute> mergedResults = IdPAttributeSupport.toMapMergeDuplicates(aggregateResults);
+
+        if (log.isTraceEnabled()) {
+            log.trace("Decoded {} total unique attribute IDs:", mergedResults.keySet().size()); 
+            for (final String attributeID : mergedResults.keySet().stream().sorted().toList()) {
+                log.trace("\tAttribute ID '{}', # of values: {}",
+                        attributeID, mergedResults.get(attributeID).getValues().size());
+            }
+        }
+        
+        return mergedResults;
+    }
+
+    /**
+     * Transcode a specified {@link Attribute} into a list of {@link IdPAttribute}.
+     * 
+     * @param samlAttribute the SAML Attribute to transcode
+     * @param profileContext the profile request context being processed, if available
+     * 
+     * @return collection of transcoded attributes
+     */
+    @Nonnull private List<IdPAttribute> transcodeAttribute(@Nonnull final Attribute samlAttribute,
+            @Nullable final ProfileRequestContext profileContext) {
+        
+        final List<IdPAttribute> attributeResults = new LinkedList<>();
+
+        final Collection<TranscodingRule> transcodingRules = transcoderRegistry.getTranscodingRules(samlAttribute);
+        if (transcodingRules.isEmpty()) {
+            log.debug("No transcoding rule for Attribute (Name '{}', NameFormat: '{}')",
+                    samlAttribute.getName(),
+                    samlAttribute.getNameFormat() != null ? samlAttribute.getNameFormat() 
+                            : Attribute.UNSPECIFIED);
+            return CollectionSupport.emptyList();
+        }
+
+        for (final TranscodingRule rule : transcodingRules) {
+            assert rule != null;
+
+            final AttributeTranscoder<Attribute> transcoder = TranscoderSupport.getTranscoder(rule);
+            try {
+                final IdPAttribute decodedAttribute = transcoder.decode(profileContext, samlAttribute, rule);
+                if (decodedAttribute != null) {
+                    attributeResults.add(decodedAttribute);
+                }
+            } catch (final Exception e) {
+                log.warn("Error transcoding for Attribute (Name '{}', NameFormat: '{}') via transcoder: {}",
+                        samlAttribute.getName(),
+                        samlAttribute.getNameFormat() != null ? samlAttribute.getNameFormat()
+                                : Attribute.UNSPECIFIED, 
+                        transcoder.getClass().getName(),
+                        e);
+            }
+        }
+
+        return attributeResults;
+    }
+
+    /**
+     * Process filtering the attributes of an Assertion.
+     * 
+     * @param assertion the assertion being processed
+     * @param assertionResults the decoded attributes of an Assertion
+     * @param assertionIssuerID the Assertion issuer ID
+     * @param profileContext  the profile request being processed
+     * @param responseData the Response data being processed
+     * 
+     * @return the results of filtering for the assertion's attributes
+     */
+    @Nonnull
+    private Collection<IdPAttribute> processAssertionFiltering(@Nonnull final Assertion assertion,
+            @Nonnull final List<IdPAttribute> assertionResults, @Nullable final String assertionIssuerID,
+            @Nullable final ProfileRequestContext profileContext, @Nonnull final ResponseData responseData) {
+
+        if (filterAttributes) {
+            if (assertionIssuerID != null && profileContext != null) {
+                log.debug("Filtering attributes decoded from Assertion '{}' issued by '{}'",
+                        assertion.getID(), assertionIssuerID);
+                return filterAssertionResults(assertionResults, assertionIssuerID, profileContext, responseData);
+                
+            } else {
+                log.error("Attribute filtering enabled, but Assertion Issuer or ProfileRequestContext unknown, "
+                        + " unable to filter and include attributes in result set from Assertion '{}'",
+                        assertion.getID());
+                return CollectionSupport.emptyList();
+            }
+        } else {
+            log.debug("Attribute filtering disabled, adding all attributes from Assertion '{}' issued by '{}'",
+                    assertion.getID(), assertionIssuerID != null ? assertionIssuerID : "<unknown>");
+            return assertionResults;
+        }
+    }
+
+    /**
+     * Filter the decoded attributes from an Assertion.
+     * 
+     * @param assertionResults the decoded attributes of an Assertion
+     * @param assertionIssuerID the Assertion issuer ID
+     * @param profileContext  the profile request being processed
+     * @param responseData the Response data being processed
+     * 
+     * @return the filtered attributes
+     */
+    @Nonnull private Collection<IdPAttribute> filterAssertionResults(@Nonnull final List<IdPAttribute> assertionResults,
+            @Nonnull final String assertionIssuerID, @Nonnull final ProfileRequestContext profileContext,
+            @Nonnull final ResponseData responseData) {
+        
+        final AttributeFilterContext filterContext =
+                profileContext.ensureSubcontext(AttributeFilterContext.class);
+        
+        final Map<String,IdPAttribute> unfilteredAttributes = 
+                IdPAttributeSupport.toMapMergeDuplicates(assertionResults);
+
+        populateFilterContext(filterContext, unfilteredAttributes, assertionIssuerID, responseData);
+
+        try (final ServiceableComponent<AttributeFilter> component = filterService.getServiceableComponent()) {
+            final AttributeFilter filter = component.getComponent();
+            filter.filterAttributes(filterContext);
+            final Map<String,IdPAttribute>  filtered = filterContext.getFilteredIdPAttributes();
+            if (filtered != null) {
+                final Collection<IdPAttribute> filteredValues = filtered.values();
+                if (filteredValues != null) {
+                    return filteredValues;
+                }
+            }
+            return CollectionSupport.emptyList();
+        } catch (final AttributeFilterException e) {
+            log.error("Error while filtering inbound attributes", e);
+            return CollectionSupport.emptyList();
+        } catch (final ServiceException e) { 
+            log.error("Invalid AttributeFilter configuration", e);
+            return CollectionSupport.emptyList();
+        } finally {
+            filterContext.removeFromParent();
+        }
+
+    }
+
+    /**
+     * Populate the {@link AttributeFilterContext} for processing.
+     * 
+     * @param filterContext the filter context to populate
+     * @param unfilteredAttributes  the unfiltered attributes
+     * @param assertionIssuer issuer of the Assertion being processed
+     * @param responseData the response data being processed
+     */
+    private void populateFilterContext(@Nonnull final AttributeFilterContext filterContext,
+            @Nonnull final Map<String, IdPAttribute> unfilteredAttributes,
+            @Nonnull final String assertionIssuer,
+            @Nonnull final ResponseData responseData) {
+        
+        final SAMLMetadataContext issuerMetadataContext = resolveIssuerMetadataContext(responseData, assertionIssuer);
+        final String selfEntityID = resolveSelfEntityID(responseData);
+        
+        filterContext.setDirection(Direction.INBOUND)
+            .setPrefilteredIdPAttributes(unfilteredAttributes)
+            .setMetadataResolver(metadataResolver)
+            .setIssuerMetadataContextLookupStrategy(t -> issuerMetadataContext)
+            .setAttributeIssuerID(assertionIssuer)
+            .setAttributeRecipientID(selfEntityID);
+    }
+
+    /**
+     * Resolve the self entityID used for the original attribute query.
+     * 
+     * @param responseData the response data being processed
+     * 
+     * @return the self entityID
+     */
+    @Nullable private String resolveSelfEntityID(@Nonnull final ResponseData responseData) {
+        return responseData
+                .getSOAPClientContext()
+                .ensureSubcontext(SAMLSelfEntityContext.class)
+                .getEntityId();
+    }
+
+    /**
+     * Resolve the {@link SAMLMetadataContext} for the assertion issuer.
+     * 
+     * @param responseData the response data being processed
+     * @param assertionIssuer issuer of the Assertion being processed
+     * 
+     * @return the resolved SAMLMetadataContext instance
+     */
+    @Nonnull private SAMLMetadataContext resolveIssuerMetadataContext(@Nonnull final ResponseData responseData,
+            @Nonnull final String assertionIssuer) {
+        
+        // First check if Assertion issuer is the same as Response issuer.
+        // If so, use the SAMLMetadataContext that already exists
+        final SAMLPeerEntityContext peerEntityContext = resolvePeerEntityContext(responseData);
+        if (assertionIssuer.equals(peerEntityContext.getEntityId())) {
+            final SAMLMetadataContext peerMetadataContext =
+                    peerEntityContext.getSubcontext(SAMLMetadataContext.class);
+            if (peerMetadataContext != null 
+                    && peerMetadataContext.getEntityDescriptor() != null 
+                    && peerMetadataContext.getRoleDescriptor() != null) {
+                log.debug("Assertion issuer same as Response, resolved SAMLMetadataContext from SOAP client context");
+                return peerMetadataContext;
+            } else {
+                log.debug("Assertion issuer same as Response but context data incomplete, " 
+                        + "resolving data for SAMLMetadataContext");
+            }
+        } else {
+            log.debug("Assertion issuer NOT same as Response, resolving data for SAMLMetadataContext");
+        }
+        
+        
+        // It didn't exist so resolve data via resolver
+        final SAMLMetadataContext metadataContext = new SAMLMetadataContext();
+
+        final AttributeAuthorityDescriptor roleDescriptor = resolveAuthorityRoleDescriptor(assertionIssuer);
+        metadataContext.setRoleDescriptor(roleDescriptor);
+        if (roleDescriptor != null && roleDescriptor.getParent() instanceof EntityDescriptor entityDescriptor) {
+            metadataContext.setEntityDescriptor(entityDescriptor);
+        }
+
+        return metadataContext;
+    }
+    
+    /**
+     * Resolve the response issuer's {@link SAMLPeerEntityContext}.
+     * 
+     * @param responseData the response data being processed
+     * 
+     * @return the self entityID
+     */
+    @Nonnull private SAMLPeerEntityContext resolvePeerEntityContext(@Nonnull final ResponseData responseData) {
+        return responseData
+                .getSOAPClientContext()
+                .ensureSubcontext(SAMLPeerEntityContext.class);
+    }
+
+    /**
+     * Resolve the attribute authority role descriptor.
+     * 
+     * @param authorityEntityID the entityID of the attribute authority being processed
+     * 
+     * @return the the role descriptor of the attribute authority being processed, or null if not available
+     */
+    @Nullable private AttributeAuthorityDescriptor resolveAuthorityRoleDescriptor(
+            @Nonnull final String authorityEntityID) {
+        
+        final CriteriaSet criteriaSet = new CriteriaSet(
+                new EntityIdCriterion(authorityEntityID),
+                new ProtocolCriterion(SAMLConstants.SAML20P_NS),
+                new EntityRoleCriterion(AttributeAuthorityDescriptor.DEFAULT_ELEMENT_NAME));
+        try {
+            final AttributeAuthorityDescriptor descriptor =
+                    (AttributeAuthorityDescriptor) roleDescriptorResolver.resolveSingle(criteriaSet);
+            if (descriptor != null) {
+                log.debug("Successfully resolved AttributeAuthorityDescriptor for entityID: {}", authorityEntityID);
+                return descriptor;
+            }
+            log.warn("Failed to resolve AttributeAuthorityDescriptor for entityID: {}", authorityEntityID);
+            return null;
+        } catch (final ResolverException e) {
+            log.warn("Fatal error resolving AttributeAuthorityDescriptor for entityID: {}", authorityEntityID, e);
+            return null;
+        }
+    }
+
+}
diff --git a/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/impl/ExecutableQueryBuilder.java b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/impl/ExecutableQueryBuilder.java
new file mode 100644
index 000000000..1776725a2
--- /dev/null
+++ b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/impl/ExecutableQueryBuilder.java
@@ -0,0 +1,658 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.attribute.resolver.dc.saml.impl;
+
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.core.criterion.EntityIdCriterion;
+import org.opensaml.core.xml.io.MarshallingException;
+import org.opensaml.core.xml.io.UnmarshallingException;
+import org.opensaml.core.xml.util.XMLObjectSupport;
+import org.opensaml.messaging.MessageException;
+import org.opensaml.messaging.context.InOutOperationContext;
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.saml.common.SAMLException;
+import org.opensaml.saml.common.SAMLObject;
+import org.opensaml.saml.common.binding.EndpointResolver;
+import org.opensaml.saml.common.messaging.soap.SAMLSOAPClientContextBuilder;
+import org.opensaml.saml.common.xml.SAMLConstants;
+import org.opensaml.saml.criterion.EndpointCriterion;
+import org.opensaml.saml.criterion.EntityRoleCriterion;
+import org.opensaml.saml.criterion.ProtocolCriterion;
+import org.opensaml.saml.criterion.RoleDescriptorCriterion;
+import org.opensaml.saml.metadata.resolver.RoleDescriptorResolver;
+import org.opensaml.saml.saml2.core.Attribute;
+import org.opensaml.saml.saml2.core.AttributeQuery;
+import org.opensaml.saml.saml2.core.Issuer;
+import org.opensaml.saml.saml2.core.NameID;
+import org.opensaml.saml.saml2.core.Response;
+import org.opensaml.saml.saml2.core.Status;
+import org.opensaml.saml.saml2.core.StatusCode;
+import org.opensaml.saml.saml2.core.Subject;
+import org.opensaml.saml.saml2.metadata.AttributeAuthorityDescriptor;
+import org.opensaml.saml.saml2.metadata.AttributeService;
+import org.opensaml.security.SecurityException;
+import org.opensaml.soap.client.SOAPClient;
+import org.opensaml.soap.client.http.PipelineFactoryHttpSOAPClient;
+import org.opensaml.soap.common.SOAPException;
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.attribute.IdPAttributeValue;
+import net.shibboleth.idp.attribute.resolver.ResolutionException;
+import net.shibboleth.idp.attribute.resolver.context.AttributeResolutionContext;
+import net.shibboleth.idp.attribute.resolver.dc.ExecutableSearchBuilder;
+import net.shibboleth.idp.attribute.resolver.dc.saml.ExecutableQuery;
+import net.shibboleth.idp.attribute.resolver.dc.saml.ResponseData;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.NotLive;
+import net.shibboleth.shared.annotation.constraint.Unmodifiable;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.component.AbstractInitializableComponent;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.primitive.StringSupport;
+import net.shibboleth.shared.resolver.CriteriaSet;
+import net.shibboleth.shared.resolver.ResolverException;
+import net.shibboleth.shared.security.IdentifierGenerationStrategy;
+
+/**
+ * Component which builds an {@link ExecutableQuery} used with the {@link SAMLDataConnector}.
+ */
+public class ExecutableQueryBuilder extends AbstractInitializableComponent
+        implements ExecutableSearchBuilder<ExecutableQuery> {
+    
+    /** Logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(ExecutableQueryBuilder.class);
+    
+    /** Resolver for AttributeService endpoints. **/
+    @NonnullAfterInit private EndpointResolver<AttributeService> authorityEndpointResolver;
+    
+    /** Role descriptor resolver. */
+    @NonnullAfterInit private RoleDescriptorResolver roleDescriptorResolver;
+    
+    /** The SOAP client message pipeline name. */
+    @NonnullAfterInit private String soapPipelineName;
+    
+    /** Identifier generation strategy. */
+    @NonnullAfterInit private IdentifierGenerationStrategy idStrategy;
+    
+    /** SOAP client security configuration profile ID. */
+    @NonnullAfterInit private String soapClientSecurityConfigurationProfileId;
+    
+    /** Strategy for resolving the authority entityID. */
+    @NonnullAfterInit private AttributeAuthorityEntityIDResolver authorityEntityIDStrategy;
+    
+    /** Strategy for resolving the authority entityID. */
+    @NonnullAfterInit private SelfEntityIDResolver selfEntityIDStrategy;
+    
+    /** Strategy for resolving the authority entityID. */
+    @NonnullAfterInit private SubjectResolver subjectStrategy;
+    
+    /** Optional list of saml2:Attributes to request in the query. */
+    @Nonnull private List<Attribute> requestedAttributes = CollectionSupport.emptyList();
+    
+    
+    /**
+     * Get the attribute authority endpoint resolver.
+     * 
+     * @return the endpoint resolver
+     */
+    @NonnullAfterInit public EndpointResolver<AttributeService> getAuthorityEndpointResolver() {
+        return authorityEndpointResolver;
+    }
+
+    /**
+     * Set the attribute authority endpoint resolver.
+     * 
+     * @param resolver the new resolver
+     */
+    public void setAuthorityEndpointResolver(@Nullable final EndpointResolver<AttributeService> resolver) {
+        checkSetterPreconditions();
+        authorityEndpointResolver = resolver;
+    }
+
+    /**
+     * Get the role descriptor resolver.
+     * 
+     * @return the role descriptor resolver
+     */
+    @NonnullAfterInit public RoleDescriptorResolver getRoleDescriptorResolver() {
+        return roleDescriptorResolver;
+    }
+
+    /**
+     * Set the role descriptor resolver.
+     * 
+     * @param resolver the role descriptor resolver
+     */
+    public void setRoleDescriptorResolver(@Nullable final RoleDescriptorResolver resolver) {
+        checkSetterPreconditions();
+        roleDescriptorResolver = resolver;
+    }
+    
+    /**
+     * Get the name of the specific SOAP client message pipeline to use, 
+     * for example with {@link PipelineFactoryHttpSOAPClient}. 
+     * 
+     * @return the pipeline name, or null
+     */
+    @NonnullAfterInit public String getSOAPPipelineName() {
+        return soapPipelineName;
+    }
+
+    /**
+     * Set the name of the specific SOAP client message pipeline to use, 
+     * for example with {@link PipelineFactoryHttpSOAPClient}. 
+     * 
+     * @param name the pipeline name, or null
+     */
+    public void setSOAPPipelineName(@Nullable final String name) {
+        checkSetterPreconditions();
+        soapPipelineName = StringSupport.trimOrNull(name);
+    }
+    
+    /**
+     * Get the identifier generation strategy.
+     * 
+     * @return Returns the identifier generation strategy
+     */
+    @NonnullAfterInit public IdentifierGenerationStrategy getIdentifierGenerationStrategy() {
+        return idStrategy;
+    }
+
+    /**
+     * Set the identifier generation strategy.
+     * 
+     * @param strategy the identifier generation strategy
+     */
+    public void setIdentifierGenerationStrategy(@Nonnull final IdentifierGenerationStrategy strategy) {
+        checkSetterPreconditions();
+        idStrategy = strategy;
+    }
+    
+    /**
+     * Get the SOAP client security configuration profile ID to use.
+     * 
+     * @return the client security configuration profile ID, or null
+     */
+    @NonnullAfterInit public String getSOAPClientSecurityConfigurationProfileId() {
+        return soapClientSecurityConfigurationProfileId;
+    }
+
+    /**
+     * Set the SOAP client security configuration profile ID to use.
+     * 
+     * @param profileId the profile ID, or null
+     */
+    public void setSOAPClientSecurityConfigurationProfileId(@Nullable final String profileId) {
+        checkSetterPreconditions();
+        soapClientSecurityConfigurationProfileId = StringSupport.trimOrNull(profileId);
+    }
+
+    /**
+     * @return the strategy
+     */
+    @NonnullAfterInit public AttributeAuthorityEntityIDResolver getAuthorityEntityIDStrategy() {
+        return authorityEntityIDStrategy;
+    }
+
+    /**
+     * @param strategy the entityID resolution strategy
+     */
+    public void setAuthorityEntityIDStrategy(
+            @Nonnull final AttributeAuthorityEntityIDResolver strategy) {
+        checkSetterPreconditions();
+        authorityEntityIDStrategy = strategy;
+    }
+
+    /**
+     * @return the strategy
+     */
+    @NonnullAfterInit public SelfEntityIDResolver getSelfEntityIDStrategy() {
+        return selfEntityIDStrategy;
+    }
+
+    /**
+     * @param strategy the entityID resolution strategy
+     */
+    public void setSelfEntityIDStrategy(
+            @Nonnull final SelfEntityIDResolver strategy) {
+        checkSetterPreconditions();
+        selfEntityIDStrategy = strategy;
+    }
+
+    /**
+     * @return the strategy
+     */
+    @NonnullAfterInit public SubjectResolver getSubjectStrategy() {
+        return subjectStrategy;
+    }
+
+    /**
+     * @param strategy the entityID resolution strategy
+     */
+    public void setSubjectStrategy(
+            @Nonnull final SubjectResolver strategy) {
+        checkSetterPreconditions();
+        subjectStrategy = strategy;
+    }
+
+    /**
+     * Get the optional list of saml2:Attributes to request in the query.
+     * 
+     * @return the list of attributes, may be null or empty
+     */
+    @Unmodifiable
+    @NotLive
+    @Nonnull public List<Attribute> getRequestedAttributes() {
+        return requestedAttributes;
+    }
+
+    /**
+     * Set the optional list of saml2:Attributes to request in the query.
+     * 
+     * @param attributes the list of attributes to request, may be null or empty
+     */
+    public void setRequestedAttributes(@Nullable List<Attribute> attributes) {
+        if (attributes == null) {
+            requestedAttributes = CollectionSupport.emptyList();
+        } else {
+            requestedAttributes = attributes.stream()
+                    .filter(Objects::nonNull)
+                    .collect(CollectionSupport.nonnullCollector(Collectors.toUnmodifiableList())).get();
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        
+        if (getSOAPClientSecurityConfigurationProfileId() == null) {
+            throw new ComponentInitializationException("SOAP client security config profile ID was null");
+        }
+        if (getAuthorityEndpointResolver() == null) {
+            throw new ComponentInitializationException("Authority EndpointResolver was null");
+        }
+        if (getAuthorityEntityIDStrategy() == null) {
+            throw new ComponentInitializationException("Authority entityID strategy was null");
+        }
+        if (getSelfEntityIDStrategy() == null) {
+            throw new ComponentInitializationException("Self entityID strategy was null");
+        }
+        if (getSubjectStrategy() == null) {
+            throw new ComponentInitializationException("Subject strategy was null");
+        }
+        if (getIdentifierGenerationStrategy() == null) {
+            throw new ComponentInitializationException("IdentifierGenerationStrategy was null");
+        }
+        if (getRoleDescriptorResolver() == null) {
+            throw new ComponentInitializationException("RoleDescriptorResolver was null");
+        }
+        if (getSOAPClientSecurityConfigurationProfileId() == null) {
+            throw new ComponentInitializationException("SOAP client security configuration profile ID was null");
+        }
+        if (getSOAPPipelineName() == null) {
+            throw new ComponentInitializationException("SOAP pipeline name was null");
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    @Nonnull
+    public ExecutableQuery build(@Nonnull final AttributeResolutionContext resolutionContext,
+            @Nonnull final Map<String, List<IdPAttributeValue>> dependencyAttributes) throws ResolutionException {
+        
+        checkComponentActive();
+        
+        final String authorityEntityID = resolveAttributeAuthorityEntityID(resolutionContext, dependencyAttributes);
+        final AttributeAuthorityDescriptor authorityRoleDescriptor = resolveAuthorityRoleDescriptor(authorityEntityID);
+        final String authorityEndpoint = resolveAuthorityEndpoint(authorityEntityID, authorityRoleDescriptor);
+        
+        final InOutOperationContext opContext = buildOperationContext(resolutionContext, dependencyAttributes,
+                authorityRoleDescriptor, authorityEndpoint);
+            
+        return new ExecutableQuery() {
+
+            /** Logger. */
+            @Nonnull private final Logger log = LoggerFactory.getLogger(ExecutableQuery.class);
+                
+            @Nullable public String getResultCacheKey() {
+                final MessageContext outbound = opContext.getOutboundMessageContext();
+                assert outbound != null;
+                final AttributeQuery query = (AttributeQuery) outbound.getMessage();
+                assert query != null;
+                final Subject subject = query.getSubject();
+                assert subject != null;
+                final NameID nameid = subject.getNameID();
+                assert nameid != null;
+                final List<String> attribNames = query.getAttributes().stream()
+                        .map(Attribute::getName)
+                        .sorted()
+                        .toList();
+
+                final StringBuilder builder = new StringBuilder();
+                
+                builder.append(authorityEntityID);
+                builder.append(":");
+                builder.append(nameid.getValue());
+                if (attribNames.size() > 0) {
+                    builder.append(":");
+                    builder.append(StringSupport.listToStringValue(attribNames, ","));
+                }
+
+                return builder.toString();
+            }
+
+            /** {@inheritDoc} */
+            public String toString() {
+                return getResultCacheKey();
+            }
+
+            @Nonnull public ResponseData execute(@Nonnull final SOAPClient soapClient)
+                    throws SAMLException, SOAPException, SecurityException {
+                
+                log.trace("Executing AttributeQuery over SOAP 1.1 binding to endpoint: {}", authorityEndpoint);
+                soapClient.send(authorityEndpoint, opContext); 
+
+                final MessageContext inboundContext = opContext.getInboundMessageContext();
+                final Object message = inboundContext != null ? inboundContext.getMessage() : null;
+                if (message instanceof Response response) {
+                    validateResponse(response);
+                    return new ResponseData(response, opContext, resolutionContext, dependencyAttributes,
+                            authorityRoleDescriptor);
+                }
+                throw new SOAPException("SOAP message payload was not an instance of Response: " 
+                        + (message != null ? message.getClass().getName() : "(null)"));
+            }
+
+            private void validateResponse(@Nonnull final Response response) throws SAMLException {
+                final Status status = response.getStatus();
+                final StatusCode statusCode = status != null ? status.getStatusCode() : null;
+                if (statusCode == null || statusCode.getValue() == null) {
+                    throw new SAMLException("Response included no StatusCode, could not validate");
+                } else if (!StatusCode.SUCCESS.equals(statusCode.getValue())){
+                    throw new SAMLException("Response carried non-success StatusCode: " + statusCode.getValue());
+                }
+            }
+        };
+    }
+
+    /**
+     * Resolve the attribute authority entityID.
+     * 
+     * @param resolutionContext the current attribute resolution context
+     * @param dependencyAttributes the current set of dependency attributes
+     * 
+     * @return the entityID of the attribute authority being processed
+     * 
+     * @throws ResolutionException if there is a fatal error during processing
+     */
+    @Nonnull private String resolveAttributeAuthorityEntityID(@Nonnull final AttributeResolutionContext resolutionContext,
+            @Nonnull final Map<String, List<IdPAttributeValue>> dependencyAttributes) throws ResolutionException {
+
+        final String entityID = getAuthorityEntityIDStrategy().resolve(resolutionContext, dependencyAttributes);
+        if (entityID != null) {
+            return entityID;
+        }
+        throw new ResolutionException("Unable to resolve AttributeAuthority entityID");
+    }
+
+    /**
+     * Resolve the attribute authority role descriptor.
+     * 
+     * @param authorityEntityID the entityID of the attribute authority being processed
+     * 
+     * @return the the role descriptor of the attribute authority being processed
+     * 
+     * @throws ResolutionException if there is a fatal error during processing
+     */
+    @Nonnull private AttributeAuthorityDescriptor resolveAuthorityRoleDescriptor(@Nonnull final String authorityEntityID)
+            throws ResolutionException {
+        
+        final CriteriaSet criteriaSet = new CriteriaSet(
+                new EntityIdCriterion(authorityEntityID),
+                new ProtocolCriterion(SAMLConstants.SAML20P_NS),
+                new EntityRoleCriterion(AttributeAuthorityDescriptor.DEFAULT_ELEMENT_NAME));
+        try {
+            final AttributeAuthorityDescriptor descriptor =
+                    (AttributeAuthorityDescriptor) roleDescriptorResolver.resolveSingle(criteriaSet);
+            if (descriptor != null) {
+                log.debug("Successfully resolved AttributeAuthorityDescriptor for entityID: {}", authorityEntityID);
+                return descriptor;
+            }
+            log.warn("Failed to resolve AttributeAuthorityDescriptor for entityID: {}", authorityEntityID);
+            throw new ResolutionException("Failed to resolve AttributeAuthorityDescriptor for entityID: "
+                    + authorityEntityID);
+        } catch (final ResolverException e) {
+            log.warn("Fatal error resolving AttributeAuthorityDescriptor for entityID: {}", authorityEntityID, e);
+            throw new ResolutionException("Failed to resolve AttributeAuthorityDescriptor", e);
+        }
+
+    }
+
+    /**
+     * Resolve the attribute authority endpoint.
+     * 
+     * @param authorityEntityID the entityID of the attribute authority being processed
+     * @param authorityRoleDescriptor the role descriptor of the attribute authority being processed
+     * 
+     * @return the attribute authority endpoint
+     * 
+     * @throws ResolutionException if there is a fatal error during processing
+     */
+    @Nonnull private String resolveAuthorityEndpoint(@Nonnull final String authorityEntityID,
+            @Nonnull final AttributeAuthorityDescriptor authorityRoleDescriptor) throws ResolutionException {
+        
+        final RoleDescriptorCriterion roleDescriptorCriterion = new RoleDescriptorCriterion(authorityRoleDescriptor);
+
+        final AttributeService serviceTemplate = 
+                (AttributeService) XMLObjectSupport.buildXMLObject(
+                        AttributeService.DEFAULT_ELEMENT_NAME);
+        serviceTemplate.setBinding(SAMLConstants.SAML2_SOAP11_BINDING_URI);
+        
+        final EndpointCriterion<AttributeService> endpointCriterion = new EndpointCriterion<>(serviceTemplate, false);
+
+        final CriteriaSet criteriaSet = new CriteriaSet(roleDescriptorCriterion, endpointCriterion);
+
+        try {
+            final AttributeService service = authorityEndpointResolver.resolveSingle(criteriaSet);
+            if (service != null) {
+                final String location = service.getLocation();
+                if (location != null) {
+                    log.debug("Successfully resolved AttibuteService for entityID: {}", authorityEntityID);
+                    return location;
+                }
+                log.warn("Failed to resolve AttibuteService Location for entityID: {}", authorityEntityID);
+                throw new ResolutionException("Failed to resolve AttibuteService Location for entityID: "
+                        + authorityEntityID);
+            }
+            log.warn("Failed to resolve AttibuteService for entityID: {}", authorityEntityID);
+            throw new ResolutionException("Failed to resolve AttibuteService for entityID: " + authorityEntityID);
+        } catch (final ResolverException e) {
+            log.warn("Fatal error resolving AttributeService for entityID: {}", authorityEntityID, e);
+            throw new ResolutionException("Failed to resolve AttributeService", e);
+        }
+    }
+
+    /**
+     * Build the SOAP client operation context.
+     * 
+     * @param resolutionContext the current attribute resolution context
+     * @param dependencyAttributes the current set of dependency attributes
+     * @param roleDescriptor the role descriptor of the attribute authority being processed
+     * @param endpoint the attribute authority endpoint
+     * 
+     * @return the SOAP client operation context
+     * 
+     * @throws ResolutionException if there is a fatal error during processing
+     */
+    @Nonnull private InOutOperationContext buildOperationContext(@Nonnull final AttributeResolutionContext resolutionContext,
+            @Nonnull final Map<String, List<IdPAttributeValue>> dependencyAttributes,
+            @Nonnull final AttributeAuthorityDescriptor roleDescriptor, @Nonnull final String endpoint)
+                throws ResolutionException {
+        
+        final String selfEntityID = resolveSelfEntityID(resolutionContext, dependencyAttributes, roleDescriptor);
+
+        try {
+            return new SAMLSOAPClientContextBuilder<>()
+                    .setOutboundMessage(buildAttributeQueryMessage(resolutionContext, dependencyAttributes, roleDescriptor,
+                            endpoint, selfEntityID))
+                    .setProtocol(SAMLConstants.SAML20P_NS)
+                    .setPipelineName(getSOAPPipelineName())
+                    .setPeerRoleDescriptor(roleDescriptor)
+                    .setSelfEntityID(selfEntityID)
+                    .setSecurityConfigurationProfileId(getSOAPClientSecurityConfigurationProfileId())
+                    .build();
+        } catch (MessageException e) {
+            throw new ResolutionException("Fatal error building operation context for SAML AttributeQuery", e);
+        }
+    }
+
+    /**
+     * Resolve the entity ID of the protocol message issuer (this entity).
+     * 
+     * @param resolutionContext the current attribute resolution context
+     * @param dependencyAttributes the current set of dependency attributes
+     * @param roleDescriptor the role descriptor of the attribute authority being processed
+     * 
+     * @return the entity ID of the protocol message issuer (this entity)
+     * 
+     * @throws ResolutionException if there is a fatal error during processing
+     */
+    @Nonnull private String resolveSelfEntityID(@Nonnull final AttributeResolutionContext resolutionContext,
+            @Nonnull final Map<String, List<IdPAttributeValue>> dependencyAttributes,
+            @Nonnull final AttributeAuthorityDescriptor roleDescriptor) throws ResolutionException {
+        
+        final String entityID = getSelfEntityIDStrategy().resolve(resolutionContext, dependencyAttributes,
+                roleDescriptor);
+        if (entityID != null) {
+            return entityID;
+        }
+        throw new ResolutionException("Unable to resolve self entityID");
+    }
+
+    /**
+     * Build the attribute query message to be sent to the attribute authority.
+     * 
+     * @param resolutionContext the current attribute resolution context
+     * @param dependencyAttributes the current set of dependency attributes
+     * @param roleDescriptor the role descriptor of the attribute authority being processed
+     * @param endpoint the attribute authority endpoint
+     * @param selfEntityID the entity ID of the protocol message issuer (this entity)
+     * 
+     * @return the new attribute query message to be sent to the attribute authority
+     * 
+     * @throws ResolutionException if there is a fatal error during processing
+     */
+    @Nonnull private SAMLObject buildAttributeQueryMessage(@Nonnull final AttributeResolutionContext resolutionContext,
+            @Nonnull final Map<String, List<IdPAttributeValue>> dependencyAttributes,
+            @Nonnull final AttributeAuthorityDescriptor roleDescriptor,
+            @Nonnull final String endpoint, @Nonnull final String selfEntityID) throws ResolutionException {
+
+        final AttributeQuery query = 
+                (AttributeQuery) XMLObjectSupport.buildXMLObject(AttributeQuery.DEFAULT_ELEMENT_NAME);
+        
+        query.setID(idStrategy.generateIdentifier(true));
+        query.setDestination(endpoint);
+        query.setIssueInstant(Instant.now());
+        query.setIssuer(buildIssuer(selfEntityID));
+        
+        query.setSubject(buildSubject(resolutionContext, dependencyAttributes, roleDescriptor));
+
+        query.getAttributes().addAll(buildRequestAttributes(resolutionContext, dependencyAttributes, roleDescriptor));
+        
+        return query;
+    }
+    
+    /**
+     * Build the SAML protocol message Issuer element.
+     * 
+     * @param selfEntityID the entity ID of the protocol message issuer (this entity)
+     * 
+     * @return the Issuer element
+     * 
+     * @throws ResolutionException if there is a fatal error during processing
+     */
+    @Nonnull private Issuer buildIssuer(@Nonnull final String selfEntityID) throws ResolutionException {
+        final Issuer issuer = (Issuer) XMLObjectSupport.buildXMLObject(Issuer.DEFAULT_ELEMENT_NAME);
+        issuer.setValue(selfEntityID);
+        return issuer;
+    }
+
+    /**
+     * Build SAML protocol message Subject element.
+     * 
+     * @param resolutionContext the current attribute resolution context
+     * @param dependencyAttributes the current set of dependency attributes
+     * @param roleDescriptor the role descriptor of the protocol message issuer (this entity)
+     * 
+     * @return the subject to be used in the attribute query message
+     * 
+     * @throws ResolutionException if there is a fatal error during processing
+     */
+    @Nonnull private Subject buildSubject(@Nonnull final AttributeResolutionContext resolutionContext,
+            @Nonnull final Map<String, List<IdPAttributeValue>> dependencyAttributes,
+            @Nonnull final AttributeAuthorityDescriptor roleDescriptor) throws ResolutionException {
+
+        final Subject subject = getSubjectStrategy().resolve(resolutionContext, dependencyAttributes, roleDescriptor);
+        if (subject != null) {
+            try {
+                return XMLObjectSupport.cloneXMLObject(subject);
+            } catch (MarshallingException | UnmarshallingException e) {
+                throw new ResolutionException("Error cloning Subject", e);
+            }
+        }
+        throw new ResolutionException("Unable to resolve self entityID");
+    }
+
+    /**
+     * Build the list of requested attributes for the SAML protocol message.
+     * 
+     * @param resolutionContext the current attribute resolution context
+     * @param dependencyAttributes the current set of dependency attributes
+     * @param roleDescriptor the role descriptor of the attribute authority being processed
+     * 
+     * @return the list of requested attributes to be used in the attribute query message
+     * 
+     * @throws ResolutionException if there is a fatal error during processing
+     */
+    @Nonnull private List <Attribute> buildRequestAttributes(@Nonnull final AttributeResolutionContext resolutionContext,
+            @Nonnull final Map<String, List<IdPAttributeValue>> dependencyAttributes,
+            @Nonnull final AttributeAuthorityDescriptor roleDescriptor) throws ResolutionException {
+        
+        final List<Attribute> attributes = new ArrayList<>();
+        
+        for (final Attribute attr : getRequestedAttributes()) {
+            assert attr != null;
+            try {
+                attributes.add(XMLObjectSupport.cloneXMLObject(attr));
+            } catch (MarshallingException | UnmarshallingException e) {
+                throw new ResolutionException("Error cloning requested Attribute", e);
+            }
+        }
+
+        return attributes;
+    }
+
+}
diff --git a/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/impl/SAMLDataConnector.java b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/impl/SAMLDataConnector.java
new file mode 100644
index 000000000..0f4286c4f
--- /dev/null
+++ b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/impl/SAMLDataConnector.java
@@ -0,0 +1,253 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.attribute.resolver.dc.saml.impl;
+
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.saml.common.SAMLException;
+import org.opensaml.saml.saml2.core.Response;
+import org.opensaml.saml.saml2.encryption.Decrypter;
+import org.opensaml.security.SecurityException;
+import org.opensaml.soap.client.SOAPClient;
+import org.opensaml.soap.common.SOAPException;
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.attribute.IdPAttribute;
+import net.shibboleth.idp.attribute.resolver.DataConnector;
+import net.shibboleth.idp.attribute.resolver.ResolutionException;
+import net.shibboleth.idp.attribute.resolver.dc.impl.AbstractSearchDataConnector;
+import net.shibboleth.idp.attribute.resolver.dc.saml.ExecutableQuery;
+import net.shibboleth.idp.attribute.resolver.dc.saml.ResponseData;
+import net.shibboleth.idp.attribute.resolver.dc.saml.ResponseMappingStrategy;
+import net.shibboleth.idp.attribute.resolver.dc.saml.util.impl.AssertionValidationProcessor;
+import net.shibboleth.idp.attribute.resolver.dc.saml.util.impl.DecryptionProcessor;
+import net.shibboleth.idp.attribute.resolver.dc.saml.util.impl.SubjectMatchProcessor;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * A {@link DataConnector} implementation which queries a SAML attribute authority for attribute data.
+ */
+public class SAMLDataConnector extends AbstractSearchDataConnector<ExecutableQuery, ResponseMappingStrategy> {
+    
+    /** Logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(SAMLDataConnector.class);
+    
+    /** SOAP client. */
+    @NonnullAfterInit private SOAPClient soapClient;
+    
+    /** Decryption processor. */
+    @NonnullAfterInit private DecryptionProcessor decryptionProcessor;
+    
+    /** Assertion validation processor. */
+    @NonnullAfterInit private AssertionValidationProcessor assertionValidationProcessor;
+    
+    /** Assertion validation processor. */
+    @NonnullAfterInit private SubjectMatchProcessor subjectMatchProcessor;
+    
+    /** Flag indicating whether to perform matching of Assertion subjects against the value in the query. */
+    private boolean subjectMatch = false;
+    
+    /**
+     * Get the SOAP client instance.
+     * 
+     * @return the SOAP client
+     */
+    @NonnullAfterInit public SOAPClient getSOAPClient() {
+        return soapClient;
+    }
+
+    /**
+     * Set the SOAP client instance.
+     * 
+     * @param client the SOAP client
+     */
+    public void setSOAPClient(@Nullable final SOAPClient client) {
+        checkSetterPreconditions();
+        soapClient = client;
+    }
+    
+    /**
+     * Get the decryption processor.
+     * 
+     * @return the decryption processor
+     */
+    @NonnullAfterInit public DecryptionProcessor getDecryptionProcessor() {
+        return decryptionProcessor;
+    }
+
+    /**
+     * Set the decryption processor
+     * 
+     * @param processor the Decryption processor
+     */
+    public void setDecryptionProcessor(@Nullable final DecryptionProcessor processor) {
+        checkSetterPreconditions();
+        decryptionProcessor = processor;
+    }
+    
+    /**
+     * Get the Assertion validation processor.
+     * 
+     * @return the Assertion validation processor
+     */
+    @NonnullAfterInit public AssertionValidationProcessor getAssertionValidationProcessor() {
+        return assertionValidationProcessor;
+    }
+
+    /**
+     * Set the Assertion validation processor
+     * 
+     * @param processor the Assertion validation processor
+     */
+    public void setAssertionValidationProcessor(@Nullable final AssertionValidationProcessor processor) {
+        checkSetterPreconditions();
+        assertionValidationProcessor = processor;
+    }
+    
+    /**
+     * Get the Subject match processor.
+     * 
+     * @return the Subject match processor
+     */
+    @NonnullAfterInit public SubjectMatchProcessor getSubjectMatchProcessor() {
+        return subjectMatchProcessor;
+    }
+
+    /**
+     * Set the Subject match processor
+     * 
+     * @param processor the Subjet match processor
+     */
+    public void setSubjectMatchProcessor(@Nullable final SubjectMatchProcessor processor) {
+        checkSetterPreconditions();
+        subjectMatchProcessor = processor;
+    }
+    
+    /**
+     * Get the flag indicating whether to perform matching of Assertion subjects against the value in the query.
+     * 
+     * <p>
+     * Default is <code>false</code>.
+     * </p>
+     * 
+     * @return returns the flag value
+     */
+    public boolean isSubjectMatch() {
+        return subjectMatch;
+    }
+
+    /**
+     * Set the flag indicating whether to perform matching of Assertion subjects against the value in the query.
+     * 
+     * <p>
+     * Default is <code>false</code>.
+     * </p>
+     * 
+     * @param flag the flag value
+     */
+    public void setSubjectMatch(boolean flag) {
+        checkSetterPreconditions();
+        subjectMatch = flag;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        
+        if (getSOAPClient() == null) {
+            throw new ComponentInitializationException("SOAPClient was null");
+        }
+        if (getDecryptionProcessor() == null) {
+            throw new ComponentInitializationException("DecryptionProcessor was null");
+        }
+        if (getAssertionValidationProcessor() == null) {
+            throw new ComponentInitializationException("AssertionValidationProcessor was null");
+        }
+        if (getSubjectMatchProcessor() == null) {
+            throw new ComponentInitializationException("SubjectMatchProcessor was null");
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    @Nullable
+    protected Map<String, IdPAttribute> retrieveAttributes(@Nonnull final ExecutableQuery executable) throws ResolutionException {
+        try {
+            final ResponseMappingStrategy strategy = getMappingStrategy();
+            final SOAPClient localClient = getSOAPClient();
+            assert strategy != null && localClient != null;
+
+            final ResponseData responseData = executable.execute(localClient);
+
+            processResponse(responseData);
+
+            final Map<String,IdPAttribute> attributes = strategy.map(responseData);
+
+            return attributes;
+        } catch (final SAMLException | SOAPException | SecurityException e) {
+            throw new ResolutionException(getLogPrefix() + " SAML AttributeQuery failed", e);
+        }
+    }
+
+    /**
+     * Process the SAML response message.
+     * 
+     * @param responseData the response data
+     * 
+     * @throws ResolutionException if there is a fatal error during processing
+     */
+    protected void processResponse(@Nonnull final ResponseData responseData) throws ResolutionException {
+        final Response response = responseData.getResponse();
+
+        // Encryption on an AttributeQuery response probably isn't that common, so be efficient.
+        // Goal is to only build the Decrypter if/when needed, and only do it once
+        // for both Assertions and Assertion content.
+        DecryptionProcessor decryptionProcessor = getDecryptionProcessor();
+        Decrypter decrypter = null;
+        if (decryptionProcessor.haveEncryptedAssertions(response)) {
+            decrypter = decryptionProcessor.buildDecrypter(responseData);
+            decryptionProcessor.decryptAssertions(response, decrypter);
+        }
+        
+        getAssertionValidationProcessor().validateAssertions(response, responseData.getSOAPClientContext());
+        if (response.getAssertions().isEmpty()) {
+            log.warn("{} SAML Response contained no valid Assertions", getLogPrefix());
+            throw new ResolutionException("SAML Response contained no valid Assertions");
+        }
+        
+        if (decryptionProcessor.haveEncryptedContent(response)) {
+            if (decrypter == null) {
+                decrypter = decryptionProcessor.buildDecrypter(responseData);
+            }
+            decryptionProcessor.decryptAssertionContent(response, decrypter);
+        }
+        
+        if (isSubjectMatch()) {
+            getSubjectMatchProcessor().process(responseData);
+        } else {
+            log.debug("Subject match is disabled, skipping");
+            return;
+        }
+    }
+
+
+
+}
\ No newline at end of file
diff --git a/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/impl/SelfEntityIDResolver.java b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/impl/SelfEntityIDResolver.java
new file mode 100644
index 000000000..2163a9f8b
--- /dev/null
+++ b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/impl/SelfEntityIDResolver.java
@@ -0,0 +1,40 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.attribute.resolver.dc.saml.impl;
+
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.saml.saml2.metadata.AttributeAuthorityDescriptor;
+
+import net.shibboleth.idp.attribute.IdPAttributeValue;
+import net.shibboleth.idp.attribute.resolver.ResolutionException;
+import net.shibboleth.idp.attribute.resolver.context.AttributeResolutionContext;
+
+/**
+ * Interface for a component which resolves the self entityID to use when executing
+ * an attribute query.
+ */
+public interface SelfEntityIDResolver {
+    
+    @Nullable
+    String resolve(@Nonnull final AttributeResolutionContext resolutionContext,
+            @Nonnull final Map<String, List<IdPAttributeValue>> dependencyAttributes,
+            @Nonnull final AttributeAuthorityDescriptor roleDescriptor) throws ResolutionException;
+
+}
diff --git a/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/impl/SimpleAggregationSAMLDataConnector.java b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/impl/SimpleAggregationSAMLDataConnector.java
new file mode 100644
index 000000000..d44a9e768
--- /dev/null
+++ b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/impl/SimpleAggregationSAMLDataConnector.java
@@ -0,0 +1,203 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.attribute.resolver.dc.saml.impl;
+
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.attribute.IdPAttribute;
+import net.shibboleth.idp.attribute.IdPAttributeSupport;
+import net.shibboleth.idp.attribute.IdPAttributeValue;
+import net.shibboleth.idp.attribute.StringAttributeValue;
+import net.shibboleth.idp.attribute.resolver.AbstractDataConnector;
+import net.shibboleth.idp.attribute.resolver.DataConnector;
+import net.shibboleth.idp.attribute.resolver.PluginDependencySupport;
+import net.shibboleth.idp.attribute.resolver.ResolutionException;
+import net.shibboleth.idp.attribute.resolver.context.AttributeResolutionContext;
+import net.shibboleth.idp.attribute.resolver.context.AttributeResolverWorkContext;
+import net.shibboleth.idp.attribute.resolver.dc.saml.util.impl.AttributeAuthorityEntityIDContext;
+import net.shibboleth.idp.attribute.resolver.dc.saml.util.impl.AttributeAuthorityEntityIDReference;
+import net.shibboleth.idp.attribute.resolver.dc.saml.util.impl.AttributeAuthorityEntityIDSource;
+import net.shibboleth.idp.attribute.resolver.dc.saml.util.impl.AttributeAuthorityEntityIDValue;
+import net.shibboleth.shared.annotation.constraint.NotLive;
+import net.shibboleth.shared.annotation.constraint.Unmodifiable;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * A {@link DataConnector} implementation which orchestrates calling {@link SAMLDataConnector}
+ * for each of a resolved list of attribute authority entityIDs, and aggregates the resolved attribute data.
+ */
+public class SimpleAggregationSAMLDataConnector extends AbstractDataConnector {
+    
+    /** Logger. */
+    private Logger log = LoggerFactory.getLogger(SimpleAggregationSAMLDataConnector.class);
+    
+    /** The SAML Data Connector which performs the actual attribute resolution operations. */
+    private SAMLDataConnector queryConnector;
+    
+    /** Sources of attribute authority entityIDs. */
+    @Nonnull private List<AttributeAuthorityEntityIDSource> entityIDSources = CollectionSupport.emptyList();
+
+    /**
+     * Get the {@link SAMLDataConnector} instance to use. 
+     * 
+     * @return the data connector instance
+     */
+    public SAMLDataConnector getQueryConnector() {
+        return queryConnector;
+    }
+
+    /**
+     * Set the {@link SAMLDataConnector} instance to use. 
+     * 
+     * @param connector the data connector instance
+     */
+    public void setQueryConnector(SAMLDataConnector connector) {
+        checkSetterPreconditions();
+        queryConnector = connector;
+    }
+
+    /**
+     * Get the attribute authority entityID sources. 
+     * 
+     * @return the entityID sources
+     */
+    @Nonnull @NotLive @Unmodifiable public List<AttributeAuthorityEntityIDSource> getEntityIDSources() {
+        return entityIDSources;
+    }
+
+    /**
+     * Set the attribute authority entityID sources. 
+     * 
+     * @param sources the attribute authority entityID sources
+     */
+    public void setEntityIDSources(@Nullable final List<AttributeAuthorityEntityIDSource> sources) {
+        checkComponentActive();
+
+        if (sources == null) {
+            entityIDSources = CollectionSupport.emptyList();
+        } else {
+            entityIDSources = sources.stream()
+                    .filter(Objects::nonNull)
+                    .collect(CollectionSupport.nonnullCollector(Collectors.toUnmodifiableList())).get();
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    @Nullable
+    protected Map<String, IdPAttribute> doDataConnectorResolve(@Nonnull final AttributeResolutionContext resolutionContext,
+            @Nonnull final AttributeResolverWorkContext workContext) throws ResolutionException {
+        checkComponentActive();
+        
+        final List<IdPAttribute> results = new LinkedList<>();
+        
+        final List<String> authtorityEntityIDs = resolveAuthorityEntityIDs(resolutionContext, workContext);
+        log.trace("{} Resolved {} attribute authority entityIDs: {}",
+                getLogPrefix(), authtorityEntityIDs.size(), authtorityEntityIDs);
+        
+        for (final String authorityEntityID : authtorityEntityIDs) {
+            assert authorityEntityID != null;
+            try {
+                log.debug("Executing SAML attribute query against authority entityID: {}", authorityEntityID);
+                resolutionContext.addSubcontext(new AttributeAuthorityEntityIDContext(authorityEntityID), true);
+                final Map<String,IdPAttribute> authorityResults = getQueryConnector().resolve(resolutionContext);
+                if (authorityResults != null) {
+                    log.debug("{} Authority returned {} unique attributes",
+                            getLogPrefix(), authorityResults.keySet().size());
+                    results.addAll(authorityResults.values());
+                } else {
+                    log.debug("{} Authority returned no attributes", getLogPrefix());
+                }
+            } catch (final Exception e) {
+                log.warn("{} Fatal error executing SAML 2 attribute query against authority: {}",
+                        getLogPrefix(), authorityEntityID);
+            } finally {
+                resolutionContext.removeSubcontext(AttributeAuthorityEntityIDContext.class);
+            }
+        }
+
+        final Map<String,IdPAttribute> mergedResults = IdPAttributeSupport.toMapMergeDuplicates(results);
+
+        if (log.isTraceEnabled()) {
+            log.trace("{} Decoded {} total unique aggregated attribute IDs from all attribute authorities:",
+                    getLogPrefix(), mergedResults.keySet().size()); 
+            for (final String attributeID : mergedResults.keySet().stream().sorted().toList()) {
+                log.trace("\tAttribute ID '{}', # of values: {}",
+                        attributeID, mergedResults.get(attributeID).getValues().size());
+            }
+        }
+        
+        return mergedResults;
+    }
+
+    /**
+     * Resolve the attribute authority entityIDs to against which to query.
+     * 
+     * @param resolutionContext current resolution context
+     * @param workContext current resolver work context
+     * 
+     * @return the effective list of entityIDs to query
+     */
+    @Nonnull
+    protected List<String> resolveAuthorityEntityIDs(@Nonnull final AttributeResolutionContext resolutionContext,
+            @Nonnull final AttributeResolverWorkContext workContext) {
+
+        final List<String> entityIDs = new ArrayList<>();
+        
+        // Don't resolve these unless/until we actually need them below
+        Map<String, List<IdPAttributeValue>> dependencyAttributes = null;
+        
+        for (AttributeAuthorityEntityIDSource source : getEntityIDSources()) {
+            if (source instanceof AttributeAuthorityEntityIDValue) {
+                entityIDs.add(source.getValue());
+            } else if (source instanceof AttributeAuthorityEntityIDReference) {
+                final String reference = source.getValue();
+
+                if (dependencyAttributes == null) {
+                    dependencyAttributes = PluginDependencySupport.getAllAttributeValues(workContext,
+                            getAttributeDependencies(), getDataConnectorDependencies());
+                }
+
+                if (dependencyAttributes.containsKey(reference)) {
+                    // We currently only support StringAttributeValue types here
+                    entityIDs.addAll(dependencyAttributes.get(reference).stream()
+                            .filter(StringAttributeValue.class::isInstance)
+                            .map(StringAttributeValue.class::cast)
+                            .map(StringAttributeValue::getValue)
+                            .toList());
+                }
+            } else {
+                log.warn("{} Saw AttributeAuthorityEntityIDSource of unsupported type, could not process: {}",
+                        getLogPrefix(), source.getClass().getName());
+            }
+        }
+
+        return entityIDs.stream()
+                .distinct()
+                .collect(CollectionSupport.nonnullCollector(Collectors.toUnmodifiableList())).get();
+    }
+
+}
diff --git a/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/impl/SubjectResolver.java b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/impl/SubjectResolver.java
new file mode 100644
index 000000000..2a654e05b
--- /dev/null
+++ b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/impl/SubjectResolver.java
@@ -0,0 +1,41 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.attribute.resolver.dc.saml.impl;
+
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.saml.saml2.core.Subject;
+import org.opensaml.saml.saml2.metadata.AttributeAuthorityDescriptor;
+
+import net.shibboleth.idp.attribute.IdPAttributeValue;
+import net.shibboleth.idp.attribute.resolver.ResolutionException;
+import net.shibboleth.idp.attribute.resolver.context.AttributeResolutionContext;
+
+/**
+ * Interface for a component which resolves the {@link Subject} to use when executing
+ * an attribute query.
+ */
+public interface SubjectResolver {
+    
+    @Nullable
+    Subject resolve(@Nonnull final AttributeResolutionContext resolutionContext,
+            @Nonnull final Map<String, List<IdPAttributeValue>> dependencyAttributes,
+            @Nonnull final AttributeAuthorityDescriptor roleDescriptor) throws ResolutionException;
+
+}
diff --git a/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/impl/package-info.java b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/impl/package-info.java
new file mode 100644
index 000000000..231ae28e8
--- /dev/null
+++ b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/impl/package-info.java
@@ -0,0 +1,22 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * A {@link net.shibboleth.idp.attribute.resolver.DataConnector} implementation that
+ * queries data from a SAML attribute authority.
+ */
+ at NonnullElements
+package net.shibboleth.idp.attribute.resolver.dc.saml.impl;
+
+import net.shibboleth.shared.annotation.constraint.NonnullElements;
diff --git a/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/plugin/impl/ChainingAuthorityEntityIDResolver.java b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/plugin/impl/ChainingAuthorityEntityIDResolver.java
new file mode 100644
index 000000000..16bfa0e83
--- /dev/null
+++ b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/plugin/impl/ChainingAuthorityEntityIDResolver.java
@@ -0,0 +1,106 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.attribute.resolver.dc.saml.plugin.impl;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.attribute.IdPAttributeValue;
+import net.shibboleth.idp.attribute.resolver.ResolutionException;
+import net.shibboleth.idp.attribute.resolver.context.AttributeResolutionContext;
+import net.shibboleth.idp.attribute.resolver.dc.saml.impl.AttributeAuthorityEntityIDResolver;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.NotLive;
+import net.shibboleth.shared.annotation.constraint.Unmodifiable;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.component.AbstractInitializableComponent;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * An implementation of {@link AttributeAuthorityEntityIDResolver} which implements a chain-of-responsibility pattern
+ * using a list of configured resolvers.
+ */
+public class ChainingAuthorityEntityIDResolver extends AbstractInitializableComponent
+        implements AttributeAuthorityEntityIDResolver {
+    
+    /** Logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(ChainingAuthorityEntityIDResolver.class);
+    
+    /** List of chain members. */
+    @NonnullAfterInit private List<AttributeAuthorityEntityIDResolver> resolvers;
+
+    /**
+     * Get the list of chain members.
+     * 
+     * @return the list of chain members
+     */
+    @NotLive
+    @Unmodifiable
+    @NonnullAfterInit public List<AttributeAuthorityEntityIDResolver> getResolvers() {
+        return resolvers;
+    }
+
+    /**
+     * Set the list of chain members.
+     * 
+     * @param chain the chain of members
+     */
+    public void setResolvers(@Nullable final List<AttributeAuthorityEntityIDResolver> chain) {
+        checkSetterPreconditions();
+        if (chain == null) {
+            resolvers = CollectionSupport.emptyList();
+        } else {
+            resolvers = chain.stream()
+                    .filter(Objects::nonNull)
+                    .collect(CollectionSupport.nonnullCollector(Collectors.toUnmodifiableList())).get();
+        }
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+
+        if (getResolvers() == null) {
+            throw new ComponentInitializationException("List chain member resolvers was null");
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    @Nullable
+    public String resolve(@Nonnull final AttributeResolutionContext resolutionContext,
+            @Nonnull final Map<String, List<IdPAttributeValue>> dependencyAttributes) throws ResolutionException {
+        checkComponentActive();
+        
+        for (final AttributeAuthorityEntityIDResolver resolver : getResolvers()) {
+            final String entityID = resolver.resolve(resolutionContext, dependencyAttributes);
+            if (entityID != null) {
+                log.debug("Resolved entityID '{}' using resolver impl: {}", entityID, resolver.getClass().getName());
+                return entityID;
+            }
+        }
+        return null;
+    }
+
+}
diff --git a/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/plugin/impl/ChainingSelfEntityIDResolver.java b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/plugin/impl/ChainingSelfEntityIDResolver.java
new file mode 100644
index 000000000..788135245
--- /dev/null
+++ b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/plugin/impl/ChainingSelfEntityIDResolver.java
@@ -0,0 +1,108 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.attribute.resolver.dc.saml.plugin.impl;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.saml.saml2.metadata.AttributeAuthorityDescriptor;
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.attribute.IdPAttributeValue;
+import net.shibboleth.idp.attribute.resolver.ResolutionException;
+import net.shibboleth.idp.attribute.resolver.context.AttributeResolutionContext;
+import net.shibboleth.idp.attribute.resolver.dc.saml.impl.SelfEntityIDResolver;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.NotLive;
+import net.shibboleth.shared.annotation.constraint.Unmodifiable;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.component.AbstractInitializableComponent;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * An implementation of {@link SelfEntityIDResolver} which implements a chain-of-responsibility pattern
+ * using a list of configured resolvers.
+ */
+public class ChainingSelfEntityIDResolver extends AbstractInitializableComponent
+        implements SelfEntityIDResolver {
+    
+    /** Logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(ChainingSelfEntityIDResolver.class);
+    
+    /** List of chain members. */
+    @NonnullAfterInit private List<SelfEntityIDResolver> resolvers;
+
+    /**
+     * Get the list of chain members.
+     * 
+     * @return the list of chain members
+     */
+    @NotLive
+    @Unmodifiable
+    @NonnullAfterInit public List<SelfEntityIDResolver> getResolvers() {
+        return resolvers;
+    }
+
+    /**
+     * Set the list of chain members.
+     * 
+     * @param chain the chain of members
+     */
+    public void setResolvers(@Nullable final List<SelfEntityIDResolver> chain) {
+        checkSetterPreconditions();
+        if (chain == null) {
+            resolvers = CollectionSupport.emptyList();
+        } else {
+            resolvers = chain.stream()
+                    .filter(Objects::nonNull)
+                    .collect(CollectionSupport.nonnullCollector(Collectors.toUnmodifiableList())).get();
+        }
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+
+        if (getResolvers() == null) {
+            throw new ComponentInitializationException("List chain member resolvers was null");
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    @Nullable
+    public String resolve(@Nonnull final AttributeResolutionContext resolutionContext,
+            @Nonnull final Map<String, List<IdPAttributeValue>> dependencyAttributes,
+            @Nonnull final AttributeAuthorityDescriptor roleDescriptor) throws ResolutionException {
+        checkComponentActive();
+        
+        for (final SelfEntityIDResolver resolver : getResolvers()) {
+            final String entityID = resolver.resolve(resolutionContext, dependencyAttributes, roleDescriptor);
+            if (entityID != null) {
+                log.debug("Resolved entityID '{}' using resolver impl: {}", entityID, resolver.getClass().getName());
+                return entityID;
+            }
+        }
+        return null;
+    }
+
+}
diff --git a/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/plugin/impl/ChainingSubjectResolver.java b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/plugin/impl/ChainingSubjectResolver.java
new file mode 100644
index 000000000..7245a34f1
--- /dev/null
+++ b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/plugin/impl/ChainingSubjectResolver.java
@@ -0,0 +1,108 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.attribute.resolver.dc.saml.plugin.impl;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.saml.saml2.core.Subject;
+import org.opensaml.saml.saml2.metadata.AttributeAuthorityDescriptor;
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.attribute.IdPAttributeValue;
+import net.shibboleth.idp.attribute.resolver.ResolutionException;
+import net.shibboleth.idp.attribute.resolver.context.AttributeResolutionContext;
+import net.shibboleth.idp.attribute.resolver.dc.saml.impl.SubjectResolver;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.NotLive;
+import net.shibboleth.shared.annotation.constraint.Unmodifiable;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.component.AbstractInitializableComponent;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * An implementation of {@link SubjectResolver} which implements a chain-of-responsibility pattern
+ * using a list of configured resolvers.
+ */
+public class ChainingSubjectResolver extends AbstractInitializableComponent implements SubjectResolver {
+    
+    /** Logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(ChainingSubjectResolver.class);
+    
+    /** List of chain members. */
+    @NonnullAfterInit private List<SubjectResolver> resolvers;
+
+    /**
+     * Get the list of chain members.
+     * 
+     * @return the list of chain members
+     */
+    @NotLive
+    @Unmodifiable
+    @NonnullAfterInit public List<SubjectResolver> getResolvers() {
+        return resolvers;
+    }
+
+    /**
+     * Set the list of chain members.
+     * 
+     * @param chain the chain of members
+     */
+    public void setResolvers(@Nullable final List<SubjectResolver> chain) {
+        checkSetterPreconditions();
+        if (chain == null) {
+            resolvers = CollectionSupport.emptyList();
+        } else {
+            resolvers = chain.stream()
+                    .filter(Objects::nonNull)
+                    .collect(CollectionSupport.nonnullCollector(Collectors.toUnmodifiableList())).get();
+        }
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+
+        if (getResolvers() == null) {
+            throw new ComponentInitializationException("List chain member resolvers was null");
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    @Nullable
+    public Subject resolve(@Nonnull final AttributeResolutionContext resolutionContext,
+            @Nonnull final Map<String, List<IdPAttributeValue>> dependencyAttributes,
+            @Nonnull final AttributeAuthorityDescriptor roleDescriptor) throws ResolutionException {
+        checkComponentActive();
+        
+        for (final SubjectResolver resolver : getResolvers()) {
+            final Subject subject = resolver.resolve(resolutionContext, dependencyAttributes, roleDescriptor);
+            if (subject != null) {
+                log.debug("Resolved Subject using resolver impl: {}", resolver.getClass().getName());
+                return subject;
+            }
+        }
+        return null;
+    }
+
+}
diff --git a/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/plugin/impl/ContextAuthorityEntityIDResolver.java b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/plugin/impl/ContextAuthorityEntityIDResolver.java
new file mode 100644
index 000000000..9d9cadcd4
--- /dev/null
+++ b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/plugin/impl/ContextAuthorityEntityIDResolver.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.attribute.resolver.dc.saml.plugin.impl;
+
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import net.shibboleth.idp.attribute.IdPAttributeValue;
+import net.shibboleth.idp.attribute.resolver.ResolutionException;
+import net.shibboleth.idp.attribute.resolver.context.AttributeResolutionContext;
+import net.shibboleth.idp.attribute.resolver.dc.saml.impl.AttributeAuthorityEntityIDResolver;
+import net.shibboleth.idp.attribute.resolver.dc.saml.util.impl.AttributeAuthorityEntityIDContext;
+
+/**
+ * An implementation of {@link AttributeAuthorityEntityIDResolver} which resolves
+ * from the {@link AttributeAuthorityEntityIDContext} child of the input {@link AttributeResolutionContext}
+ * if present.
+ */
+public class ContextAuthorityEntityIDResolver implements AttributeAuthorityEntityIDResolver {
+
+    /** {@inheritDoc} */
+    @Override
+    @Nullable
+    public String resolve(@Nonnull final AttributeResolutionContext resolutionContext,
+            @Nonnull Map<String, List<IdPAttributeValue>> dependencyAttributes) throws ResolutionException {
+        
+        final AttributeAuthorityEntityIDContext context =
+                resolutionContext.getSubcontext(AttributeAuthorityEntityIDContext.class);
+        if (context != null) {
+            return context.getAuthorityEntityID();
+        }
+        return null;
+    }
+
+}
diff --git a/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/plugin/impl/CriteriaDecryptionConfigurationLookup.java b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/plugin/impl/CriteriaDecryptionConfigurationLookup.java
new file mode 100644
index 000000000..90c4e01a5
--- /dev/null
+++ b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/plugin/impl/CriteriaDecryptionConfigurationLookup.java
@@ -0,0 +1,124 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.attribute.resolver.dc.saml.plugin.impl;
+
+import java.util.List;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.criterion.ProfileIDCriterion;
+import org.opensaml.saml.criterion.RoleDescriptorCriterion;
+import org.opensaml.xmlsec.DecryptionConfiguration;
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.attribute.resolver.dc.saml.ResponseData;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.component.AbstractInitializableComponent;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.primitive.StringSupport;
+import net.shibboleth.shared.resolver.CriteriaSet;
+import net.shibboleth.shared.resolver.Resolver;
+import net.shibboleth.shared.resolver.ResolverException;
+
+/**
+ * A component which resolves a list of {@link DecryptionConfiguration} using a delegated criteria-based resolver.
+ */
+public class CriteriaDecryptionConfigurationLookup extends AbstractInitializableComponent
+    implements Function<ResponseData,List<DecryptionConfiguration>> {
+    
+    /** Logger. */
+    @Nonnull private Logger log = LoggerFactory.getLogger(ChainingSelfEntityIDResolver.class);
+    
+    /** Resolver for decryption configurations. */
+    @NonnullAfterInit private Resolver<List<DecryptionConfiguration>, CriteriaSet> configurationResolver;
+    
+    /** Security configuration profile ID. */
+    @NonnullAfterInit private String securityConfigurationProfileId;
+    
+    /**
+     * Get the resolver delegate.
+     * 
+     * @return the resolver
+     */
+    @NonnullAfterInit public Resolver<List<DecryptionConfiguration>,CriteriaSet> getConfigurationResolver() {
+        return configurationResolver;
+    }
+    
+    /**
+     * Set the resolver delegate.
+     * 
+     * @param resolver the resolver instance
+     */
+    public void setConfigurationResolver(@Nullable final Resolver<List<DecryptionConfiguration>, CriteriaSet> resolver) {
+        checkSetterPreconditions();
+        configurationResolver = resolver;
+    }
+    
+    /**
+     * Set the security configuration profile ID to use.
+     * 
+     * @param profileId the profile ID, or null
+     */
+    public void setSecurityConfigurationProfileId(@Nullable final String profileId) {
+        checkSetterPreconditions();
+        securityConfigurationProfileId = StringSupport.trimOrNull(profileId);
+    }
+    
+    /** {@inheritDoc} */
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        
+        if (configurationResolver == null) {
+            throw new ComponentInitializationException("DecryptionConfiguration resolver cannot be null");
+        }
+        if (securityConfigurationProfileId == null) {
+            throw new ComponentInitializationException("Security config profile ID cannot be null");
+        }
+
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    @Nullable
+    public List<DecryptionConfiguration> apply(@Nullable final ResponseData responseData) {
+        checkComponentActive();
+        
+        // Implementing java.util.Function won't allow for this arg to be @Nonnull
+        if (responseData == null) {
+            return CollectionSupport.emptyList();
+        }
+
+        final CriteriaSet criteria = new CriteriaSet(
+                new RoleDescriptorCriterion(responseData.getAttributeAuthorityRoleDescriptor()),
+                new ProfileIDCriterion(securityConfigurationProfileId));
+
+        try {
+            final List<DecryptionConfiguration> configs = getConfigurationResolver().resolveSingle(criteria);
+            if (configs == null) {
+                log.warn("Failed to resolve list of DecryptionConfiguration from criteria");
+                return null;
+            }
+            return configs;
+        } catch (final ResolverException e) {
+            log.warn("Fatal error resolving DecryptionConfiguration via criteria");
+            return null;
+        }
+    }
+
+}
diff --git a/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/plugin/impl/CriteriaSelfEntityIDResolver.java b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/plugin/impl/CriteriaSelfEntityIDResolver.java
new file mode 100644
index 000000000..1085d1a6c
--- /dev/null
+++ b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/plugin/impl/CriteriaSelfEntityIDResolver.java
@@ -0,0 +1,101 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.attribute.resolver.dc.saml.plugin.impl;
+
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.saml.criterion.RoleDescriptorCriterion;
+import org.opensaml.saml.saml2.metadata.AttributeAuthorityDescriptor;
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.attribute.IdPAttributeValue;
+import net.shibboleth.idp.attribute.resolver.ResolutionException;
+import net.shibboleth.idp.attribute.resolver.context.AttributeResolutionContext;
+import net.shibboleth.idp.attribute.resolver.dc.saml.impl.SelfEntityIDResolver;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.component.AbstractInitializableComponent;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.resolver.CriteriaSet;
+import net.shibboleth.shared.resolver.Resolver;
+import net.shibboleth.shared.resolver.ResolverException;
+
+/**
+ * An implementation of {@link SelfEntityIDResolver} which uses a delegated resolver
+ * based on an input {@link CriteriaSet}.
+ */
+public class CriteriaSelfEntityIDResolver extends AbstractInitializableComponent implements SelfEntityIDResolver {
+    
+    /** Logger. */
+    @Nonnull private Logger log = LoggerFactory.getLogger(ChainingSelfEntityIDResolver.class);
+    
+    /** Resolver for the self entityID, based on the peer entity data. */
+    @NonnullAfterInit private Resolver<String, CriteriaSet> selfEntityIDResolver;
+    
+    /**
+     * Get the resolver for the self entityID.
+     * 
+     * @return the resolver
+     */
+    @NonnullAfterInit public Resolver<String,CriteriaSet> getSelfEntityIDResolver() {
+        return selfEntityIDResolver;
+    }
+    
+    /**
+     * Set the resolver for the self entityID.
+     * 
+     * @param resolver the resolver instance
+     */
+    public void setSelfEntityIDResolver(@Nullable final Resolver<String, CriteriaSet> resolver) {
+        checkSetterPreconditions();
+        selfEntityIDResolver = resolver;
+    }
+    
+    /** {@inheritDoc} */
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        
+        if (selfEntityIDResolver == null) {
+            throw new ComponentInitializationException("Self entityID resolver cannot be null");
+        }
+
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    @Nullable
+    public String resolve(@Nonnull final AttributeResolutionContext resolutionContext,
+            @Nonnull final Map<String, List<IdPAttributeValue>> dependencyAttributes,
+            @Nonnull final AttributeAuthorityDescriptor roleDescriptor) throws ResolutionException {
+        checkComponentActive();
+
+        final CriteriaSet criteria = new CriteriaSet(new RoleDescriptorCriterion(roleDescriptor));
+        try {
+            final String selfEntityID = getSelfEntityIDResolver().resolveSingle(criteria);
+            if (selfEntityID == null) {
+                throw new ResolutionException("Unable to resolve self entityID from peer RoleDescriptor");
+            }
+            log.debug("Resolved self entityID via criteria: {}", selfEntityID);
+            return selfEntityID;
+        } catch (final ResolverException e) {
+            throw new ResolutionException("Fatal error resolving self entityID from peer RoleDescriptor", e);
+        }
+    }
+
+}
diff --git a/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/plugin/impl/DependencyAttributeSubjectResolver.java b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/plugin/impl/DependencyAttributeSubjectResolver.java
new file mode 100644
index 000000000..f9fbd324b
--- /dev/null
+++ b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/plugin/impl/DependencyAttributeSubjectResolver.java
@@ -0,0 +1,249 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.attribute.resolver.dc.saml.plugin.impl;
+
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.core.xml.io.MarshallingException;
+import org.opensaml.core.xml.io.UnmarshallingException;
+import org.opensaml.core.xml.util.XMLObjectSupport;
+import org.opensaml.saml.saml2.core.NameID;
+import org.opensaml.saml.saml2.core.Subject;
+import org.opensaml.saml.saml2.metadata.AttributeAuthorityDescriptor;
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.attribute.IdPAttributeValue;
+import net.shibboleth.idp.attribute.ScopedStringAttributeValue;
+import net.shibboleth.idp.attribute.StringAttributeValue;
+import net.shibboleth.idp.attribute.XMLObjectAttributeValue;
+import net.shibboleth.idp.attribute.resolver.ResolutionException;
+import net.shibboleth.idp.attribute.resolver.context.AttributeResolutionContext;
+import net.shibboleth.idp.attribute.resolver.dc.saml.impl.SubjectResolver;
+import net.shibboleth.shared.annotation.constraint.NotLive;
+import net.shibboleth.shared.annotation.constraint.Unmodifiable;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.component.AbstractInitializableComponent;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.primitive.StringSupport;
+import net.shibboleth.shared.xml.SerializeSupport;
+
+/**
+ * An implementation of {@link SubjectResolver} which returns a {@link Subject} using a {@link NameID} built
+ * using the value from one of an ordered list of dependency attributes.
+ * 
+ * <p>
+ * Dependency attributes are searched in the order specified and the first one found to have a value
+ * of a supported type will be used. Supported types are:
+ * </p>
+ * <ul>
+ * <li>{@link StringAttributeValue}</li>
+ * <li>{@link ScopedStringAttributeValue}</li>
+ * <li>{@link XMLObjectAttributeValue} containing a {@link NameID}</li>
+ * </ul>
+ */
+public class DependencyAttributeSubjectResolver extends AbstractInitializableComponent  implements SubjectResolver {
+    
+    /** Logger. */
+    @Nonnull final Logger log = LoggerFactory.getLogger(DependencyAttributeSubjectResolver.class);
+    
+    /** List of dependency attribute IDs to search for a value. */
+    @Nonnull private List<String> attributeIDs = CollectionSupport.emptyList();
+    
+    /** Map allowing to customize the NameID format URI on a per-attributeID basis. */
+    @Nonnull private Map<String,String> nameIDFormatMap = CollectionSupport.emptyMap();
+
+    /** The default format URI used to construct the NameID if a custom format for the attribute ID is
+     * not defined in {@link #getNameIDFormatMap()}. */
+    @Nullable private String defaultNameIDFormat;
+    
+    /**
+     * Get the list of dependency attribute IDs to search for a value.
+     * 
+     * @return the list of attribute IDs, may be empty
+     */
+    @Unmodifiable
+    @NotLive
+    @Nonnull public List<String> getAttributeIDs() {
+        return attributeIDs;
+    }
+
+    /**
+     * Set the list of dependency attribute IDs to search for a value.
+     * 
+     * @param values the list of attribute IDs
+     */
+    public void setAttributeIDs(@Nullable final List<String> values) {
+        checkSetterPreconditions();
+        if (values == null) {
+            attributeIDs = CollectionSupport.emptyList();
+        } else {
+            attributeIDs = CollectionSupport.copyToList(StringSupport.normalizeStringCollection(values));
+        }
+    }
+
+    /**
+     * Get the map allowing to customize the NameID format URI on a per-attributeID basis.
+     * 
+     * @return the custom format map
+     */
+    @Unmodifiable
+    @NotLive
+    @Nonnull public Map<String,String> getNameIDFormatMap() {
+        return nameIDFormatMap;
+    }
+
+    /**
+     * Set the map allowing to customize the NameID format URI on a per-attributeID basis.
+     * 
+     * @param formatMap the custom format map
+     */
+    public void setNameIDFormatMap(@Nullable final Map<String,String> formatMap) {
+        checkSetterPreconditions();
+        if (formatMap == null) {
+            nameIDFormatMap = CollectionSupport.emptyMap();
+        } else {
+            nameIDFormatMap = CollectionSupport.copyToMap(formatMap);
+        }
+    }
+
+    /**
+     * Get the default format URI used to construct the NameID if a custom format for the attribute ID is
+     * not defined in {@link #getNameIDFormatMap()}.
+     * 
+     * @return the format URI
+     */
+    @Nullable public String getDefaultNameIDFormat() {
+        return defaultNameIDFormat;
+    }
+
+    /**
+     * Set the default format URI used to construct the NameID if a custom format for the attribute ID is
+     * not defined in {@link #getNameIDFormatMap()}.
+     * 
+     * @param uri the format URI
+     */
+    public void setDefaultNameIDFormat(@Nullable final String uri) {
+        checkSetterPreconditions();
+        defaultNameIDFormat = StringSupport.trimOrNull(uri);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    @Nullable
+    public Subject resolve(@Nonnull final AttributeResolutionContext resolutionContext,
+            @Nonnull final Map<String, List<IdPAttributeValue>> dependencyAttributes,
+            @Nonnull final AttributeAuthorityDescriptor roleDescriptor) throws ResolutionException {
+
+        checkComponentActive();
+
+        for (final String attributeID : getAttributeIDs()) {
+            assert attributeID != null;
+            log.trace("Evaluating attribute ID: {}", attributeID); 
+            if (dependencyAttributes.containsKey(attributeID)) {
+                log.trace("Dependency attribute '{}' present, checking values");
+                for (final IdPAttributeValue value : dependencyAttributes.get(attributeID)) {
+                    if (value instanceof StringAttributeValue stringValue) {
+                        log.debug("In dependency attribute '{}' found string value '{}'",
+                                attributeID, stringValue.getValue());
+                        return buildSubject(stringValue.getValue(), attributeID);
+                    } else if (value instanceof ScopedStringAttributeValue scopedStringValue) {
+                        final String scopedString = scopedStringValue.getValue() + "@" + scopedStringValue.getScope();
+                        log.debug("In dependency attribute '{}' found scoped string value '{}'",
+                                attributeID, scopedString);
+                        return buildSubject(scopedString, attributeID);
+                    } else if (value instanceof XMLObjectAttributeValue xmlObjectValue 
+                            && xmlObjectValue.getValue() instanceof NameID nameIDValue) {
+                        try {
+                            if (log.isDebugEnabled()) {
+                                log.debug("In dependency attribute '{}' found NameID value '{}'", attributeID,
+                                        SerializeSupport.nodeToString(XMLObjectSupport.marshall(nameIDValue)));
+                            }
+                        } catch (final MarshallingException e) {
+                            log.debug("Error while marshalling+serializing NameID for logging", e);
+                        }
+                        final Subject subject = buildSubject(nameIDValue);
+                        if (subject != null) {
+                            return subject;
+                        } else {
+                            log.warn("Error constructing Subject from NameID value, processing remaining attributes");
+                            continue;
+                        }
+                    }
+                }
+           }
+        }
+
+        log.debug("Could not resolve Subject based on dependency attributes");
+        return null;
+    }
+
+    /**
+     * Build a Subject using the specified string as the NameID value.
+     * 
+     * @param value the string value
+     * @param attributeID the attribute ID which contained the specified value
+     * 
+     * @return newly constructed Subject instance
+     */
+    @Nonnull private Subject buildSubject(@Nonnull final String value, @Nonnull final String attributeID) {
+        final NameID nameID = (NameID) XMLObjectSupport.buildXMLObject(NameID.DEFAULT_ELEMENT_NAME);
+        nameID.setValue(value);
+        nameID.setFormat(determinieNameIDFormat(attributeID));
+
+        final Subject subject = (Subject) XMLObjectSupport.buildXMLObject(Subject.DEFAULT_ELEMENT_NAME);
+        subject.setNameID(nameID);
+        return subject;
+    }
+
+    /**
+     * Build a Subject using the specified NameID.
+     * 
+     * @param value the NameID value
+     * 
+     * @return newly constructed Subject instance, or null if there was a fatal error cloning the input NameID
+     */
+    @Nullable private Subject buildSubject(@Nonnull final NameID value) {
+        try {
+            final NameID nameID = XMLObjectSupport.cloneXMLObject(value);
+
+            final Subject subject = (Subject) XMLObjectSupport.buildXMLObject(Subject.DEFAULT_ELEMENT_NAME);
+            subject.setNameID(nameID);
+            return subject;
+        } catch (final MarshallingException | UnmarshallingException e) {
+            log.warn("Fatal error cloning NameID for Subject construction", e);
+            return null;
+        }
+    }
+
+    /**
+     * Determine the NamedID Format to use for the given dependency attribute ID.
+     * 
+     * @param attributeID the attribute ID to process
+     * 
+     * @return the NameID Format to use for attributes of the specified ID
+     */
+    @Nullable private String determinieNameIDFormat(@Nonnull final String attributeID) {
+        final String mappedValue = getNameIDFormatMap().get(attributeID);
+        if (mappedValue != null) {
+            return mappedValue;
+        }
+        return getDefaultNameIDFormat();
+    }
+
+}
diff --git a/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/plugin/impl/package-info.java b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/plugin/impl/package-info.java
new file mode 100644
index 000000000..62a071d61
--- /dev/null
+++ b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/plugin/impl/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Plug-in component interface implementations used when querying a SAML attribute authority.
+ */
+ at NonnullElements
+package net.shibboleth.idp.attribute.resolver.dc.saml.plugin.impl;
+
+import net.shibboleth.shared.annotation.constraint.NonnullElements;
diff --git a/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/util/impl/AssertionValidationProcessor.java b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/util/impl/AssertionValidationProcessor.java
new file mode 100644
index 000000000..5934a59c2
--- /dev/null
+++ b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/util/impl/AssertionValidationProcessor.java
@@ -0,0 +1,222 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.attribute.resolver.dc.saml.util.impl;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.InOutOperationContext;
+import org.opensaml.saml.common.assertion.AssertionValidationException;
+import org.opensaml.saml.common.assertion.ValidationContext;
+import org.opensaml.saml.common.assertion.ValidationProcessingData;
+import org.opensaml.saml.common.assertion.ValidationResult;
+import org.opensaml.saml.saml2.assertion.SAML20AssertionValidator;
+import org.opensaml.saml.saml2.assertion.messaging.AssertionValidationInput;
+import org.opensaml.saml.saml2.assertion.messaging.AssertionValidationNetworkInformationSupplier;
+import org.opensaml.saml.saml2.assertion.messaging.BasicNetworkInformationSupplier;
+import org.opensaml.saml.saml2.core.Assertion;
+import org.opensaml.saml.saml2.core.Response;
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.attribute.resolver.ResolutionException;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.collection.Pair;
+import net.shibboleth.shared.component.AbstractInitializableComponent;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.logic.FunctionSupport;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * A helper component for validating the {@link Assertion} content of a {@link Response}.
+ */
+public class AssertionValidationProcessor extends AbstractInitializableComponent {
+    
+    /** Logger. */
+    private Logger log = LoggerFactory.getLogger(AssertionValidationProcessor.class);
+    
+    /** Function that builds a {@link ValidationContext} instance based on a 
+     * {@link AssertionValidationInput} instance. */
+    @NonnullAfterInit private Function<AssertionValidationInput, ValidationContext> assertionValidationContextBuilder;
+    
+    /** The SAML 2.0 Assertion validator lookup function, may be null.*/
+    @NonnullAfterInit
+    private Function<Pair<InOutOperationContext, Assertion>, SAML20AssertionValidator> assertionValidatorLookup;
+    
+    /**
+     * Get the function that builds a {@link ValidationContext} instance based on a 
+     * {@link AssertionValidationInput} instance.
+     * 
+     * @return the builder function
+     */
+    @NonnullAfterInit
+    public Function<AssertionValidationInput, ValidationContext> getAssertionValidationContextBuilder() {
+        return assertionValidationContextBuilder;
+    }
+
+    /**
+     * Set the function that builds a {@link ValidationContext} instance based on a 
+     * {@link AssertionValidationInput} instance.
+     * 
+     * @param builder the builder function
+     */
+    public void setAssertionValidationContextBuilder(
+            @Nonnull final Function<AssertionValidationInput, ValidationContext> builder) {
+        checkSetterPreconditions();
+        assertionValidationContextBuilder = Constraint.isNotNull(builder,
+                "ValidationContext builder cannot be null");
+    }
+
+    /**
+     * Set the locally-configured Assertion validator.
+     * 
+     * @param validator the local Assertion validator, may be null
+     */
+    public void setAssertionValidator(@Nullable final SAML20AssertionValidator validator) {
+        checkSetterPreconditions();
+        assertionValidatorLookup = FunctionSupport.constant(validator);
+    }
+
+    /**
+     * Get the Assertion validator lookup function.
+     * 
+     * @return the lookup function
+     */
+    @NonnullAfterInit
+    public Function<Pair<InOutOperationContext, Assertion>, SAML20AssertionValidator> getAssertionValidatorLookup() {
+        return assertionValidatorLookup;
+    }
+
+    /**
+     * Set the Assertion validator lookup function.
+     * 
+     * @param function the Assertion validator lookup function
+     */
+    public void setAssertionValidatorLookup(
+            @Nonnull final Function<Pair<InOutOperationContext, Assertion>, SAML20AssertionValidator> function) {
+        checkSetterPreconditions();
+        assertionValidatorLookup = Constraint.isNotNull(function, "AssertionValidator lookup function cannot be null");
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        
+        if (getAssertionValidationContextBuilder() == null) {
+            throw new ComponentInitializationException("Assertion validation context builder was null");
+        }
+        if (getAssertionValidatorLookup() == null) {
+            throw new ComponentInitializationException("Assertion validator lookup function was null");
+        }
+    }
+
+    /**
+     * Validate the assertions in the specified Response
+     * 
+     * <p>
+     * If an assertion fails validation, it will be removed from the response.
+     * </p>
+     * 
+     * @param response the response being evaluated 
+     * @param opContext the SOAP client operation context
+     * 
+     * @throws ResolutionException if there was a fatal error determining assertion validity
+     */
+    public void validateAssertions(@Nonnull final Response response, @Nonnull final InOutOperationContext opContext)
+            throws ResolutionException {
+        checkComponentActive();
+        
+        final Collection<Assertion> notValid = new ArrayList<>();
+        
+        for (final Assertion assertion : response.getAssertions()) {
+            assert assertion != null;
+
+            final SAML20AssertionValidator validator =
+                    getAssertionValidatorLookup().apply(new Pair<>(opContext, assertion));
+            if (validator == null) {
+                log.warn("No SAML20AssertionValidator was available, terminating");
+                throw new ResolutionException("No SAML20AssertionValidator was available");
+            }
+            
+            try { 
+                final ValidationContext validationContext = buildValidationContext(opContext, assertion);
+                final ValidationResult validationResult = validator.validate(assertion, validationContext);
+                if (validationResult != ValidationResult.VALID) {
+                    notValid.add(assertion);
+                }
+                processAssertionValidationResult(validationContext, validationResult, assertion, opContext);
+            } catch (final Exception e) {
+                log.warn("There was a problem determining Assertion validity", e);
+                throw new ResolutionException("There was a problem determing SAML Assertion validity", e);
+            }
+            
+        }
+        
+        response.getAssertions().removeAll(notValid);
+    }
+    
+    /**
+     * Build the Assertion ValidationContext.
+     * 
+     * @param opContext the current operation context
+     * @param assertion the assertion which is to be validated
+     * 
+     * @return the new Assertion validation context to use
+     * 
+     * @throws AssertionValidationException if no validation context instance could be built
+     */
+    @Nonnull protected ValidationContext buildValidationContext(@Nonnull final InOutOperationContext opContext, 
+            @Nonnull final Assertion assertion) throws AssertionValidationException {
+        
+        // For this use case we don't have any of this input that is relevant, so just supply an empty impl 
+        final AssertionValidationNetworkInformationSupplier networkInformation = new BasicNetworkInformationSupplier();
+
+        final ValidationContext validationContext = getAssertionValidationContextBuilder().apply(
+                new AssertionValidationInput(assertion, opContext, networkInformation));
+        
+        if (validationContext == null) {
+            log.warn("ValidationContext produced was null");
+            throw new AssertionValidationException("Assertion ValidationContext was null");
+        }
+        
+        return validationContext;
+    }
+    
+    /**
+     * Process the result of the assertion validation.
+     * 
+     * @param validationContext the Assertion validation context
+     * @param validationResult the Assertion validation result
+     * @param assertion the assertion being evaluated produced
+     * @param opContext the current profile request context
+     */
+    protected void processAssertionValidationResult(@Nonnull final ValidationContext validationContext, 
+            @Nonnull final ValidationResult validationResult, @Nonnull final Assertion assertion, 
+            @Nonnull final InOutOperationContext opContext) {
+
+        log.debug("Assertion validation result was: {}", validationResult);
+        if (validationResult != ValidationResult.VALID) {
+            log.info("Assertion validation failure(s): {}",validationContext.getValidationFailureMessages());
+        }
+
+        assertion.getObjectMetadata().put(new ValidationProcessingData(validationContext, validationResult));
+    }
+
+}
diff --git a/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/util/impl/AttributeAuthorityEntityIDContext.java b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/util/impl/AttributeAuthorityEntityIDContext.java
new file mode 100644
index 000000000..775f5db0c
--- /dev/null
+++ b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/util/impl/AttributeAuthorityEntityIDContext.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.attribute.resolver.dc.saml.util.impl;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.messaging.context.BaseContext;
+
+import net.shibboleth.idp.attribute.resolver.dc.saml.impl.SimpleAggregationSAMLDataConnector;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.StringSupport;
+
+/**
+ * Used by {@link SimpleAggregationSAMLDataConnector} hold the current attribute authority entityID
+ * from which attributes are being resolved.
+ */
+public class AttributeAuthorityEntityIDContext extends BaseContext {
+    
+    /** The authority entityID. */
+    private String authorityEntityID;
+
+    /**
+     * Constructor.
+     *
+     * @param entityID the authority entityID
+     */
+    public AttributeAuthorityEntityIDContext(@Nonnull final String entityID) {
+        super();
+        authorityEntityID = Constraint.isNotNull(StringSupport.trimOrNull(entityID), "Authority entityID was null");
+    }
+
+    /**
+     * Get the authority entityID.
+     * 
+     * @return the entityID
+     */
+    public String getAuthorityEntityID() {
+        return authorityEntityID;
+    }
+
+}
diff --git a/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/util/impl/AttributeAuthorityEntityIDReference.java b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/util/impl/AttributeAuthorityEntityIDReference.java
new file mode 100644
index 000000000..3e295d683
--- /dev/null
+++ b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/util/impl/AttributeAuthorityEntityIDReference.java
@@ -0,0 +1,34 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.attribute.resolver.dc.saml.util.impl;
+
+import javax.annotation.Nonnull;
+
+/**
+ * Sub-type of {@link AttributeAuthorityEntityIDSource} which specifies a reference
+ * to an entityID in the form of an attribute resolver dependency attribute name.
+ */
+public class AttributeAuthorityEntityIDReference extends AttributeAuthorityEntityIDSource {
+
+    /**
+     * Constructor.
+     *
+     * @param reference the authority entityID reference (dependency attribute name)
+     */
+    protected AttributeAuthorityEntityIDReference(@Nonnull final String reference) {
+        super(reference);
+    }
+
+}
diff --git a/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/util/impl/AttributeAuthorityEntityIDSource.java b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/util/impl/AttributeAuthorityEntityIDSource.java
new file mode 100644
index 000000000..ec6bf16b9
--- /dev/null
+++ b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/util/impl/AttributeAuthorityEntityIDSource.java
@@ -0,0 +1,49 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.attribute.resolver.dc.saml.util.impl;
+
+import javax.annotation.Nonnull;
+
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.StringSupport;
+
+/**
+ * Abstraction used to represent the source data for deriving an attribute authority entityID.
+ */
+public abstract class AttributeAuthorityEntityIDSource {
+    
+    /** The source value. */
+    @Nonnull private String value;
+
+    /**
+     * Constructor.
+     *
+     * @param newValue the source value
+     */
+    protected AttributeAuthorityEntityIDSource(@Nonnull final String newValue) {
+        super();
+        value = Constraint.isNotNull(StringSupport.trimOrNull(newValue), "Source value was null");
+    }
+
+    /**
+     * Get the source value
+     * 
+     * @return the value 
+     */
+    @Nonnull public String getValue() {
+        return value;
+    }
+
+}
diff --git a/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/util/impl/AttributeAuthorityEntityIDValue.java b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/util/impl/AttributeAuthorityEntityIDValue.java
new file mode 100644
index 000000000..1e00f4391
--- /dev/null
+++ b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/util/impl/AttributeAuthorityEntityIDValue.java
@@ -0,0 +1,34 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.attribute.resolver.dc.saml.util.impl;
+
+import javax.annotation.Nonnull;
+
+/**
+ *
+ * Sub-type of {@link AttributeAuthorityEntityIDSource} which specifies the entityID directly by value.
+ */
+public class AttributeAuthorityEntityIDValue extends AttributeAuthorityEntityIDSource {
+
+    /**
+     * Constructor.
+     *
+     * @param entityID the authority entityID value
+     */
+    protected AttributeAuthorityEntityIDValue(@Nonnull final String entityID) {
+        super(entityID);
+    }
+
+}
diff --git a/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/util/impl/DecryptionProcessor.java b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/util/impl/DecryptionProcessor.java
new file mode 100644
index 000000000..a2473f82a
--- /dev/null
+++ b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/util/impl/DecryptionProcessor.java
@@ -0,0 +1,367 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.attribute.resolver.dc.saml.util.impl;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.core.xml.io.MarshallingException;
+import org.opensaml.core.xml.util.XMLObjectSupport;
+import org.opensaml.saml.common.SAMLObject;
+import org.opensaml.saml.saml2.core.Assertion;
+import org.opensaml.saml.saml2.core.Attribute;
+import org.opensaml.saml.saml2.core.AttributeStatement;
+import org.opensaml.saml.saml2.core.EncryptedAssertion;
+import org.opensaml.saml.saml2.core.EncryptedAttribute;
+import org.opensaml.saml.saml2.core.EncryptedID;
+import org.opensaml.saml.saml2.core.NameID;
+import org.opensaml.saml.saml2.core.Response;
+import org.opensaml.saml.saml2.core.Subject;
+import org.opensaml.saml.saml2.core.SubjectConfirmation;
+import org.opensaml.saml.saml2.encryption.Decrypter;
+import org.opensaml.xmlsec.DecryptionConfiguration;
+import org.opensaml.xmlsec.DecryptionParameters;
+import org.opensaml.xmlsec.DecryptionParametersResolver;
+import org.opensaml.xmlsec.criterion.DecryptionConfigurationCriterion;
+import org.opensaml.xmlsec.encryption.support.DecryptionException;
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.attribute.resolver.ResolutionException;
+import net.shibboleth.idp.attribute.resolver.dc.saml.ResponseData;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.component.AbstractInitializableComponent;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.resolver.CriteriaSet;
+import net.shibboleth.shared.resolver.ResolverException;
+
+/**
+ * A helper component for processing encrypted content of a {@link Response}.
+ */
+public class DecryptionProcessor extends AbstractInitializableComponent {
+    
+    /** Logger. */
+    private Logger log = LoggerFactory.getLogger(DecryptionProcessor.class);
+    
+    /** Decryption parameters resolver. */
+    @NonnullAfterInit private DecryptionParametersResolver decryptionParamsResolver;
+    
+    /** Strategy used to lookup a per-response {@link DecryptionConfiguration} list. */
+    @NonnullAfterInit private Function<ResponseData,List<DecryptionConfiguration>>
+        decryptionConfigurationLookupStrategy;
+    
+    /**
+     * Get the resolver to use for the parameters to store into the context.
+     * 
+     * @return the resolver
+     */
+    @NonnullAfterInit DecryptionParametersResolver getDecryptionParametersResolver() {
+        return decryptionParamsResolver;
+    }
+
+    /**
+     * Set the resolver to use for the parameters to store into the context.
+     * 
+     * @param resolver the resolver to use
+     */
+    public void setDecryptionParametersResolver(@Nonnull final DecryptionParametersResolver resolver) {
+        checkSetterPreconditions();
+        decryptionParamsResolver = resolver;
+    }
+    
+    /**
+     * Get the strategy used to look up a per-response {@link DecryptionConfiguration} list.
+     * 
+     * @return the strategy
+     */
+    @NonnullAfterInit Function<ResponseData,List<DecryptionConfiguration>> getDecryptionConfigurationLookupStrategy() {
+        return decryptionConfigurationLookupStrategy;
+    }
+
+    /**
+     * Set the strategy used to look up a per-response {@link DecryptionConfiguration} list.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setDecryptionConfigurationLookupStrategy(
+            @Nonnull final Function<ResponseData,List<DecryptionConfiguration>> strategy) {
+        checkSetterPreconditions();
+        decryptionConfigurationLookupStrategy = strategy;
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        if (getDecryptionConfigurationLookupStrategy() == null) {
+            throw new ComponentInitializationException("DecryptionConfiguration lookup strategy was null");
+        }
+        if (getDecryptionParametersResolver() == null) {
+            throw new ComponentInitializationException("DecryptionParametersResolver was null");
+        }
+    }
+    
+    /**
+     * Determine if the Response has any EncryptedAsseertion elements.
+     * 
+     * @param response the Response to process
+     * 
+     * @return true if Response contains EncryptedAssertion elements, otherwise false
+     */
+    public boolean haveEncryptedAssertions(@Nonnull final Response response) {
+        return response.getEncryptedAssertions().size() > 0;
+    }
+
+    /**
+     * Determine if the Response has any EncryptedID or EncryptedAttribute elements.
+     * 
+     * @param response the Response to process
+     * 
+     * @return true if Response contains EncryptedID or EncryptedAttribute elements, otherwise false
+     */
+    public boolean haveEncryptedContent(@Nonnull final Response response) {
+        for (Assertion assertion : response.getAssertions()) {
+            final Subject subject = assertion.getSubject();
+            if (subject != null && subject.getEncryptedID() != null) {
+                return true;
+            }
+            for (AttributeStatement statement : assertion.getAttributeStatements()) {
+                if (statement.getEncryptedAttributes().size() > 0) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Decrypt the EncryptedAssertion elements of a Response.
+     * 
+     * <p>
+     * The resulting decrypted elements are stored in-place in the Response.
+     * </p>
+     * 
+     * @param response the Response to process
+     * @param decrypter the decrypter used for decryption operations
+     * 
+     * @throws ResolutionException if there is a fatal error during decryption
+     */
+    public void decryptAssertions(@Nonnull final Response response, @Nonnull final Decrypter decrypter)
+            throws ResolutionException {
+        checkComponentActive();
+
+        final Collection<Assertion> decrypteds = new ArrayList<>();
+        final Collection<EncryptedAssertion> encrypteds = new ArrayList<>();
+        
+        final Iterator<EncryptedAssertion> i = response.getEncryptedAssertions().iterator();
+        while (i.hasNext()) {
+            log.debug("Decrypting EncryptedAssertion in Response");
+            try {
+                final EncryptedAssertion encrypted = i.next();
+                assert encrypted != null;
+                final Assertion decrypted = decrypter.decrypt(encrypted);
+                if (decrypted != null) {
+                    encrypteds.add(encrypted);
+                    decrypteds.add(decrypted);
+                }
+            } catch (final DecryptionException e) {
+                throw new ResolutionException("Error decrypting Assertion", e);
+            }
+        }
+        
+        response.getEncryptedAssertions().removeAll(encrypteds);
+        response.getAssertions().addAll(decrypteds); 
+
+        // Re-marshall the response so that any ID attributes within the decrypted Assertions
+        // will have their ID-ness re-established at the DOM level.
+        if (!decrypteds.isEmpty()) {
+            try {
+                XMLObjectSupport.marshall(response);
+            } catch (final MarshallingException e) {
+                throw new ResolutionException("Error re-marshalling Response after Assertion decryption", e);
+            }
+        }
+    }
+
+    /**
+     * Decrypt the EncryptedID and EncryptedAttribute elements of a Response.
+     * 
+     * <p>
+     * The resulting decrypted elements are stored in-place in the Response.
+     * </p>
+     * 
+     * @param response the Response to process
+     * @param decrypter the decrypter used for decryption operations
+     * 
+     * @throws ResolutionException if there is a fatal error during decryption
+     */
+    public void decryptAssertionContent(@Nonnull final Response response, @Nonnull final Decrypter decrypter)
+            throws ResolutionException {
+
+        for (final Assertion assertion : response.getAssertions()) {
+            if (assertion == null) {
+                continue;
+            }
+            decryptEncryptedIDs(assertion, decrypter);
+            decryptEncryptedAttributes(assertion, decrypter);
+        }
+    }
+    
+    /**
+     * Decrypt the EncryptedID elements of an Assertion.
+     * 
+     * <p>
+     * The resulting decrypted elements are stored in-place in the Assertion.
+     * </p>
+     * 
+     * @param assertion the Assertion to process
+     * @param decrypter the decrypter used for decryption operations
+     * 
+     * @throws ResolutionException if there is a fatal error during decryption
+     */
+    public void decryptEncryptedIDs(@Nonnull final Assertion assertion, @Nonnull final Decrypter decrypter)
+            throws ResolutionException {
+        checkComponentActive();
+
+        final Subject subject = assertion.getSubject();
+
+        if (subject != null) {
+            EncryptedID encID = subject.getEncryptedID();
+            if (encID != null) {
+                log.debug("Decrypting EncryptedID in Subject");
+                final NameID decrypted = decryptEncryptedID(encID, decrypter);
+                subject.setNameID(decrypted);
+                subject.setEncryptedID(null);
+            }
+            
+            for (final SubjectConfirmation sc : subject.getSubjectConfirmations()) {
+                encID = sc.getEncryptedID();
+                if (encID != null) {
+                    log.debug("Decrypting EncryptedID in SubjectConfirmation");
+                    final NameID decrypted = decryptEncryptedID(encID, decrypter);
+                    sc.setNameID(decrypted);
+                    sc.setEncryptedID(null);
+                    }
+                }
+            }
+    }
+    
+    /**
+     * Decrypt an EncryptedID element.
+     * 
+     * @param encID the EncryptedID to process
+     * @param decrypter the decrypter used for decryption operations
+     * 
+     * @return the decrypted NameID element
+     * 
+     * @throws ResolutionException if there is a fatal error during decryption
+     */
+    @Nonnull public NameID decryptEncryptedID(@Nonnull final EncryptedID encID, @Nonnull final Decrypter decrypter)
+            throws ResolutionException {
+        checkComponentActive();
+        
+        try {
+            final SAMLObject object = decrypter.decrypt(encID);
+            if (object instanceof NameID) {
+                return (NameID) object;
+            }
+            throw new DecryptionException("Decrypted EncryptedID was not a NameID, was a "
+                    + object.getElementQName().toString());
+        } catch (DecryptionException e) {
+            throw new ResolutionException("Error decryptng EncryptedID", e);
+        }
+    }
+
+    /**
+     * Decrypt the EncryptedAttributes of an Assertion.
+     * 
+     * <p>
+     * The resulting decrypted elements are stored in-place in the Assertion.
+     * </p>
+     * 
+     * @param assertion the Assertion to process
+     * @param decrypter the decrypter to use
+     * 
+     * @throws ResolutionException  if there is a fatal error during decryption
+     */
+    public void decryptEncryptedAttributes(@Nonnull final Assertion assertion, @Nonnull final Decrypter decrypter)
+            throws ResolutionException {
+        checkComponentActive();
+
+        for (final AttributeStatement s : assertion.getAttributeStatements()) {
+
+            final Collection<Attribute> decrypteds = new ArrayList<>();
+            final Collection<EncryptedAttribute> encrypteds = new ArrayList<>();
+            
+            final Iterator<EncryptedAttribute> i = s.getEncryptedAttributes().iterator();
+            while (i.hasNext()) {
+                log.debug("Decrypting EncryptedAttribute in AttributeStatement");
+                
+                try {
+                    final EncryptedAttribute encrypted = i.next();
+                    assert encrypted != null;
+                    final Attribute decrypted = decrypter.decrypt(encrypted);
+                    if (decrypted != null) {
+                        encrypteds.add(encrypted);
+                        decrypteds.add(decrypted);
+                    }
+                } catch (final DecryptionException e) {
+                    throw new ResolutionException("Error decrypting Attribute", e);
+                }
+            }
+            
+            s.getEncryptedAttributes().removeAll(encrypteds);
+            s.getAttributes().addAll(decrypteds); 
+        }
+    }
+
+    /**
+     * Build the {@link Decrypter} instance to be used for decryption operations.
+     * 
+     * @param responseData the response data being processed
+     * 
+     * @return the new Decrypter instance
+     * 
+     * @throws ResolutionException if there is a fatal error constructing the Decrypter instance
+     */
+    @Nonnull public Decrypter buildDecrypter(@Nonnull final ResponseData responseData) throws ResolutionException {
+        checkComponentActive();
+
+        final List<DecryptionConfiguration> configs = getDecryptionConfigurationLookupStrategy().apply(responseData);
+        if (configs == null || configs.isEmpty()) {
+            log.error("No DecryptionConfigurations returned by lookup strategy");
+            throw new ResolutionException("No DecryptionConfigurations returned by lookup strategy");
+        }
+        
+        try {
+            final CriteriaSet criteria = new CriteriaSet(new DecryptionConfigurationCriterion(configs));
+
+            final DecryptionParameters params = getDecryptionParametersResolver().resolveSingle(criteria);
+            log.debug("{} DecryptionParameters", params != null ? "Resolved" : "Failed to resolve");
+            if (params != null) {
+                return new Decrypter(params);
+            }
+            throw new ResolutionException("Failed to resolve DecryptionParameters");
+        } catch (final ResolverException e) {
+            throw new ResolutionException("Error resolving DecryptionParameters", e);
+        }
+    }
+
+
+}
diff --git a/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/util/impl/SubjectMatchProcessor.java b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/util/impl/SubjectMatchProcessor.java
new file mode 100644
index 000000000..c530395ad
--- /dev/null
+++ b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/util/impl/SubjectMatchProcessor.java
@@ -0,0 +1,89 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.attribute.resolver.dc.saml.util.impl;
+
+import java.util.LinkedList;
+import java.util.List;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.saml.saml2.core.Assertion;
+import org.opensaml.saml.saml2.core.AttributeQuery;
+import org.opensaml.saml.saml2.core.Issuer;
+import org.opensaml.saml.saml2.core.Subject;
+import org.opensaml.saml.saml2.profile.SAML2ObjectSupport;
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.attribute.resolver.dc.saml.ResponseData;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * A helper component for matching the {@link Subject} of an {@link Assertion} against the 
+ * value sent in the query.
+ */
+public class SubjectMatchProcessor {
+    
+    /** Logger. */
+    @Nonnull private Logger log = LoggerFactory.getLogger(SubjectMatchProcessor.class);
+    
+    /**
+     * Match the response Assertions' Subject NameIDs to the one requested in the outgoing query.
+     * 
+     * <p>
+     * If an assertion's subject does not match what was requested, it will be removed from further processing.
+     * </p>
+     * 
+     * @param responseData the response data being processed
+     */
+    public void process(@Nonnull final ResponseData responseData) {
+        // We know these are all non-null because we created them
+        final MessageContext outboundContext = responseData.getSOAPClientContext().getOutboundMessageContext();
+        assert outboundContext != null;
+        final AttributeQuery query = ((AttributeQuery)outboundContext.getMessage());
+        assert query != null;
+        final Subject querySubject = query.getSubject();
+        assert querySubject != null;
+        
+        List<Assertion> toRemove = new LinkedList<>();
+        
+        for (final Assertion assertion : responseData.getResponse().getAssertions()) {
+            final Issuer assertionIssuer = assertion.getIssuer();
+
+            final Subject assertionSubject = assertion.getSubject();
+            if (assertionSubject == null) {
+                log.warn("Assertion '{}' from Issuer '{}' did not contain Subject, removing",
+                        assertion.getID(),
+                        assertionIssuer != null ? assertionIssuer.getValue() : "null");
+                toRemove.add(assertion);
+                continue;
+            }
+
+            if (! SAML2ObjectSupport.matchSubject(assertionSubject, querySubject)) {
+                log.warn("Subject of Assertion '{}' from Issuer '{}' did not match query Subject, removing",
+                        assertion.getID(),
+                        assertionIssuer != null ? assertionIssuer.getValue() : "null");
+                toRemove.add(assertion);
+            }
+        }
+        
+        if (!toRemove.isEmpty()) {
+            responseData.getResponse().getAssertions().removeAll(toRemove);
+            log.debug("Removed a total of {} assertions which failed subject match processing", toRemove.size());
+        }
+        
+    }
+
+}
diff --git a/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/util/impl/package-info.java b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/util/impl/package-info.java
new file mode 100644
index 000000000..59b1e3ed2
--- /dev/null
+++ b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/util/impl/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Utility support classes used when querying a SAML attribute authority.
+ */
+ at NonnullElements
+package net.shibboleth.idp.attribute.resolver.dc.saml.util.impl;
+
+import net.shibboleth.shared.annotation.constraint.NonnullElements;

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.


More information about the commits mailing list