[java-idp-plugin-oidc-op-oidfed] branch dev/CACHE-REFACTOR updated: Initial support for pushed/provided trust chains

Codeberg noreply at shibboleth.net
Mon Mar 16 15:34:14 UTC 2026


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

codeberg pushed a commit to branch dev/CACHE-REFACTOR
in repository java-idp-plugin-oidc-op-oidfed.

View the commit online:
https://codeberg.org/Shibboleth/java-idp-plugin-oidc-op-oidfed/commit/2d5e1255a4a8f43e62cdf77d93ee28f23b2e506f

The following commit(s) were added to refs/heads/dev/CACHE-REFACTOR by this push:
     new 2d5e125  Initial support for pushed/provided trust chains
2d5e125 is described below

commit 2d5e1255a4a8f43e62cdf77d93ee28f23b2e506f
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Mon Mar 16 17:33:39 2026 +0200

    Initial support for pushed/provided trust chains
    
    - Request objects sent to PAR and authorize endpoint may contain trust_chain header
    - OIDFED.AutomaticRegistration can be configured with a decorator type
      - p:defaultClaimsSetDecorationType: for requests without provided trust chain
      - p:providedChainClaimsSetDecorationType: for requests with provided trust chain
      - ValidateAutomaticRegistrationProfileConfiguration sets the configured decorator type into AutomaticRegistrationContext
        - The context is exploited by claims set decorators
    - Harmonised trust chain deserialization
---
 ...ration.java => ProvidedTrustChainResolver.java} |  21 ++-
 ...nAutomaticRegistrationProfileConfiguration.java |  29 +++
 .../ClaimsSetDecorationTypeLookupFunction.java     |  50 ++++++
 ...ChainClaimsSetDecorationTypeLookupFunction.java |  50 ++++++
 .../context/AutomaticRegistrationContext.java      |  51 ++++++
 .../context/RelyingPartyTrustChainContext.java     |  24 +++
 .../oidfed/support/ClaimsSetExtensionSupport.java  |  42 +++++
 .../ExplicitClientRegistrationRequestDecoder.java  |  54 ++----
 ...tEntityStatementContentValidationCondition.java |  27 +++
 ...DefaultTrustChainHeaderValidationCondition.java | 196 +++++++++++++++++++++
 .../metadata/util/EntityStatementHelper.java       | 114 ++++++++++++
 ...nAutomaticRegistrationProfileConfiguration.java |  69 ++++++++
 .../impl/AbstractTokenClaimsSetDecorator.java      | 123 +++++++++++++
 ...edClientInformationStoreClaimsSetDecorator.java | 145 +++++++++++++++
 ...redTrustChainAccessTokenClaimsSetDecorator.java |  54 +-----
 ...stChainAuthorizationCodeClaimsSetDecorator.java |  54 +-----
 ...RegisteredTrustChainIDsClaimsSetDecorator.java} |  16 +-
 ...edTrustChainRefreshTokenClaimsSetDecorator.java |  54 +-----
 ...eredTrustChainRequestUriClaimsSetDecorator.java |  52 +-----
 .../oidfed/profile/impl/CallResolveEntityApi.java  |  33 +---
 .../impl/DefaultProvidedTrustChainResolver.java    | 116 ++++++++++++
 .../oidfed/profile/impl/OidFederationEventIds.java |  12 ++
 ...eAutomaticRegistrationProfileConfiguration.java |  75 +++++++-
 .../profile/impl/ValidateProvidedTrustChain.java   | 106 +++++++++--
 ...actRequestObjectProvidedTrustChainResolver.java | 146 +++++++++++++++
 ...ionRequestObjectProvidedTrustChainResolver.java |  43 +++++
 ...ionRequestObjectProvidedTrustChainResolver.java |  45 +++++
 ...ientRegistrationRequestTrustChainResolver.java} |  28 ++-
 .../META-INF/net.shibboleth.idp/postconfig.xml     |  79 ++++++++-
 .../oidfed/metadata-lookup-ext-oidfed-beans.xml    |  77 ++++++++
 .../oidfed/metadata-lookup-ext-oidfed-flow.xml     |  13 +-
 .../idp/flows/oidfed/register/register-beans.xml   |  21 ++-
 .../idp/service/relying-party/postconfig.xml       |   4 +-
 .../plugin/oidc/op/oidfed/TrustChainTestUtil.java  |  13 +-
 .../flow/oidfed/AbstractFederationFlowTest.java    |   9 +-
 .../AuthorizeFlowAutomaticRegistrationTest.java    |  68 +++++++
 ...shedAuthorizeFlowAutomaticRegistrationTest.java |  75 ++++++++
 .../EntityConfigurationMetadataCacheTest.java      |  44 +++++
 .../SubordinateStatementMetadataCacheTest.java     |  50 ++++++
 39 files changed, 1950 insertions(+), 332 deletions(-)

diff --git a/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/config/OIDFederationAutomaticRegistrationProfileConfiguration.java b/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/ProvidedTrustChainResolver.java
similarity index 51%
copy from idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/config/OIDFederationAutomaticRegistrationProfileConfiguration.java
copy to idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/ProvidedTrustChainResolver.java
index 5339180..15e1732 100644
--- a/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/config/OIDFederationAutomaticRegistrationProfileConfiguration.java
+++ b/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/ProvidedTrustChainResolver.java
@@ -12,14 +12,21 @@
  * limitations under the License.
  */
 
-package net.shibboleth.idp.plugin.oidc.op.oidfed.profile.config;
+package net.shibboleth.idp.plugin.oidc.op.oidfed.profile;
 
-/** 
- * Profile configuration for an OpenID Federation Automatic Registration.
- */
-public interface OIDFederationAutomaticRegistrationProfileConfiguration extends OIDFederationRegistrationProfileConfiguration {
+import java.util.List;
+
+import javax.annotation.concurrent.ThreadSafe;
+
+import org.opensaml.profile.context.ProfileRequestContext;
 
-    /** ID for this profile configuration. */
-    public static final String PROFILE_ID = "http://shibboleth.net/ns/profiles/oidfed/automaticregistration";
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.EntityStatement;
+import net.shibboleth.shared.resolver.Resolver;
+
+/**
+ * A resolver that is capable of finding provided trust chains from the {@link ProfileRequestContext}.
+ */
+ at ThreadSafe
+public interface ProvidedTrustChainResolver extends Resolver<List<EntityStatement<?>>, ProfileRequestContext> {
 
 }
diff --git a/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/config/OIDFederationAutomaticRegistrationProfileConfiguration.java b/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/config/OIDFederationAutomaticRegistrationProfileConfiguration.java
index 5339180..7ef25f5 100644
--- a/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/config/OIDFederationAutomaticRegistrationProfileConfiguration.java
+++ b/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/config/OIDFederationAutomaticRegistrationProfileConfiguration.java
@@ -14,6 +14,13 @@
 
 package net.shibboleth.idp.plugin.oidc.op.oidfed.profile.config;
 
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import net.shibboleth.shared.annotation.ConfigurationSetting;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+
 /** 
  * Profile configuration for an OpenID Federation Automatic Registration.
  */
@@ -22,4 +29,26 @@ public interface OIDFederationAutomaticRegistrationProfileConfiguration extends
     /** ID for this profile configuration. */
     public static final String PROFILE_ID = "http://shibboleth.net/ns/profiles/oidfed/automaticregistration";
 
+    /**
+     * Get claims set decoration type.
+     * 
+     * @param profileRequestContext profile request context
+     * 
+     * @return claims set decoration type, or null for default
+     */
+    @ConfigurationSetting(name="defaultClaimsSetDecorationType")
+    @Nullable @NotEmpty String getDefaultClaimsSetDecorationType(
+            @Nullable final ProfileRequestContext profileRequestContext);
+
+    /**
+     * Get claims set decoration type for a client with request-provided trust chain.
+     * 
+     * @param profileRequestContext profile request context
+     * 
+     * @return claims set decoration type, or null for default
+     */
+    @ConfigurationSetting(name="providedChainClaimsSetDecorationType")
+    @Nullable @NotEmpty String getProvidedChainClaimsSetDecorationType(
+            @Nullable final ProfileRequestContext profileRequestContext);
+
 }
diff --git a/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/config/navigate/ClaimsSetDecorationTypeLookupFunction.java b/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/config/navigate/ClaimsSetDecorationTypeLookupFunction.java
new file mode 100644
index 0000000..2590ec5
--- /dev/null
+++ b/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/config/navigate/ClaimsSetDecorationTypeLookupFunction.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.plugin.oidc.op.oidfed.profile.config.navigate;
+
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.config.OIDFederationAutomaticRegistrationProfileConfiguration;
+import net.shibboleth.profile.config.ProfileConfiguration;
+import net.shibboleth.profile.context.RelyingPartyContext;
+import net.shibboleth.profile.context.navigate.AbstractRelyingPartyLookupFunction;
+
+/**
+ * A function that returns
+ * {@link OIDFederationAutomaticRegistrationProfileConfiguration#getDefaultClaimsSetDecorationType(ProfileRequestContext)}
+ * if such a profile is available from a {@link RelyingPartyContext} obtained via a lookup function,
+ * by default a child of the {@link ProfileRequestContext}.
+ * 
+ * <p>If a specific setting is unavailable, a null value is returned.</p>
+ */
+public class ClaimsSetDecorationTypeLookupFunction extends AbstractRelyingPartyLookupFunction<String> {
+
+    /** {@inheritDoc} */
+    @Override
+    @Nullable public String apply(@Nullable final ProfileRequestContext input) {
+        final RelyingPartyContext rpc = getRelyingPartyContextLookupStrategy().apply(input);
+        if (rpc != null) {
+            final ProfileConfiguration pc = rpc.getProfileConfig();
+            if (pc instanceof OIDFederationAutomaticRegistrationProfileConfiguration ofarpc) {
+                return ofarpc.getDefaultClaimsSetDecorationType(input);
+            }
+        }
+        
+        return null;
+    }
+
+}
diff --git a/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/config/navigate/ProvidedChainClaimsSetDecorationTypeLookupFunction.java b/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/config/navigate/ProvidedChainClaimsSetDecorationTypeLookupFunction.java
new file mode 100644
index 0000000..13fd28b
--- /dev/null
+++ b/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/config/navigate/ProvidedChainClaimsSetDecorationTypeLookupFunction.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.plugin.oidc.op.oidfed.profile.config.navigate;
+
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.config.OIDFederationAutomaticRegistrationProfileConfiguration;
+import net.shibboleth.profile.config.ProfileConfiguration;
+import net.shibboleth.profile.context.RelyingPartyContext;
+import net.shibboleth.profile.context.navigate.AbstractRelyingPartyLookupFunction;
+
+/**
+ * A function that returns
+ * {@link OIDFederationAutomaticRegistrationProfileConfiguration#getProvidedChainClaimsSetDecorationType(
+ * ProfileRequestContext)} if such a profile is available from a {@link RelyingPartyContext} obtained via a lookup
+ * function, by default a child of the {@link ProfileRequestContext}.
+ * 
+ * <p>If a specific setting is unavailable, a null value is returned.</p>
+ */
+public class ProvidedChainClaimsSetDecorationTypeLookupFunction extends AbstractRelyingPartyLookupFunction<String> {
+
+    /** {@inheritDoc} */
+    @Override
+    @Nullable public String apply(@Nullable final ProfileRequestContext input) {
+        final RelyingPartyContext rpc = getRelyingPartyContextLookupStrategy().apply(input);
+        if (rpc != null) {
+            final ProfileConfiguration pc = rpc.getProfileConfig();
+            if (pc instanceof OIDFederationAutomaticRegistrationProfileConfiguration ofarpc) {
+                return ofarpc.getProvidedChainClaimsSetDecorationType(input);
+            }
+        }
+        
+        return null;
+    }
+
+}
diff --git a/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/context/AutomaticRegistrationContext.java b/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/context/AutomaticRegistrationContext.java
new file mode 100644
index 0000000..fcf4306
--- /dev/null
+++ b/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/context/AutomaticRegistrationContext.java
@@ -0,0 +1,51 @@
+/*
+ * 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.plugin.oidc.op.oidfed.profile.context;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.BaseContext;
+
+/**
+ * Subcontext carrying information for automatic registration.
+ */
+public class AutomaticRegistrationContext extends BaseContext {
+
+    /** Claims set decoration type. */
+    @Nullable private String claimsSetDecorationType;
+
+    /**
+     * Get the claims set decoration typet.
+     * 
+     * @return claims set decoration type
+     */
+    @Nullable public String getClaimsSetDecorationType() {
+        return claimsSetDecorationType;
+    }
+
+    /**
+     * Set the claims set decoration type.
+     * 
+     * @param type claims set decoration type
+     * 
+     * @return this context
+     */
+    @Nonnull public AutomaticRegistrationContext setClaimsSetDecorationType(
+            @Nullable final String type) {
+        claimsSetDecorationType = type;
+        return this;
+    }
+}
diff --git a/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/context/RelyingPartyTrustChainContext.java b/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/context/RelyingPartyTrustChainContext.java
index 1ccedce..c850ea7 100644
--- a/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/context/RelyingPartyTrustChainContext.java
+++ b/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/context/RelyingPartyTrustChainContext.java
@@ -33,6 +33,9 @@ import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.TrustedRemoteResolverEnt
  */
 public final class RelyingPartyTrustChainContext extends BaseContext {
 
+    /** Trust chain provided within the request. */
+    @Nullable private List<EntityStatement<?>> providedTrustChain;
+
     /** All resolved trust chains for the relying party. */
     @Nullable private List<List<EntityStatement<?>>> resolvedTrustChains;
 
@@ -58,7 +61,28 @@ public final class RelyingPartyTrustChainContext extends BaseContext {
     @Nullable private List<TrustedRemoteResolverEntity> attemptedTrustedRemoteResolverEntities;
 
     /**
+     * Get the trust chain provided within the request.
+     * 
+     * @return provided trust chain
+     */
+    @Nullable public List<EntityStatement<?>> getProvidedTrustChain() {
+        return providedTrustChain;
+    }
 
+    /**
+     * Set the trust chain provided within the request.
+     * 
+     * @param trustChain provided trust chain
+     * 
+     * @return this context
+     */
+    @Nonnull public RelyingPartyTrustChainContext setProvidedTrustChain(
+            @Nullable final List<EntityStatement<?>> trustChain) {
+        providedTrustChain = trustChain;
+        return this;
+    }
+
+    /**
      * Get the resolved trust chains for the relying party.
      * 
      * @return the trust chains
diff --git a/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/support/ClaimsSetExtensionSupport.java b/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/support/ClaimsSetExtensionSupport.java
index 82707d5..b09355d 100644
--- a/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/support/ClaimsSetExtensionSupport.java
+++ b/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/support/ClaimsSetExtensionSupport.java
@@ -39,6 +39,9 @@ public class ClaimsSetExtensionSupport {
     /** Identifier for trust chain used when client was registered via OID federation. */
     @Nonnull @NotEmpty public static final String KEY_AUTO_REGISTERED_TRUST_CHAIN = "oidfed_artc";
 
+    /** Identifier for flag indicating the client was stored. */
+    @Nonnull @NotEmpty public static final String KEY_AUTO_REGISTERED_STORED = "oidfed_sto";
+
     /** Class logger. */
     @Nonnull private static Logger log = LoggerFactory.getLogger(ClaimsSetExtensionSupport.class);
 
@@ -67,6 +70,31 @@ public class ClaimsSetExtensionSupport {
         return null;
     }
 
+    /**
+     * Parse flag indicating the client was stored from the given token claims set.
+     * 
+     * @param claimsSet token claims set
+     * @return flag
+     */
+    public static boolean parseAutoRegisteredStored(@Nonnull final TokenClaimsSet claimsSet) {
+        return parseAutoRegisteredStored(claimsSet.getClaimsSet());
+    }
+
+    /**
+     * Parse flag indicating the client was stored from the given JWT claims set.
+     * 
+     * @param claimsSet JWT claims set
+     * @return flag
+     */
+    public static boolean parseAutoRegisteredStored(@Nullable final JWTClaimsSet claimsSet) {
+        try {
+            return claimsSet != null ? claimsSet.getBooleanClaim(KEY_AUTO_REGISTERED_STORED) : false;
+        } catch (final ParseException e) {
+            log.warn("Could not parse auto registered stored flag from the claims set", e);
+        }
+        return false;
+    }
+
     /**
      * Parse automatically registered trust chain from the given map pf claims
      * 
@@ -85,4 +113,18 @@ public class ClaimsSetExtensionSupport {
                 .orElse(null);
     }
 
+    /**
+     * Parse flag indicating the client was stored from the given map pf claims
+     * 
+     * @param claimsSet map of claims
+     * @return flag
+     */
+    public static boolean parseAutoRegisteredStored(@Nullable final Map<String,Object> claimsSet) {
+        return Optional.ofNullable(claimsSet)
+                .map(set -> set.get(KEY_AUTO_REGISTERED_STORED))
+                .filter(Boolean.class::isInstance)
+                .map(Boolean.class::cast)
+                .orElse(false);
+    }
+
 }
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/decoding/impl/ExplicitClientRegistrationRequestDecoder.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/decoding/impl/ExplicitClientRegistrationRequestDecoder.java
index 7669f97..e261c01 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/decoding/impl/ExplicitClientRegistrationRequestDecoder.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/decoding/impl/ExplicitClientRegistrationRequestDecoder.java
@@ -16,8 +16,6 @@ package net.shibboleth.idp.plugin.oidc.op.oidfed.decoding.impl;
 
 import java.io.IOException;
 import java.net.URI;
-import java.text.ParseException;
-import java.util.ArrayList;
 import java.util.List;
 
 import javax.annotation.Nonnull;
@@ -26,11 +24,9 @@ import javax.annotation.Nullable;
 import org.opensaml.messaging.decoder.MessageDecodingException;
 import org.slf4j.Logger;
 
-import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.core.type.TypeReference;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.google.common.base.MoreObjects;
-import com.nimbusds.jwt.SignedJWT;
 import com.nimbusds.oauth2.sdk.http.HTTPRequest;
 import com.nimbusds.oauth2.sdk.http.JakartaServletUtils;
 
@@ -40,12 +36,11 @@ import net.shibboleth.idp.plugin.oidc.op.oauth2.decoding.impl.BaseOAuth2RequestD
 import net.shibboleth.idp.plugin.oidc.op.oidfed.messaging.impl.ExplicitClientRegistrationRequest;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.EntityConfiguration;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.EntityStatement;
-import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.impl.EntityConfigurationImpl;
-import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.impl.SubordinateStatementImpl;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.util.EntityStatementHelper;
 import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
 import net.shibboleth.shared.component.ComponentInitializationException;
 import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.logic.ConstraintViolationException;
 import net.shibboleth.shared.primitive.LoggerFactory;
 
 /**
@@ -97,22 +92,28 @@ public class ExplicitClientRegistrationRequestDecoder
             }
             final String contentType = request.getContentType();
             if ("application/entity-statement+jwt".equals(contentType)) {
-                if (deserializeEntityStatement(httpRequest.getQuery()) instanceof EntityConfiguration configuration) {
-                    log.trace("Entity configuration claims set: {}", configuration.getParsedPayload());
-                    return new ExplicitClientRegistrationRequest(uri, configuration);
+                final String query = httpRequest.getQuery();
+                if (query != null) {
+                    try {
+                        assert objectMapper != null;
+                        final EntityStatement<?> statement =
+                                EntityStatementHelper.parseEntityStatement(query, objectMapper);
+                        if (statement instanceof EntityConfiguration configuration) {
+                            log.trace("Entity configuration claims set: {}", configuration.getParsedPayload());
+                            return new ExplicitClientRegistrationRequest(uri, configuration);
+                        }
+                    } catch (final ConstraintViolationException e) {
+                        throw new MessageDecodingException("Could not deserialize entity configuration", e);
+                    }
                 }
                 throw new MessageDecodingException("Could not deserialize entity configuration");
             } else if ("application/trust-chain+json".equals(contentType)) {
                 final TypeReference<List<String>> typeReference = new TypeReference<List<String>>() {};
                 final List<String> strings = objectMapper.readValue(httpRequest.getQuery(), typeReference);
-                final List<EntityStatement<?>> trustChain = new ArrayList<>();
-                strings.forEach(item -> {
-                    final EntityStatement<?> deserialized = deserializeEntityStatement(item);
-                    if (deserialized != null) {
-                        trustChain.add(deserialized);
-                    }
-                });
-                if (trustChain.isEmpty()) {
+                assert strings != null; assert objectMapper != null;
+                final List<EntityStatement<?>> trustChain =
+                        EntityStatementHelper.deserializeTrustChain(strings, objectMapper);
+                if (trustChain == null) {
                     throw new MessageDecodingException("Could not deserialize trust chain");
                 }
                 return new ExplicitClientRegistrationRequest(uri, trustChain);
@@ -126,25 +127,6 @@ public class ExplicitClientRegistrationRequestDecoder
         }
     }
 
-    @Nullable private EntityStatement<?> deserializeEntityStatement(@Nullable final String input) {
-        try {
-            final SignedJWT jwt = SignedJWT.parse(input);
-            assert jwt != null;
-            final String subject = jwt.getJWTClaimsSet().getSubject();
-            final String issuer = jwt.getJWTClaimsSet().getIssuer();
-            if (subject != null && subject.equals(issuer)) {
-                assert objectMapper != null;
-                return EntityConfigurationImpl.parse(jwt, objectMapper);
-            } else {
-                assert objectMapper != null;
-                return SubordinateStatementImpl.parse(jwt, objectMapper);
-            }
-        } catch (final JsonProcessingException | ParseException e) {
-            log.warn("Could not parse entity statement", e);
-            return null;
-        }
-    }
-
     /** {@inheritDoc} */
     @Override
     protected String getMessageToLog(@Nullable final ExplicitClientRegistrationRequest message) {
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/DefaultEntityStatementContentValidationCondition.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/DefaultEntityStatementContentValidationCondition.java
index 1fa5054..c071569 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/DefaultEntityStatementContentValidationCondition.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/DefaultEntityStatementContentValidationCondition.java
@@ -25,6 +25,7 @@ import org.opensaml.profile.context.ProfileRequestContext;
 import org.slf4j.Logger;
 
 import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.SignedJWT;
 
 import jakarta.servlet.http.HttpServletRequest;
 import net.shibboleth.oidc.jwt.claims.ClaimsValidator;
@@ -57,6 +58,9 @@ public class DefaultEntityStatementContentValidationCondition
     /** The claims validator to use for validating the entity statement claims. */
     @NonnullAfterInit private ClaimsValidator claimsValidator;
 
+    /** The header validator to use for validating the entity statement header. */
+    @NonnullAfterInit private BiPredicate<SignedJWT, ProfileRequestContext> headerValidator;
+
     /** Supplier for the Current HTTP request, if available. */
     @NonnullAfterInit private NonnullSupplier<HttpServletRequest> httpServletRequestSupplier;
 
@@ -70,6 +74,16 @@ public class DefaultEntityStatementContentValidationCondition
         claimsValidator = Constraint.isNotNull(validator, "Claims validator cannot be null");
     }
 
+    /**
+     * Set the header validator to use for validating the entity statement header.
+     * 
+     * @param validator header validator
+     */
+    public void setHeaderValidator(@Nonnull final BiPredicate<SignedJWT, ProfileRequestContext> validator) {
+        checkSetterPreconditions();
+        headerValidator = Constraint.isNotNull(validator, "Header validator cannot be null");
+    }
+
     /**
      * Set the current HTTP request Supplier.
      *
@@ -87,6 +101,9 @@ public class DefaultEntityStatementContentValidationCondition
         if (claimsValidator == null) {
             throw new ComponentInitializationException("Claims validator cannot be null");
         }
+        if (headerValidator == null) {
+            throw new ComponentInitializationException("Header validator cannot be null");
+        }
         if (httpServletRequestSupplier == null) {
             throw new ComponentInitializationException("Http servlet request supplier cannot be null");
         }
@@ -114,8 +131,18 @@ public class DefaultEntityStatementContentValidationCondition
                 .filter(ProfileRequestContext.class::isInstance)
                 .map(ProfileRequestContext.class::cast)
                 .orElse(null);
+        final ProfileRequestContext nonNullProfileRequestContext;
         if (profileRequestContext == null) {
             log.warn("Could not resolve profile request context, supplying an empty one to claims validators");
+            nonNullProfileRequestContext = new ProfileRequestContext();
+        } else {
+            nonNullProfileRequestContext = profileRequestContext;
+        }
+
+        //TODO: make validator interface to commons?
+        if (!headerValidator.test(wrapper.getJwt(), nonNullProfileRequestContext)) {
+            log.warn("Header validation failed for entity statement {}", responseContainer.getEntityId());
+            return false;
         }
 
         try {
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/DefaultTrustChainHeaderValidationCondition.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/DefaultTrustChainHeaderValidationCondition.java
new file mode 100644
index 0000000..32cdee3
--- /dev/null
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/DefaultTrustChainHeaderValidationCondition.java
@@ -0,0 +1,196 @@
+/*
+ * 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.plugin.oidc.op.oidfed.metadata.cache;
+
+import java.text.ParseException;
+import java.util.List;
+import java.util.Set;
+import java.util.function.BiPredicate;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.nimbusds.jose.JWSHeader;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.SignedJWT;
+
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.EntityStatement;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.util.EntityStatementHelper;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * Default validator that validates the syntax of trust_chain and peer_trust_chain headers if they're allowed to be
+ * used with the given entity statement {@link SignedJWT}.
+ */
+public class DefaultTrustChainHeaderValidationCondition extends AbstractIdentifiableInitializableComponent
+    implements BiPredicate<SignedJWT, ProfileRequestContext> {
+
+    /** Class logger. */
+    @Nonnull private Logger log = LoggerFactory.getLogger(DefaultTrustChainHeaderValidationCondition.class);
+
+    /** JSON object mapper used for decoding entity statement payload. */
+    @NonnullAfterInit private ObjectMapper objectMapper;
+
+    /** The flag to indicate allowing the use of trust_chain header. */
+    private boolean allowTrustChainHeader = false;
+
+    /** The flag to indicate allowing the use of peer_trust_chain header. */
+    private boolean allowPeerTrustChainHeader = false;
+
+    /** The flag to indicate requiring the same trust anchor if both headers are present. */
+    private boolean requireCommonTrustAnchor = true;
+
+    /**
+     * Set the JSON {@link ObjectMapper} used for decoding entity statement payload.
+     * 
+     * @param mapper object mapper
+     */
+    public void setObjectMapper(@Nonnull final ObjectMapper mapper) {
+        checkSetterPreconditions();
+        objectMapper = Constraint.isNotNull(mapper, "Object mapper cannot be null");
+    }
+
+    /**
+     * Set the flag to indicate allowing the use of trust_chain header.
+     * 
+     * @param flag value
+     */
+    public void setAllowTrustChainHeader(final boolean flag) {
+        checkSetterPreconditions();
+        allowTrustChainHeader = flag;
+    }
+
+    /**
+     * Set the flag to indicate allowing the use of peer_trust_chain header.
+     * 
+     * @param flag value
+     */
+    public void setAllowPeerTrustChainHeader(final boolean flag) {
+        checkSetterPreconditions();
+        allowPeerTrustChainHeader = flag;
+    }
+
+    /**
+     * Set the flag to indicate requiring the same trust anchor if both headers are present.
+     * 
+     * @param flag value
+     */
+    public void setRequireCommonTrustAnchor(final boolean flag) {
+        checkSetterPreconditions();
+        requireCommonTrustAnchor = flag;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+
+        if (objectMapper == null) {
+            throw new ComponentInitializationException("ObjectMapper cannot be null");
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean test(@Nullable final SignedJWT signedJwt,
+            @Nullable final ProfileRequestContext profileRequestContext) {
+        checkComponentActive();
+        if (signedJwt == null || signedJwt.getHeader() == null || profileRequestContext == null) {
+            log.error("Invalid input: signed JWT nor profile request context cannot be null");
+            return false;
+        }
+        final JWTClaimsSet claimsSet;
+        try {
+            claimsSet = signedJwt.getJWTClaimsSet();
+        } catch (final ParseException e) {
+            log.warn("Could not parse entity statement claims set", e);
+            return false;
+        }
+        final JWSHeader header = signedJwt.getHeader();
+        assert header != null;
+        final Set<String> params = header.getIncludedParams();
+        final List<EntityStatement<?>> trustChain;
+        if (params.contains("trust_chain")) {
+            if (!allowTrustChainHeader) {
+                log.warn("The use of trust_chain header is not allowed for {}", claimsSet.getSubject());
+                return false;
+            }
+            trustChain = parseToList(header.getCustomParam("trust_chain"), profileRequestContext);
+            if (trustChain == null) {
+                log.warn("Could not parse trust_chain contents into a trust chain");
+                return false;
+            }
+        } else {
+            trustChain = null;
+        }
+
+        final List<EntityStatement<?>> peerTrustChain;
+        if (params.contains("peer_trust_chain")) {
+            if (!allowPeerTrustChainHeader) {
+                log.warn("The use of peer_trust_chain header is not allowed for {}", claimsSet.getSubject());
+                return false;
+            }
+            peerTrustChain = parseToList(header.getCustomParam("peer_trust_chain"), profileRequestContext);
+            if (peerTrustChain == null) {
+                log.warn("Could not parse peer_trust_chain contents into a trust chain");
+                return false;
+            }
+        } else {
+            peerTrustChain = null;
+        }
+
+        if (trustChain != null && peerTrustChain != null) {
+            if (requireCommonTrustAnchor) {
+                log.debug("Both trust_chain and peer_trust_chain exists, verifying the trust anchors match");
+                final String trustChainAnchor = trustChain.get(trustChain.size() - 1).getSubject();
+                final String peerTrustChainAnchor = peerTrustChain.get(peerTrustChain.size() - 1).getSubject();
+                if (!trustChainAnchor.equals(peerTrustChainAnchor)) {
+                    log.warn("The trust_chain anchor {} does not match with the peer_trust_chain anchor {}",
+                            trustChainAnchor, peerTrustChainAnchor);
+                    return false;
+                }
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Parse the raw object to trust chain.
+     * 
+     * @param input raw object
+     * @param profileRequestContext profile request context
+     * @return trust chain if the input could be parsed, null otherwise
+     */
+    @Nullable protected List<EntityStatement<?>> parseToList(@Nullable final Object input,
+            @Nonnull final ProfileRequestContext profileRequestContext) {
+        if (input instanceof List<?> list) {
+            final List<String> strings = list.stream()
+                    .filter(String.class::isInstance)
+                    .map(String.class::cast)
+                    .toList();
+            assert strings != null; assert objectMapper != null;
+            return EntityStatementHelper.deserializeTrustChain(strings, objectMapper);
+        }
+        return null;
+    }
+}
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/util/EntityStatementHelper.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/util/EntityStatementHelper.java
index e3ab216..a095edc 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/util/EntityStatementHelper.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/util/EntityStatementHelper.java
@@ -15,6 +15,10 @@
 package net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.util;
 
 import java.net.URI;
+import java.text.ParseException;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Comparator;
 import java.util.List;
 import java.util.Objects;
 import java.util.Optional;
@@ -24,11 +28,22 @@ import javax.annotation.Nullable;
 
 import org.slf4j.Logger;
 
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.SignedJWT;
+
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.EntityConfiguration;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.EntityStatement;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.SubordinateStatement;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.SubjectEntityIDCriterion;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.configuration.EntityConfigurationContainer;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.impl.EntityConfigurationImpl;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.impl.SubordinateStatementImpl;
 import net.shibboleth.oidc.metadata.cache.MetadataCache;
 import net.shibboleth.oidc.metadata.cache.MetadataCacheException;
+import net.shibboleth.shared.annotation.constraint.NonnullElements;
+import net.shibboleth.shared.logic.ConstraintViolationException;
 import net.shibboleth.shared.primitive.LoggerFactory;
 import net.shibboleth.shared.resolver.CriteriaSet;
 
@@ -125,4 +140,103 @@ public class EntityStatementHelper {
                 .map(statement -> statement.getSubject())
                 .anyMatch(id -> subject.equals(id));
     }
+
+    /**
+     * Deserialize the given list of strings into a list of entity statements. The method also verifies that the
+     * trust chain syntax is correct: it contains three or more items, it begins and ends with an entity configuration,
+     * other items being subordinate statements.
+     * 
+     * @param chain serialized trust chain
+     * @param objectMapper object mapper used for deserializing the statement payloads
+     * @return list of entity statements, or null if the input was invalid
+     */
+    @Nullable public static List<EntityStatement<?>> deserializeTrustChain(
+            @Nonnull @NonnullElements final List<String> chain, @Nonnull final ObjectMapper objectMapper) {
+        if (chain.size() < 3) {
+            log.warn("Invalid trust chain: shorter than three items");
+            return null;
+        }
+        final List<EntityStatement<?>> result = new ArrayList<>();
+        for (final String entry : chain) {
+            try {
+                assert entry != null;
+                result.add(parseEntityStatement(entry, objectMapper));
+            } catch (final ConstraintViolationException e) {
+                log.warn("Could not deserialize the given trust chain", e);
+                return null;
+            }
+        }
+        if (!(result.get(0) instanceof EntityConfiguration)) {
+            log.warn("Trust chain does not begin with an entity configuration");
+            return null;
+        }
+        for (int i = 1; i < result.size() - 1; i++) {
+            if (!(result.get(i) instanceof SubordinateStatement)) {
+                log.warn("Trust chain entry {} is not a subordinate statement", i);
+                return null;
+            }
+        }
+        if (!(result.get(result.size() - 1) instanceof EntityConfiguration)) {
+            log.warn("Trust chain does not end with an entity configuration");
+            return null;
+        }
+        return result;
+    }
+
+    /**
+     * Parse an entity statement from the given input. If the subject and issuer matches in the payload, an entity
+     * configuration is returned. Otherwise, a subordinate statement is returned.
+     * 
+     * @param input serialized entity statement
+     * @param objectMapper object mapper used for deserializing the statement payload
+     * @return entity configuration or subordinate statement, edpending on the content
+     * @throws ConstraintViolationException if input was invalid
+     */
+    @Nonnull public static EntityStatement<?> parseEntityStatement(@Nonnull final String input,
+            @Nonnull final ObjectMapper objectMapper) throws ConstraintViolationException {
+        final SignedJWT jwt;
+        final JWTClaimsSet claimsSet;
+        try {
+            jwt = SignedJWT.parse(input);
+            assert jwt != null;
+            claimsSet = jwt.getJWTClaimsSet();
+        } catch (final ParseException e) {
+            log.warn("Could not construct a JWT from the entity statement", e);
+            throw new ConstraintViolationException(e.getMessage());
+        }
+        assert objectMapper != null;
+        final String subject = claimsSet.getSubject();
+        if (subject == null) {
+            throw new ConstraintViolationException("Subject is null");
+        }
+        final String issuer = claimsSet.getIssuer();
+        if (issuer == null) {
+            throw new ConstraintViolationException("Issuer is null");
+        }
+        try {
+            if (subject.equals(issuer)) {
+                return EntityConfigurationImpl.parse(jwt, objectMapper);
+            } else {
+                return SubordinateStatementImpl.parse(jwt, objectMapper);
+            }
+        } catch (final JsonProcessingException e) {
+            log.debug("Could not parse the entity statement " + subject + ", issued by " + issuer, e);
+            throw new ConstraintViolationException(e.getMessage());
+        }
+    }
+
+    /**
+     * Parse the expiration time from the trust chain: the lowest expiration time value is taken from the chain.
+     * 
+     * @param trustChain trust chain
+     * @return expiration instant
+     */
+    @Nonnull public static Instant parseTrustChainExpiration(@Nonnull final List<EntityStatement<?>> trustChain) {
+        final Instant result = trustChain.stream()
+                .map(statement -> statement.getParsedPayload().getExpiration())
+                .min(Comparator.comparing(Instant::toEpochMilli))
+                .orElse(Instant.now());
+        assert result != null;
+        return result;
+    }
 }
\ No newline at end of file
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/config/impl/DefaultOIDFederationAutomaticRegistrationProfileConfiguration.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/config/impl/DefaultOIDFederationAutomaticRegistrationProfileConfiguration.java
index 3c78977..533b00f 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/config/impl/DefaultOIDFederationAutomaticRegistrationProfileConfiguration.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/config/impl/DefaultOIDFederationAutomaticRegistrationProfileConfiguration.java
@@ -14,10 +14,18 @@
 
 package net.shibboleth.idp.plugin.oidc.op.oidfed.profile.config.impl;
 
+import java.util.function.Function;
+
 import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
 
 import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.config.OIDFederationAutomaticRegistrationProfileConfiguration;
 import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.logic.FunctionSupport;
+import net.shibboleth.shared.primitive.StringSupport;
 
 /**
  * Implementation of a profile configuration for the OpenID Federation Automatic Registration.
@@ -30,6 +38,12 @@ public class DefaultOIDFederationAutomaticRegistrationProfileConfiguration
     @Nonnull @NotEmpty
     public static final String PROFILE_COUNTER = "net.shibboleth.idp.profiles.oidfed.automaticregistration";
 
+    /** Lookup function to supply claims set decoration type. */
+    @Nonnull private Function<ProfileRequestContext,String> claimsSetDecorationTypeLookupStrategy;
+
+    /** Lookup function to supply claims set decoration type for clients with request-provided trust chains. */
+    @Nonnull private Function<ProfileRequestContext,String> providedChainClaimsSetDecorationTypeLookupStrategy;
+
     /**
      * Constructor.
      */
@@ -44,5 +58,60 @@ public class DefaultOIDFederationAutomaticRegistrationProfileConfiguration
      */
     public DefaultOIDFederationAutomaticRegistrationProfileConfiguration(@Nonnull @NotEmpty final String profileId) {
         super(profileId);
+        claimsSetDecorationTypeLookupStrategy = FunctionSupport.constant(null);
+        providedChainClaimsSetDecorationTypeLookupStrategy = FunctionSupport.constant(null);
     }
+
+    /** {@inheritDoc} */
+    @Override @Nullable @NotEmpty
+    public String getDefaultClaimsSetDecorationType(@Nullable final ProfileRequestContext profileRequestContext) {
+         return StringSupport.trimOrNull(claimsSetDecorationTypeLookupStrategy.apply(profileRequestContext));
+     }
+
+    /**
+     * Set claims set decoration type.
+     * 
+     * @param type decoration type, or null for default
+     */
+     public void setDefaultClaimsSetDecorationType(@Nullable @NotEmpty final String type) {
+         claimsSetDecorationTypeLookupStrategy = FunctionSupport.constant(StringSupport.trimOrNull(type));
+     }
+
+     /**
+      * Set lookup strategy for claims set decoration type.
+      * 
+      * @param strategy lookup strategy
+      */
+      public void setDefaultClaimsSetDecorationTypeLookupStrategy(
+              @Nonnull final Function<ProfileRequestContext,String> strategy) {
+          claimsSetDecorationTypeLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null");
+      }
+
+     /** {@inheritDoc} */
+     @Override @Nullable @NotEmpty
+     public String getProvidedChainClaimsSetDecorationType(
+             @Nullable final ProfileRequestContext profileRequestContext) {
+          return StringSupport.trimOrNull(
+                  providedChainClaimsSetDecorationTypeLookupStrategy.apply(profileRequestContext));
+      }
+
+     /**
+      * Set claims set decoration type for clients with request-provided trust chain.
+      * 
+      * @param type decoration type
+      */
+      public void setProvidedChainClaimsSetDecorationType(@Nullable @NotEmpty final String type) {
+          providedChainClaimsSetDecorationTypeLookupStrategy = FunctionSupport.constant(StringSupport.trimOrNull(type));
+      }
+
+      /**
+       * Set lookup strategy for claims set decoration type for clients with request-provided trust chain.
+       * 
+       * @param strategy lookup strategy
+       */
+       public void setProvidedChainClaimsSetDecorationTypeLookupStrategy(
+               @Nonnull final Function<ProfileRequestContext,String> strategy) {
+           providedChainClaimsSetDecorationTypeLookupStrategy =
+                   Constraint.isNotNull(strategy, "Lookup strategy cannot be null");
+       }
 }
\ No newline at end of file
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/AbstractTokenClaimsSetDecorator.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/AbstractTokenClaimsSetDecorator.java
new file mode 100644
index 0000000..ca4148a
--- /dev/null
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/AbstractTokenClaimsSetDecorator.java
@@ -0,0 +1,123 @@
+/*
+ * 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.plugin.oidc.op.oidfed.profile.impl;
+
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiConsumer;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.context.AutomaticRegistrationContext;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.ThreadSafeAfterInit;
+import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * Abstract base class for the token type specific claims set decorators.
+ */
+ at ThreadSafeAfterInit
+public abstract class AbstractTokenClaimsSetDecorator extends AbstractIdentifiableInitializableComponent
+    implements BiConsumer<Map<String, Object>, ProfileRequestContext> {
+
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(AbstractTokenClaimsSetDecorator.class);
+
+    /** Lookup strategy for automatic registration context. */
+    @Nonnull
+    private Function<ProfileRequestContext, AutomaticRegistrationContext> automaticRegistrationContextLookupStrategy;
+
+    /** Map of claims set decorators. */
+    @NonnullAfterInit
+    private Map<String, List<BiConsumer<Map<String, Object>, ProfileRequestContext>>> claimsSetDecorators;
+
+    /**
+     * Constructor.
+     */
+    public AbstractTokenClaimsSetDecorator() {
+        final Function<ProfileRequestContext, AutomaticRegistrationContext> arcls = 
+                new ChildContextLookup<>(AutomaticRegistrationContext.class).compose(
+                        new InboundMessageContextLookup());
+        assert arcls != null;
+        automaticRegistrationContextLookupStrategy = arcls;
+    }
+
+    /**
+     * Set the lookup strategy for automatic registration context.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setAutomaticRegistrationContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext, AutomaticRegistrationContext> strategy) {
+        checkSetterPreconditions();
+        automaticRegistrationContextLookupStrategy =
+                Constraint.isNotNull(strategy, "AutomaticRegistrationContextLookupStrategy cannot be null");
+    }
+
+    /**
+     * Set the map of claims set decorators.
+     * 
+     * @param map decorators
+     */
+    public void setClaimsSetDecorators(
+            @Nonnull final Map<String, List<BiConsumer<Map<String, Object>, ProfileRequestContext>>> map) {
+        checkSetterPreconditions();
+        claimsSetDecorators = Constraint.isNotNull(map, "ClaimsSetDecorators cannot be null");
+        Constraint.isNotEmpty(map.entrySet(), "ClaimsSetDecorators cannot be empty");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+
+        if (claimsSetDecorators == null) {
+            throw new ComponentInitializationException("ClaimsSetDecorators cannot be null");
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void accept(@Nullable final Map<String, Object> claimsSet,
+            @Nullable final ProfileRequestContext profileRequestContext) {
+        final AutomaticRegistrationContext registrationContext =
+                automaticRegistrationContextLookupStrategy.apply(profileRequestContext);
+        if (registrationContext == null) {
+            log.error("Could not resolve automatic registration context, cannot proceed");
+            return;
+        }
+        final String decorationType = registrationContext.getClaimsSetDecorationType();
+        final List<BiConsumer<Map<String, Object>, ProfileRequestContext>> decorators = 
+                claimsSetDecorators.get(decorationType);
+        if (decorators == null) {
+            log.error("Could not resolve decorators for type {}", decorationType);
+            return;
+        }
+        for (final BiConsumer<Map<String, Object>, ProfileRequestContext> decorator : decorators) {
+            log.trace("Running decorator {}", decorator);
+            decorator.accept(claimsSet, profileRequestContext);
+        }
+    }
+}
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/AutoRegisteredClientInformationStoreClaimsSetDecorator.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/AutoRegisteredClientInformationStoreClaimsSetDecorator.java
new file mode 100644
index 0000000..c911d9e
--- /dev/null
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/AutoRegisteredClientInformationStoreClaimsSetDecorator.java
@@ -0,0 +1,145 @@
+/*
+ * 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.plugin.oidc.op.oidfed.profile.impl;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.BiConsumer;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
+import org.slf4j.Logger;
+
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformation;
+
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.EntityStatement;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.util.EntityStatementHelper;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.context.RelyingPartyTrustChainContext;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.support.ClaimsSetExtensionSupport;
+import net.shibboleth.oidc.metadata.ClientInformationManager;
+import net.shibboleth.oidc.metadata.ClientInformationManagerException;
+import net.shibboleth.oidc.metadata.context.OIDCMetadataContext;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.ThreadSafeAfterInit;
+import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * A decorator that stores the client information for the configurable {@link ClientInformationManager} and sets a
+ * custom claim {@link ClaimsSetExtensionSupport#KEY_AUTO_REGISTERED_STORED} as true if the operation was successful.
+ */
+ at ThreadSafeAfterInit
+public class AutoRegisteredClientInformationStoreClaimsSetDecorator
+    extends AbstractIdentifiableInitializableComponent
+    implements BiConsumer<Map<String, Object>, ProfileRequestContext> {
+
+    /** Class logger. */
+    @Nonnull private final Logger log =
+            LoggerFactory.getLogger(AutoRegisteredClientInformationStoreClaimsSetDecorator.class);
+
+    /** Strategy used to lookup the trust chain context. */
+    @Nonnull protected Function<ProfileRequestContext, RelyingPartyTrustChainContext> trustChainContextLookupStrategy;
+
+    /** The client information manager used for storing the information. */
+    @NonnullAfterInit private ClientInformationManager clientInformationManager;
+
+    /**
+     * Constructor.
+     */
+    public AutoRegisteredClientInformationStoreClaimsSetDecorator() {
+        final Function<ProfileRequestContext, RelyingPartyTrustChainContext> tcls =
+                new ChildContextLookup<>(RelyingPartyTrustChainContext.class).compose(
+                        new InboundMessageContextLookup());
+        assert tcls != null;
+        trustChainContextLookupStrategy = tcls;
+    }
+
+    /**
+     * Set the strategy used to lookup the trust chain context.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setTrustChainContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext, RelyingPartyTrustChainContext> strategy) {
+        checkSetterPreconditions();
+        trustChainContextLookupStrategy =
+                Constraint.isNotNull(strategy, "TrustChainContextLookupStrategy cannot be null");
+    }
+
+    /**
+     * Set the client information manager used for storing the information.
+     * 
+     * @param manager client information manager
+     */
+    public void setClientInformationManager(@Nonnull final ClientInformationManager manager) {
+        checkSetterPreconditions();
+        clientInformationManager = Constraint.isNotNull(manager, "ClientInformationManager cannot be null!");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        
+        if (clientInformationManager == null) {
+            throw new ComponentInitializationException("ClientInformationManager cannot be null");
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void accept(@Nullable final Map<String, Object> claimsSet,
+            @Nullable final ProfileRequestContext profileRequestContext) {
+        checkComponentActive();
+
+        if (claimsSet == null) {
+            log.error("Claims set is null");
+            return;
+        }
+        final List<EntityStatement<?>> selectedTrustChain =
+                Optional.ofNullable(trustChainContextLookupStrategy.apply(profileRequestContext))
+                .map(trustChainContext -> trustChainContext.getSelectedTrustChain())
+                .map(chain -> chain.getTrustChain())
+                .orElse(null);
+        if (selectedTrustChain != null) {
+            final OIDCClientInformation clientInformation = Optional.ofNullable(profileRequestContext)
+                    .map(prc -> prc.getInboundMessageContext())
+                    .map(msgCtx -> msgCtx.getSubcontext(OIDCMetadataContext.class))
+                    .map(oidcCtx -> oidcCtx.getClientInformation())
+                    .orElse(null);
+            if (clientInformation != null) {
+                try {
+                    clientInformationManager.storeClientInformation(clientInformation,
+                            EntityStatementHelper.parseTrustChainExpiration(selectedTrustChain), true);
+                    claimsSet.put(ClaimsSetExtensionSupport.KEY_AUTO_REGISTERED_STORED, true);
+                } catch (final ClientInformationManagerException e) {
+                    log.warn("Could not store client information record {}", clientInformation.getID().getValue());
+                }
+            } else {
+                log.error("Could not resolve client information, cannot store it");
+            }
+        } else {
+            log.debug("No selected trust chain found, nothing to store");
+        }
+    }
+}
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/AutoRegisteredTrustChainAccessTokenClaimsSetDecorator.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/AutoRegisteredTrustChainAccessTokenClaimsSetDecorator.java
index 0200d3e..4cf2b8e 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/AutoRegisteredTrustChainAccessTokenClaimsSetDecorator.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/AutoRegisteredTrustChainAccessTokenClaimsSetDecorator.java
@@ -14,62 +14,12 @@
 
 package net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl;
 
-import java.util.List;
-import java.util.Map;
-import java.util.function.Function;
-
-import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
-
-import org.opensaml.profile.context.ProfileRequestContext;
-
-import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.navigate.DefaultSelectedTrustChainIDsLookupStrategy;
-import net.shibboleth.idp.plugin.oidc.op.oidfed.support.ClaimsSetExtensionSupport;
 import net.shibboleth.idp.plugin.oidc.op.profile.AccessTokenClaimsSetDecorator;
-import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
-import net.shibboleth.shared.logic.Constraint;
 
 /**
- * The decorator that adds a custom claim {@link ClaimsSetExtensionSupport#KEY_AUTO_REGISTERED_TRUST_CHAIN} to the
- * access token claims set if a value was resolved via {@link #selectedTrustChainIDsLookupStrategy}.
+ * Default claims set decorator for access token claims sets.
  */
-public class AutoRegisteredTrustChainAccessTokenClaimsSetDecorator extends AbstractIdentifiableInitializableComponent
+public class AutoRegisteredTrustChainAccessTokenClaimsSetDecorator extends AbstractTokenClaimsSetDecorator
     implements AccessTokenClaimsSetDecorator {
 
-    /** The strategy used to locate IDs for the selected trust chain. */
-    @Nonnull private Function<ProfileRequestContext,List<String>> selectedTrustChainIDsLookupStrategy;
-
-    /**
-     * Constructor.
-     */
-    public AutoRegisteredTrustChainAccessTokenClaimsSetDecorator() {
-        selectedTrustChainIDsLookupStrategy = new DefaultSelectedTrustChainIDsLookupStrategy();
-    }
-
-    /**
-     * Set the lookup strategy used to locate IDs for the selected trust chain.
-     * 
-     * @param strategy What to set
-     */
-    public void setSelectedTrustChainIDsLookupStrategy(
-            @Nonnull final Function<ProfileRequestContext, List<String>> strategy) {
-        checkSetterPreconditions();
-    
-        selectedTrustChainIDsLookupStrategy  = Constraint.isNotNull(strategy,
-                "SelectedTrustChainIDsLookupStrategy lookup strategy cannot be null");
-    }
-    /** {@inheritDoc} */
-    @Override
-    public void accept(@Nullable final Map<String, Object> claimsSet,
-            @Nullable final ProfileRequestContext profileRequestContext) {
-        checkComponentActive();
-        if (claimsSet != null) {
-            final List<String> trustChainIds = selectedTrustChainIDsLookupStrategy.apply(profileRequestContext);
-            if (trustChainIds != null && !trustChainIds.isEmpty()) {
-                claimsSet.put(ClaimsSetExtensionSupport.KEY_AUTO_REGISTERED_TRUST_CHAIN, trustChainIds);
-            }
-            
-        }
-    }
-
 }
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/AutoRegisteredTrustChainAuthorizationCodeClaimsSetDecorator.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/AutoRegisteredTrustChainAuthorizationCodeClaimsSetDecorator.java
index bb948c0..7108c34 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/AutoRegisteredTrustChainAuthorizationCodeClaimsSetDecorator.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/AutoRegisteredTrustChainAuthorizationCodeClaimsSetDecorator.java
@@ -14,62 +14,12 @@
 
 package net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl;
 
-import java.util.List;
-import java.util.Map;
-import java.util.function.Function;
-
-import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
-
-import org.opensaml.profile.context.ProfileRequestContext;
-
-import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.navigate.DefaultSelectedTrustChainIDsLookupStrategy;
-import net.shibboleth.idp.plugin.oidc.op.oidfed.support.ClaimsSetExtensionSupport;
 import net.shibboleth.idp.plugin.oidc.op.profile.AuthorizationCodeClaimsSetDecorator;
-import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
-import net.shibboleth.shared.logic.Constraint;
 
 /**
- * The decorator that adds a custom claim {@link ClaimsSetExtensionSupport#KEY_AUTO_REGISTERED_TRUST_CHAIN} to the
- * builder if a value was resolved via {@link #selectedTrustChainIDsLookupStrategy}.
+ * Default claims set decorator for authorization code claims sets.
  */
-public class AutoRegisteredTrustChainAuthorizationCodeClaimsSetDecorator extends AbstractIdentifiableInitializableComponent
+public class AutoRegisteredTrustChainAuthorizationCodeClaimsSetDecorator extends AbstractTokenClaimsSetDecorator
     implements AuthorizationCodeClaimsSetDecorator {
 
-    /** The strategy used to locate IDs for the selected trust chain. */
-    @Nonnull private Function<ProfileRequestContext,List<String>> selectedTrustChainIDsLookupStrategy;
-
-    /**
-     * Constructor.
-     */
-    public AutoRegisteredTrustChainAuthorizationCodeClaimsSetDecorator() {
-        selectedTrustChainIDsLookupStrategy = new DefaultSelectedTrustChainIDsLookupStrategy();
-    }
-
-    /**
-     * Set the lookup strategy used to locate IDs for the selected trust chain.
-     * 
-     * @param strategy What to set
-     */
-    public void setSelectedTrustChainIDsLookupStrategy(
-            @Nonnull final Function<ProfileRequestContext, List<String>> strategy) {
-        checkSetterPreconditions();
-    
-        selectedTrustChainIDsLookupStrategy  = Constraint.isNotNull(strategy,
-                "SelectedTrustChainIDsLookupStrategy lookup strategy cannot be null");
-    }
-    /** {@inheritDoc} */
-    @Override
-    public void accept(@Nullable final Map<String, Object> claimsSet,
-            @Nullable final ProfileRequestContext profileRequestContext) {
-        checkComponentActive();
-        if (claimsSet != null) {
-            final List<String> trustChainIds = selectedTrustChainIDsLookupStrategy.apply(profileRequestContext);
-            if (trustChainIds != null && !trustChainIds.isEmpty()) {
-                claimsSet.put(ClaimsSetExtensionSupport.KEY_AUTO_REGISTERED_TRUST_CHAIN, trustChainIds);
-            }
-            
-        }
-    }
-
 }
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/AutoRegisteredTrustChainAuthorizationCodeClaimsSetDecorator.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/AutoRegisteredTrustChainIDsClaimsSetDecorator.java
similarity index 81%
copy from idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/AutoRegisteredTrustChainAuthorizationCodeClaimsSetDecorator.java
copy to idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/AutoRegisteredTrustChainIDsClaimsSetDecorator.java
index bb948c0..7bbbc03 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/AutoRegisteredTrustChainAuthorizationCodeClaimsSetDecorator.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/AutoRegisteredTrustChainIDsClaimsSetDecorator.java
@@ -16,6 +16,7 @@ package net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl;
 
 import java.util.List;
 import java.util.Map;
+import java.util.function.BiConsumer;
 import java.util.function.Function;
 
 import javax.annotation.Nonnull;
@@ -25,16 +26,17 @@ import org.opensaml.profile.context.ProfileRequestContext;
 
 import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.navigate.DefaultSelectedTrustChainIDsLookupStrategy;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.support.ClaimsSetExtensionSupport;
-import net.shibboleth.idp.plugin.oidc.op.profile.AuthorizationCodeClaimsSetDecorator;
+import net.shibboleth.shared.annotation.constraint.ThreadSafeAfterInit;
 import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
 import net.shibboleth.shared.logic.Constraint;
 
 /**
- * The decorator that adds a custom claim {@link ClaimsSetExtensionSupport#KEY_AUTO_REGISTERED_TRUST_CHAIN} to the
- * builder if a value was resolved via {@link #selectedTrustChainIDsLookupStrategy}.
+ * A decorator that adds a custom claim {@link ClaimsSetExtensionSupport#KEY_AUTO_REGISTERED_TRUST_CHAIN} to the
+ * claims set if a value was resolved via {@link #selectedTrustChainIDsLookupStrategy}.
  */
-public class AutoRegisteredTrustChainAuthorizationCodeClaimsSetDecorator extends AbstractIdentifiableInitializableComponent
-    implements AuthorizationCodeClaimsSetDecorator {
+ at ThreadSafeAfterInit
+public class AutoRegisteredTrustChainIDsClaimsSetDecorator extends AbstractIdentifiableInitializableComponent
+    implements BiConsumer<Map<String, Object>, ProfileRequestContext> {
 
     /** The strategy used to locate IDs for the selected trust chain. */
     @Nonnull private Function<ProfileRequestContext,List<String>> selectedTrustChainIDsLookupStrategy;
@@ -42,7 +44,7 @@ public class AutoRegisteredTrustChainAuthorizationCodeClaimsSetDecorator extends
     /**
      * Constructor.
      */
-    public AutoRegisteredTrustChainAuthorizationCodeClaimsSetDecorator() {
+    public AutoRegisteredTrustChainIDsClaimsSetDecorator() {
         selectedTrustChainIDsLookupStrategy = new DefaultSelectedTrustChainIDsLookupStrategy();
     }
 
@@ -54,10 +56,10 @@ public class AutoRegisteredTrustChainAuthorizationCodeClaimsSetDecorator extends
     public void setSelectedTrustChainIDsLookupStrategy(
             @Nonnull final Function<ProfileRequestContext, List<String>> strategy) {
         checkSetterPreconditions();
-    
         selectedTrustChainIDsLookupStrategy  = Constraint.isNotNull(strategy,
                 "SelectedTrustChainIDsLookupStrategy lookup strategy cannot be null");
     }
+
     /** {@inheritDoc} */
     @Override
     public void accept(@Nullable final Map<String, Object> claimsSet,
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/AutoRegisteredTrustChainRefreshTokenClaimsSetDecorator.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/AutoRegisteredTrustChainRefreshTokenClaimsSetDecorator.java
index b3c59d7..8480c94 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/AutoRegisteredTrustChainRefreshTokenClaimsSetDecorator.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/AutoRegisteredTrustChainRefreshTokenClaimsSetDecorator.java
@@ -14,62 +14,12 @@
 
 package net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl;
 
-import java.util.List;
-import java.util.Map;
-import java.util.function.Function;
-
-import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
-
-import org.opensaml.profile.context.ProfileRequestContext;
-
-import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.navigate.DefaultSelectedTrustChainIDsLookupStrategy;
-import net.shibboleth.idp.plugin.oidc.op.oidfed.support.ClaimsSetExtensionSupport;
 import net.shibboleth.idp.plugin.oidc.op.profile.RefreshTokenClaimsSetDecorator;
-import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
-import net.shibboleth.shared.logic.Constraint;
 
 /**
- * The decorator that adds a custom claim {@link ClaimsSetExtensionSupport#KEY_AUTO_REGISTERED_TRUST_CHAIN} to the
- * refresh token claims set if a value was resolved via {@link #selectedTrustChainIDsLookupStrategy}.
+ * Default claims set decorator for refresh token claims sets.
  */
-public class AutoRegisteredTrustChainRefreshTokenClaimsSetDecorator extends AbstractIdentifiableInitializableComponent
+public class AutoRegisteredTrustChainRefreshTokenClaimsSetDecorator extends AbstractTokenClaimsSetDecorator
     implements RefreshTokenClaimsSetDecorator {
 
-    /** The strategy used to locate IDs for the selected trust chain. */
-    @Nonnull private Function<ProfileRequestContext,List<String>> selectedTrustChainIDsLookupStrategy;
-
-    /**
-     * Constructor.
-     */
-    public AutoRegisteredTrustChainRefreshTokenClaimsSetDecorator() {
-        selectedTrustChainIDsLookupStrategy = new DefaultSelectedTrustChainIDsLookupStrategy();
-    }
-
-    /**
-     * Set the lookup strategy used to locate IDs for the selected trust chain.
-     * 
-     * @param strategy What to set
-     */
-    public void setSelectedTrustChainIDsLookupStrategy(
-            @Nonnull final Function<ProfileRequestContext, List<String>> strategy) {
-        checkSetterPreconditions();
-    
-        selectedTrustChainIDsLookupStrategy  = Constraint.isNotNull(strategy,
-                "SelectedTrustChainIDsLookupStrategy lookup strategy cannot be null");
-    }
-    /** {@inheritDoc} */
-    @Override
-    public void accept(@Nullable final Map<String, Object> claimsSet,
-            @Nullable final ProfileRequestContext profileRequestContext) {
-        checkComponentActive();
-        if (claimsSet != null) {
-            final List<String> trustChainIds = selectedTrustChainIDsLookupStrategy.apply(profileRequestContext);
-            if (trustChainIds != null && !trustChainIds.isEmpty()) {
-                claimsSet.put(ClaimsSetExtensionSupport.KEY_AUTO_REGISTERED_TRUST_CHAIN, trustChainIds);
-            }
-            
-        }
-    }
-
 }
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/AutoRegisteredTrustChainRequestUriClaimsSetDecorator.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/AutoRegisteredTrustChainRequestUriClaimsSetDecorator.java
index 1185961..0d60c31 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/AutoRegisteredTrustChainRequestUriClaimsSetDecorator.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/AutoRegisteredTrustChainRequestUriClaimsSetDecorator.java
@@ -14,60 +14,12 @@
 
 package net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl;
 
-import java.util.List;
-import java.util.Map;
-import java.util.function.Function;
-
-import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
-
-import org.opensaml.profile.context.ProfileRequestContext;
-
-import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.navigate.DefaultSelectedTrustChainIDsLookupStrategy;
-import net.shibboleth.idp.plugin.oidc.op.oidfed.support.ClaimsSetExtensionSupport;
 import net.shibboleth.idp.plugin.oidc.op.profile.RequestUriClaimsSetDecorator;
-import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
-import net.shibboleth.shared.logic.Constraint;
 
 /**
- *
+ * Default claims set decorator for pushed authorization request_uri claims sets.
  */
-public class AutoRegisteredTrustChainRequestUriClaimsSetDecorator extends AbstractIdentifiableInitializableComponent
+public class AutoRegisteredTrustChainRequestUriClaimsSetDecorator extends AbstractTokenClaimsSetDecorator
         implements RequestUriClaimsSetDecorator {
 
-    /** The strategy used to locate IDs for the selected trust chain. */
-    @Nonnull private Function<ProfileRequestContext,List<String>> selectedTrustChainIDsLookupStrategy;
-
-    /**
-     * Constructor.
-     */
-    public AutoRegisteredTrustChainRequestUriClaimsSetDecorator() {
-        selectedTrustChainIDsLookupStrategy = new DefaultSelectedTrustChainIDsLookupStrategy();
-    }
-
-    /**
-     * Set the lookup strategy used to locate IDs for the selected trust chain.
-     * 
-     * @param strategy What to set
-     */
-    public void setSelectedTrustChainIDsLookupStrategy(
-            @Nonnull final Function<ProfileRequestContext, List<String>> strategy) {
-        checkSetterPreconditions();
-    
-        selectedTrustChainIDsLookupStrategy  = Constraint.isNotNull(strategy,
-                "SelectedTrustChainIDsLookupStrategy lookup strategy cannot be null");
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    public void accept(@Nullable final Map<String, Object> claimsSet,
-            @Nullable final ProfileRequestContext profileRequestContext) {
-        checkComponentActive();
-
-        final List<String> trustChainIds = selectedTrustChainIDsLookupStrategy.apply(profileRequestContext);
-        if (claimsSet != null && trustChainIds != null && !trustChainIds.isEmpty()) {
-            claimsSet.put(ClaimsSetExtensionSupport.KEY_AUTO_REGISTERED_TRUST_CHAIN, trustChainIds);
-        }
-    }
-
 }
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/CallResolveEntityApi.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/CallResolveEntityApi.java
index 13e3df2..fc0fb5b 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/CallResolveEntityApi.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/CallResolveEntityApi.java
@@ -39,7 +39,6 @@ import org.opensaml.profile.context.ProfileRequestContext;
 import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
 import org.slf4j.Logger;
 
-import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.nimbusds.jwt.SignedJWT;
 import com.nimbusds.oauth2.sdk.id.ClientID;
@@ -55,10 +54,9 @@ import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.configuration.Ent
 import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.resolver.ResolveEntityCacheContainerIdentifier;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.resolver.ResolveEntityCacheIdentifierCriterion;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.resolver.ResolveEntityResponseContainer;
-import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.impl.EntityConfigurationImpl;
-import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.impl.SubordinateStatementImpl;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.payload.ResolveEntityResponsePayload;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.payload.claim.Metadata;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.util.EntityStatementHelper;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.TrustedRemoteResolverEntity;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.context.RelyingPartyTrustChainContext;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.context.VerifiedTrustChain;
@@ -72,7 +70,6 @@ import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
 import net.shibboleth.shared.collection.CollectionSupport;
 import net.shibboleth.shared.component.ComponentInitializationException;
 import net.shibboleth.shared.logic.Constraint;
-import net.shibboleth.shared.logic.ConstraintViolationException;
 import net.shibboleth.shared.logic.FunctionSupport;
 import net.shibboleth.shared.logic.PredicateSupport;
 import net.shibboleth.shared.primitive.LoggerFactory;
@@ -438,27 +435,13 @@ public class CallResolveEntityApi extends AbstractProfileAction {
                             trustedEntity);
                     continue;
                 }
-                final List<EntityStatement<?>> chain = new ArrayList<>();
-                rawTrustChain.stream()
-                        .filter(Objects::nonNull)
-                        .forEach(entry -> {
-                            try {
-                                final SignedJWT jwt = SignedJWT.parse(entry);
-                                assert jwt != null;
-                                assert objectMapper != null;
-                                final String subject = jwt.getJWTClaimsSet().getSubject();
-                                final String issuer = jwt.getJWTClaimsSet().getIssuer();
-                                if (subject != null && subject.equals(issuer)) {
-                                    assert objectMapper != null;
-                                    chain.add(EntityConfigurationImpl.parse(jwt, objectMapper));
-                                } else {
-                                    assert objectMapper != null;
-                                    chain.add(SubordinateStatementImpl.parse(jwt, objectMapper));
-                                }
-                            } catch (final JsonProcessingException | ParseException | ConstraintViolationException e) {
-                                log.warn("Could not parse the trust chain into list of entity statements", e);
-                            }
-                        });
+                assert objectMapper != null;
+                final List<EntityStatement<?>> chain =
+                        EntityStatementHelper.deserializeTrustChain(rawTrustChain, objectMapper);
+                if (chain == null) {
+                    log.warn("Could not parse the trust chain into list of entity statements");
+                    continue;
+                }
                 if (!preSelectedChain.isEmpty() && !preSelectedChain.equals(trustChainIDsLookupStrategy.apply(chain))) {
                     log.debug("{} Ignored resolved trust chain that doesn't match with preselected chain",
                             getLogPrefix());
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/DefaultProvidedTrustChainResolver.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/DefaultProvidedTrustChainResolver.java
new file mode 100644
index 0000000..c38952e
--- /dev/null
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/DefaultProvidedTrustChainResolver.java
@@ -0,0 +1,116 @@
+/*
+ * 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.plugin.oidc.op.oidfed.profile.impl;
+
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.EntityStatement;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.ProvidedTrustChainResolver;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.ThreadSafeAfterInit;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.resolver.ResolverException;
+
+/**
+ * Resolver for provided trust chains that exploits a map of profile-specific resolvers.
+ */
+ at ThreadSafeAfterInit
+public class DefaultProvidedTrustChainResolver extends AbstractIdentifiableInitializableComponent
+    implements ProvidedTrustChainResolver {
+
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(DefaultProvidedTrustChainResolver.class);
+
+    /** Strategy to locate the profile ID. */
+    @NonnullAfterInit private Function<ProfileRequestContext, String> profileIdLookupStrategy;
+
+    /** Strategies used to locate profile-specific lookup strategies. */
+    @Nonnull private Map<String,ProvidedTrustChainResolver> profileSpecificResolvers;
+
+    /**
+     * Constructor.
+     */
+    public DefaultProvidedTrustChainResolver() {
+        profileSpecificResolvers = CollectionSupport.emptyMap();
+    }
+
+    /**
+     * Set strategy to locate the profile ID. 
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setProfileIdLookupStrategy(@Nonnull final Function<ProfileRequestContext, String> strategy) {
+        checkSetterPreconditions();
+        profileIdLookupStrategy = Constraint.isNotNull(strategy, "ProfileIdLookupStrategy cannot be null");
+    }
+
+    /**
+     * Set map used to locate profile-specific resolvers.
+     * 
+     * @param resolvers map of resolvers
+     */
+    public void setProfileSpecificResolvers(@Nonnull final Map<String,ProvidedTrustChainResolver> resolvers) {
+        checkSetterPreconditions();
+        profileSpecificResolvers = Constraint.isNotNull(resolvers, "ProfileSpecificResolvers cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+
+        if (profileIdLookupStrategy == null) {
+            throw new ComponentInitializationException("ProfileIdLookupStrategy cannot be null");
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override @Nonnull
+    public Iterable<List<EntityStatement<?>>> resolve(@Nullable final ProfileRequestContext profileRequestContext)
+            throws ResolverException {
+        final List<EntityStatement<?>> result = resolveSingle(profileRequestContext);
+        return result == null ? CollectionSupport.emptyList() : CollectionSupport.listOf(result);
+    }
+
+    /** {@inheritDoc} */
+    @Override @Nullable
+    public List<EntityStatement<?>> resolveSingle(@Nullable final ProfileRequestContext profileRequestContext)
+            throws ResolverException {
+        checkComponentActive();
+        final String profileId = profileIdLookupStrategy.apply(profileRequestContext);
+        if (profileId == null) {
+            throw new ResolverException("Could not resolve profile ID via lookup strategy");
+        }
+        final ProvidedTrustChainResolver resolver = profileSpecificResolvers.get(profileId);
+        if (resolver == null) {
+            log.debug("No resolver found for profile {}", profileId);
+            return null;
+        }
+        return resolver.resolveSingle(profileRequestContext);
+    }
+
+}
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/OidFederationEventIds.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/OidFederationEventIds.java
index d8c4c01..82837a4 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/OidFederationEventIds.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/OidFederationEventIds.java
@@ -75,4 +75,16 @@ public class OidFederationEventIds {
      */
     @Nonnull @NotEmpty public static final String INVALID_TRUST_CHAIN_AGAINST_CONSTRAINTS =
             "InvalidTrustChainAgainstConstraints";
+
+    /**
+     * ID of event returned if the mandatory provided trust chain could not be fetched.
+     */
+    @Nonnull @NotEmpty public static final String MISSING_MANDATORY_PROVIDED_TRUST_CHAIN =
+            "MissingMandatoryProvidedTrustChain";
+
+    /**
+     * ID of event returned if the provided trust chain could not be verified.
+     */
+    @Nonnull @NotEmpty public static final String INVALID_PROVIDED_TRUST_CHAIN = "InvalidProvidedTrustChain";
+
 }
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ValidateAutomaticRegistrationProfileConfiguration.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ValidateAutomaticRegistrationProfileConfiguration.java
index ce6d5ad..758f368 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ValidateAutomaticRegistrationProfileConfiguration.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ValidateAutomaticRegistrationProfileConfiguration.java
@@ -35,8 +35,11 @@ import com.nimbusds.openid.connect.sdk.rp.OIDCClientMetadata;
 
 import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.EntityStatement;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.payload.claim.impl.MetadataImpl;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.config.navigate.ClaimsSetDecorationTypeLookupFunction;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.config.navigate.LocalMetadataPolicyLookupFunction;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.config.navigate.MandatoryTrustMarksLookupFunction;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.config.navigate.ProvidedChainClaimsSetDecorationTypeLookupFunction;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.context.AutomaticRegistrationContext;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.context.RelyingPartyTrustChainContext;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.context.VerifiedTrustChain;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.navigate.DefaultSelectedTrustChainMetadataLookupStrategy;
@@ -73,6 +76,10 @@ public class ValidateAutomaticRegistrationProfileConfiguration extends AbstractP
     /** Strategy used to lookup the trust chain context. */
     @Nonnull private Function<ProfileRequestContext, RelyingPartyTrustChainContext> trustChainContextLookupStrategy;
 
+    /** Strategy used to create the automatic registration context. */
+    @Nonnull
+    private Function<ProfileRequestContext, AutomaticRegistrationContext> automaticRegistrationContextCreationStrategy;
+
     /** Strategy used to lookup the selected metadata. */
     @Nonnull private Function<ProfileRequestContext, OIDCClientMetadata> selectedMetadataLookupStrategy;
 
@@ -86,6 +93,12 @@ public class ValidateAutomaticRegistrationProfileConfiguration extends AbstractP
     @NonnullAfterInit private BiFunction<OIDCClientInformation,Map<String, MetadataPolicy>,OIDCClientInformation>
         localMetadataPolicyMergingStrategy;
 
+    /** Strategy used to lookup claims set decoration type. */
+    @Nonnull private Function<ProfileRequestContext, String> claimsSetDecorationTypeLookupStrategy;
+
+    /** Strategy used to lookup claims set decoration type for clients with request-provided trust chain. */
+    @Nonnull private Function<ProfileRequestContext, String> providedChainClaimsSetDecorationTypeLookupStrategy;
+
     /** Trust chain context to operate on. */
     @NonnullBeforeExec private RelyingPartyTrustChainContext trustChainContext;
 
@@ -101,9 +114,16 @@ public class ValidateAutomaticRegistrationProfileConfiguration extends AbstractP
                         new InboundMessageContextLookup());
         assert tcls != null;
         trustChainContextLookupStrategy = tcls;
+        final Function<ProfileRequestContext, AutomaticRegistrationContext> arccs =
+                new ChildContextLookup<>(AutomaticRegistrationContext.class, true).compose(
+                        new InboundMessageContextLookup());
+        assert arccs != null;
+        automaticRegistrationContextCreationStrategy = arccs;
         selectedMetadataLookupStrategy = new DefaultSelectedTrustChainMetadataLookupStrategy();
         mandatoryTrustMarksLookupStrategy = new MandatoryTrustMarksLookupFunction();
         localMetadataPolicyLookupStrategy = new LocalMetadataPolicyLookupFunction();
+        claimsSetDecorationTypeLookupStrategy = new ClaimsSetDecorationTypeLookupFunction();
+        providedChainClaimsSetDecorationTypeLookupStrategy = new ProvidedChainClaimsSetDecorationTypeLookupFunction();
     }
 
     /**
@@ -118,6 +138,18 @@ public class ValidateAutomaticRegistrationProfileConfiguration extends AbstractP
                 Constraint.isNotNull(strategy, "TrustChainContextLookupStrategy cannot be null");
     }
 
+    /**
+     * Set the strategy used to create automatic registration context.
+     * 
+     * @param strategy creation strategy
+     */
+    public void setAutomaticRegistrationContextCreationStrategy(
+            @Nonnull final Function<ProfileRequestContext, AutomaticRegistrationContext> strategy) {
+        checkSetterPreconditions();
+        automaticRegistrationContextCreationStrategy =
+                Constraint.isNotNull(strategy, "AutomaticRegistrationContextCreationStrategy cannot be null");
+    }
+
     /**
      * Set the strategy used to lookup the selected metadata.
      * 
@@ -166,6 +198,30 @@ public class ValidateAutomaticRegistrationProfileConfiguration extends AbstractP
                 Constraint.isNotNull(strategy, "Local metadata policy merging strategy cannot be null");
     }
 
+    /**
+     * Set the strategy used to lookup claims set decoration type.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setClaimsSetDecorationTypeLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext, String> strategy) {
+        checkSetterPreconditions();
+        claimsSetDecorationTypeLookupStrategy =
+                Constraint.isNotNull(strategy, "Claims set decoration type lookup strategy cannot be null");
+    }
+
+    /**
+     * Set the strategy used to lookup claims set decoration type for clients with request-provided trust chain.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setProvidedChainClaimsSetDecorationTypeLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext, String> strategy) {
+        checkSetterPreconditions();
+        providedChainClaimsSetDecorationTypeLookupStrategy =
+                Constraint.isNotNull(strategy, "Claims set decoration type lookup strategy cannot be null");
+    }
+
     /** {@inheritDoc} */
     @Override
     protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
@@ -258,10 +314,27 @@ public class ValidateAutomaticRegistrationProfileConfiguration extends AbstractP
             clientInformation.getOIDCMetadata().setCustomField(
                     ClientInformationExtensionSupport.KEY_VALIDATED_TRUST_MARK_IDS, verifiedTrustMarks);
         }
-        log.debug("{} Client information after adding custom extension {}", getLogPrefix(), clientInformation.toJSONObject().toJSONString());
+        log.debug("{} Client information after adding custom extension {}", getLogPrefix(),
+                clientInformation.toJSONObject().toJSONString());
         oidcCtx.setClientInformation(clientInformation);
         profileRequestContext.ensureInboundMessageContext().addSubcontext(oidcCtx);
         log.debug("{} Client information attached to the OIDCMetadataContext", getLogPrefix());
+        final AutomaticRegistrationContext automaticRegistrationContext =
+                automaticRegistrationContextCreationStrategy.apply(profileRequestContext);
+        if (automaticRegistrationContext == null) {
+            log.error("{} Could not create automatic registration context", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
+            return;
+        }
+        final String decorationType;
+        if (trustChainContext.getProvidedTrustChain() != null) {
+            decorationType = providedChainClaimsSetDecorationTypeLookupStrategy.apply(profileRequestContext);
+            log.debug("{} Resolved provided chain claims set decoration type {}", getLogPrefix(), decorationType);
+        } else {
+            decorationType = claimsSetDecorationTypeLookupStrategy.apply(profileRequestContext);
+            log.debug("{} Resolved default claims set decoration type {}", getLogPrefix(), decorationType);
+        }
+        automaticRegistrationContext.setClaimsSetDecorationType(decorationType);
     }
 
     /**
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ValidateProvidedTrustChain.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ValidateProvidedTrustChain.java
index 7afe12b..51379a2 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ValidateProvidedTrustChain.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ValidateProvidedTrustChain.java
@@ -17,15 +17,17 @@ package net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.function.BiPredicate;
-import java.util.function.Function;
+import java.util.function.Predicate;
 
 import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
 
 import org.opensaml.profile.action.EventIds;
 import org.opensaml.profile.context.ProfileRequestContext;
 import org.slf4j.Logger;
 
 import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.EntityStatement;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.ProvidedTrustChainResolver;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.context.RelyingPartyTrustChainContext;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.context.VerifiedTrustChain;
 import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
@@ -34,6 +36,7 @@ import net.shibboleth.shared.collection.CollectionSupport;
 import net.shibboleth.shared.component.ComponentInitializationException;
 import net.shibboleth.shared.logic.Constraint;
 import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.resolver.ResolverException;
 
 import org.opensaml.profile.action.ActionSupport;
 
@@ -53,22 +56,28 @@ public class ValidateProvidedTrustChain extends AbstractTrustChainResolutionActi
     @NonnullAfterInit
     private BiPredicate<ProfileRequestContext, List<EntityStatement<?>>> providedTrustChainValidationStrategy;
 
-    /** Strategy used to locate the provided trust chain. */
-    @NonnullAfterInit
-    private Function<ProfileRequestContext, List<EntityStatement<?>>> providedTrustChainLookupStrategy;
+    /** Resolver used to fetch the provided trust chain. */
+    @NonnullAfterInit private ProvidedTrustChainResolver providedTrustChainResolver;
+
+    /** Condition used to define if provided trust chain is mandatory. */
+    @NonnullAfterInit private Predicate<ProfileRequestContext> mandatoryProvidedTrustChainCondition;
+
+    /** Condition used to define if provided trust chain is ignored. */
+    @NonnullAfterInit private Predicate<ProfileRequestContext> ignoreProvidedTrustChainCondition;
+
+    /** Event ID to publish if the provided trust chain was valid and populated into the context. */
+    @Nullable private String validProvidedTrustChainEventId = null;
 
     /** Trust chain to operate on. */
     @NonnullBeforeExec private List<EntityStatement<?>> trustChain;
 
     /**
-     * Set the strategy used to locate the provided trust chain.
+     * Set the resolver to fetch the provided trust chain.
      * 
-     * @param strategy lookup strategy
+     * @param resolver provided trust chain resolver
      */
-    public void setProvidedTrustChainLookupStrategy(
-            @Nonnull final Function<ProfileRequestContext, List<EntityStatement<?>>> strategy) {
-        providedTrustChainLookupStrategy =
-                Constraint.isNotNull(strategy, "ProvidedTrustChainLookupStrategy cannot be null");
+    public void setProvidedTrustChainResolver(@Nonnull final ProvidedTrustChainResolver resolver) {
+        providedTrustChainResolver = Constraint.isNotNull(resolver, "ProvidedTrustChainResolver cannot be null");
     }
 
     /**
@@ -83,17 +92,55 @@ public class ValidateProvidedTrustChain extends AbstractTrustChainResolutionActi
                 Constraint.isNotNull(strategy, "ProvidedTrustChainValidationStrategy cannot be null");
     }
 
+    /**
+     * Set the condition used to define if provided trust chain is mandatory.
+     * 
+     * @param condition mandatory condition
+     */
+    public void setMandatoryProvidedTrustChainCondition(@Nonnull final Predicate<ProfileRequestContext> condition) {
+        checkSetterPreconditions();
+        mandatoryProvidedTrustChainCondition =
+                Constraint.isNotNull(condition, "MandatoryProvidedTrustChainCondition cannot be null");
+    }
+
+    /**
+     * Set the condition used to define if provided trust chain is ignored.
+     * 
+     * @param condition ignoring condition
+     */
+    public void setIgnoreProvidedTrustChainCondition(@Nonnull final Predicate<ProfileRequestContext> condition) {
+        checkSetterPreconditions();
+        ignoreProvidedTrustChainCondition =
+                Constraint.isNotNull(condition, "IgnoreProvidedTrustChainCondition cannot be null");
+    }
+
+    /**
+     * Set the event ID to publish if the provided trust chain was valid and populated into the context.
+     * 
+     * @param eventId event ID to publish
+     */
+    public void setValidProvidedTrustChainEventId(@Nullable final String eventId) {
+        checkSetterPreconditions();
+        validProvidedTrustChainEventId = eventId;
+    }
+
     /** {@inheritDoc} */
     @Override
     protected void doInitialize() throws ComponentInitializationException {
         super.doInitialize();
 
-        if (providedTrustChainLookupStrategy == null) {
-            throw new ComponentInitializationException("ProvidedTrustChainLookupStrategy cannot be null");
+        if (providedTrustChainResolver == null) {
+            throw new ComponentInitializationException("ProvidedTrustChainResolver cannot be null");
         }
         if (providedTrustChainValidationStrategy == null) {
             throw new ComponentInitializationException("ProvidedTrustChainValidationStrategy cannot be null");
         }
+        if (mandatoryProvidedTrustChainCondition == null) {
+            throw new ComponentInitializationException("MandatoryProvidedTrustChainCondition cannot be null");
+        }
+        if (ignoreProvidedTrustChainCondition == null) {
+            throw new ComponentInitializationException("IgnoreProvidedTrustChainCondition cannot be null");
+        }
     }
 
     /** {@inheritDoc} */
@@ -102,10 +149,25 @@ public class ValidateProvidedTrustChain extends AbstractTrustChainResolutionActi
         if (!super.doPreExecute(profileRequestContext)) {
             return false;
         }
-        trustChain = providedTrustChainLookupStrategy.apply(profileRequestContext);
+        try {
+            trustChain = providedTrustChainResolver.resolveSingle(profileRequestContext);
+        } catch (final ResolverException e) {
+            log.warn("{} Could not parse the provided trust chain", getLogPrefix(), e);
+            ActionSupport.buildEvent(profileRequestContext, OidFederationEventIds.INVALID_PROVIDED_TRUST_CHAIN);
+            return false;
+        }
+        if (ignoreProvidedTrustChainCondition.test(profileRequestContext)) {
+            log.debug("{} Provided trust chains are configurd to be ignored, nothing to do", getLogPrefix());
+            return false;
+        }
         if (trustChain == null) {
-            log.error("{} Unable to fetch trust chain", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
+            if (mandatoryProvidedTrustChainCondition.test(profileRequestContext)) {
+                log.warn("{} Unable to fetch mandatory trust chain", getLogPrefix());
+                ActionSupport.buildEvent(profileRequestContext,
+                        OidFederationEventIds.MISSING_MANDATORY_PROVIDED_TRUST_CHAIN);
+                return false;
+            }
+            log.debug("{} Optional provided trust chain was not found, nothing to do", getLogPrefix());
             return false;
         }
         return true;
@@ -116,7 +178,7 @@ public class ValidateProvidedTrustChain extends AbstractTrustChainResolutionActi
     protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
         if (!providedTrustChainValidationStrategy.test(profileRequestContext, trustChain)) {
             log.warn("{} The trust chain validation failed", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
+            ActionSupport.buildEvent(profileRequestContext, OidFederationEventIds.INVALID_PROVIDED_TRUST_CHAIN);
             return;
         }
 
@@ -133,10 +195,22 @@ public class ValidateProvidedTrustChain extends AbstractTrustChainResolutionActi
             ActionSupport.buildEvent(profileRequestContext, errorEventId);
             return;
         }
+        if (policyCompliantChains.size() != 1) {
+            log.error("{} Unexpected result for the policy compliant trust chains: {}", getLogPrefix(),
+                    policyCompliantChains.size());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return;
+        }
 
         log.debug("{} Setting the policy compliant trust chains into the context: {}", getLogPrefix(),
                 policyCompliantChains);
         trustChainContext.setPolicyCompliantTrustChains(policyCompliantChains);
+        trustChainContext.setProvidedTrustChain(policyCompliantChains.get(0).getTrustChain());
+        if (validProvidedTrustChainEventId != null) {
+            ActionSupport.buildEvent(profileRequestContext, validProvidedTrustChainEventId);
+            log.debug("{} Published event {} as provided trust chain was valid", getLogPrefix(),
+                    validProvidedTrustChainEventId);
+        }
     }
 
 }
\ No newline at end of file
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/AbstractRequestObjectProvidedTrustChainResolver.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/AbstractRequestObjectProvidedTrustChainResolver.java
new file mode 100644
index 0000000..139b193
--- /dev/null
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/AbstractRequestObjectProvidedTrustChainResolver.java
@@ -0,0 +1,146 @@
+/*
+ * 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.plugin.oidc.op.oidfed.profile.navigate;
+
+import java.text.ParseException;
+import java.util.List;
+import java.util.Optional;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.SignedJWT;
+
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.EntityStatement;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.util.EntityStatementHelper;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.ProvidedTrustChainResolver;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.resolver.ResolverException;
+
+/**
+ * Abstract resolver for provided trust chains to be used with messages containing request object.
+ */
+public abstract class AbstractRequestObjectProvidedTrustChainResolver
+    extends AbstractIdentifiableInitializableComponent
+    implements ProvidedTrustChainResolver {
+
+    /** JSON object mapper used for decoding entity statement payload. */
+    @NonnullAfterInit protected ObjectMapper objectMapper;
+
+    /**
+     * Set the JSON {@link ObjectMapper} used for decoding entity statement payload.
+     * 
+     * @param mapper object mapper
+     */
+    public void setObjectMapper(@Nonnull final ObjectMapper mapper) {
+        checkSetterPreconditions();
+        objectMapper = Constraint.isNotNull(mapper, "Object mapper cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+
+        if (objectMapper == null) {
+            throw new ComponentInitializationException("ObjectMapper cannot be null");
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override @Nonnull
+    public Iterable<List<EntityStatement<?>>> resolve(@Nullable final ProfileRequestContext profileRequestContext)
+            throws ResolverException {
+        final List<EntityStatement<?>> result = resolveSingle(profileRequestContext);
+        return result == null ? CollectionSupport.emptyList() : CollectionSupport.listOf(result);
+    }
+
+    /** {@inheritDoc} */
+    @Override @Nullable
+    public List<EntityStatement<?>> resolveSingle(@Nullable final ProfileRequestContext profileRequestContext)
+            throws ResolverException {
+        checkComponentActive();
+        final SignedJWT requestObject = Optional.ofNullable(profileRequestContext)
+                .filter(prc -> prc.getInboundMessageContext() != null)
+                .map(prc -> prc.ensureInboundMessageContext())
+                .map(msgCtx -> msgCtx.getMessage())
+                .map(message -> fetchRequestObject(message))
+                .orElse(null);
+
+        if (requestObject == null) {
+            return null;
+        }
+        
+        final Object rawHeader = requestObject.getHeader().getCustomParam("trust_chain");
+        if (rawHeader != null) {
+            return parseList(rawHeader);
+        }
+        final JWTClaimsSet claimsSet;
+        try {
+            claimsSet = requestObject.getJWTClaimsSet();
+        } catch (final ParseException e) {
+            throw new ResolverException("Could not parse JWT payload");
+        }
+        final Object rawPayload = claimsSet.getClaim("trust_chain");
+        if (rawPayload != null) {
+            return parseList(rawPayload);
+        }
+        return null;
+    }
+
+    /**
+     * Parses request object from the inbound message.
+     * 
+     * @param message input containing request object
+     * @return request object or null if it could not be parsed
+     */
+    @Nullable protected abstract SignedJWT fetchRequestObject(@Nullable final Object message);
+
+    /**
+     * Parses the given input into a trust chain.
+     * 
+     * @param input raw object
+     * @return trust chain
+     * @throws ResolverException if the non-value input was not valid
+     */
+    @Nonnull private List<EntityStatement<?>> parseList(@Nonnull final Object input)
+            throws ResolverException {
+        if (input instanceof List<?> list) {
+            final List<String> strings = list.stream()
+                    .filter(String.class::isInstance)
+                    .map(String.class::cast)
+                    .toList();
+            if (strings.size() == list.size()) {
+                assert objectMapper != null;
+                final List<EntityStatement<?>> result =
+                        EntityStatementHelper.deserializeTrustChain(strings, objectMapper);
+                if (result != null) {
+                    return result;
+                }
+                throw new ResolverException("Unexpected contents in the trust_chain: invalid syntax");
+            }
+        }
+        throw new ResolverException("Unexpected contents in the trust_chain: not a list of strings");
+    }
+
+}
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/DefaultAuthorizationRequestObjectProvidedTrustChainResolver.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/DefaultAuthorizationRequestObjectProvidedTrustChainResolver.java
new file mode 100644
index 0000000..e3f3a50
--- /dev/null
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/DefaultAuthorizationRequestObjectProvidedTrustChainResolver.java
@@ -0,0 +1,43 @@
+/*
+ * 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.plugin.oidc.op.oidfed.profile.navigate;
+
+import java.util.Optional;
+
+import javax.annotation.Nullable;
+
+import com.nimbusds.jwt.SignedJWT;
+import com.nimbusds.oauth2.sdk.AuthorizationRequest;
+
+/**
+ * Default resolver for looking up the previously selected trust chain to be used in the authorization endpoint. The
+ * selected trust chain is fetched primarily via trust_chain header of the signed request object, secondarily via
+ * similarly named payload claim.
+ */
+public class DefaultAuthorizationRequestObjectProvidedTrustChainResolver extends AbstractRequestObjectProvidedTrustChainResolver {
+
+    /** {@inheritDoc} */
+    @Nullable protected SignedJWT fetchRequestObject(@Nullable final Object message) {
+        return Optional.ofNullable(message)
+                .filter(AuthorizationRequest.class::isInstance)
+                .map(AuthorizationRequest.class::cast)
+                .filter(request -> request.specifiesRequestObject())
+                .map(request -> request.getRequestObject())
+                .filter(SignedJWT.class::isInstance)
+                .map(SignedJWT.class::cast)
+                .orElse(null);
+    }
+
+}
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/DefaultPushedAuthorizationRequestObjectProvidedTrustChainResolver.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/DefaultPushedAuthorizationRequestObjectProvidedTrustChainResolver.java
new file mode 100644
index 0000000..10ec0e6
--- /dev/null
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/DefaultPushedAuthorizationRequestObjectProvidedTrustChainResolver.java
@@ -0,0 +1,45 @@
+/*
+ * 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.plugin.oidc.op.oidfed.profile.navigate;
+
+import java.util.Optional;
+
+import javax.annotation.Nullable;
+
+import com.nimbusds.jwt.SignedJWT;
+import com.nimbusds.oauth2.sdk.PushedAuthorizationRequest;
+
+/**
+ * Default resolver for looking up the previously selected trust chain to be used in the pushed authorization endpoint.
+ * The selected trust chain is fetched primarily via trust_chain header of the signed request object, secondarily via
+ * similarly named payload claim.
+ */
+public class DefaultPushedAuthorizationRequestObjectProvidedTrustChainResolver
+    extends AbstractRequestObjectProvidedTrustChainResolver {
+
+    /** {@inheritDoc} */
+    @Nullable protected SignedJWT fetchRequestObject(@Nullable final Object message) {
+        return Optional.ofNullable(message)
+                .filter(PushedAuthorizationRequest.class::isInstance)
+                .map(PushedAuthorizationRequest.class::cast)
+                .map(request -> request.getAuthorizationRequest())
+                .filter(authzRequest -> authzRequest.specifiesRequestObject())
+                .map(request -> request.getRequestObject())
+                .filter(SignedJWT.class::isInstance)
+                .map(SignedJWT.class::cast)
+                .orElse(null);
+    }
+
+}
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/ExplicitClientRegistrationRequestTrustChainLookupFunction.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/ExplicitClientRegistrationRequestTrustChainResolver.java
similarity index 55%
rename from idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/ExplicitClientRegistrationRequestTrustChainLookupFunction.java
rename to idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/ExplicitClientRegistrationRequestTrustChainResolver.java
index 7747d45..637f83e 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/ExplicitClientRegistrationRequestTrustChainLookupFunction.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/ExplicitClientRegistrationRequestTrustChainResolver.java
@@ -17,27 +17,39 @@ package net.shibboleth.idp.plugin.oidc.op.oidfed.profile.navigate;
 import java.util.List;
 import java.util.Optional;
 
+import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.ThreadSafe;
 
-import org.opensaml.messaging.context.navigate.ContextDataLookupFunction;
 import org.opensaml.profile.context.ProfileRequestContext;
 
 import net.shibboleth.idp.plugin.oidc.op.oidfed.messaging.impl.ExplicitClientRegistrationRequest;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.EntityStatement;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.ProvidedTrustChainResolver;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.resolver.ResolverException;
 
 /**
- * A function that returns pushed trust chain set in the explicit registration request.
+ * A resolver that returns pushed trust chain set in the explicit registration request.
  */
 @ThreadSafe
-public class ExplicitClientRegistrationRequestTrustChainLookupFunction
-    implements ContextDataLookupFunction<ProfileRequestContext, List<EntityStatement<?>>> {
+public class ExplicitClientRegistrationRequestTrustChainResolver
+    implements ProvidedTrustChainResolver {
 
     /** {@inheritDoc} */
-    @Nullable
-    public List<EntityStatement<?>> apply(@Nullable final ProfileRequestContext input) {
-        return Optional.ofNullable(input)
-                .map(profileRequestContext -> profileRequestContext.getInboundMessageContext())
+    @Override @Nonnull
+    public Iterable<List<EntityStatement<?>>> resolve(@Nullable final ProfileRequestContext profileRequestContext)
+            throws ResolverException {
+        final List<EntityStatement<?>> result = resolveSingle(profileRequestContext);
+        return result == null ? CollectionSupport.emptyList() : CollectionSupport.listOf(result);
+    }
+
+    /** {@inheritDoc} */
+    @Override @Nullable
+    public List<EntityStatement<?>> resolveSingle(@Nullable final ProfileRequestContext profileRequestContext)
+            throws ResolverException {
+        return Optional.ofNullable(profileRequestContext)
+                .map(profileContext -> profileContext.getInboundMessageContext())
                 .map(messageContext -> messageContext.getMessage())
                 .filter(ExplicitClientRegistrationRequest.class::isInstance)
                 .map(ExplicitClientRegistrationRequest.class::cast)
diff --git a/idp-oidfed-op-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml b/idp-oidfed-op-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
index 4d7606f..01e771e 100644
--- a/idp-oidfed-op-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
+++ b/idp-oidfed-op-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
@@ -136,7 +136,12 @@
                         </bean>
                         <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.DefaultEntityStatementContentValidationCondition"
                             p:claimsValidator-ref="%{idp.oidfed.entityConfiguration.claimsValidator:shibboleth.oidfed.DefaultEntityConfigurationClaimsValidator}"
-                            p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier"/>
+                            p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier">
+                            <property name="headerValidator">
+                                <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.DefaultTrustChainHeaderValidationCondition"
+                                    p:objectMapper-ref="shibboleth.oidfed.JWTPayloadJSONObjectMapper" />
+                            </property>
+                        </bean>
                     </util:list>
                 </property>
             </bean>
@@ -233,7 +238,12 @@
                         </bean>
                         <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.DefaultEntityStatementContentValidationCondition"
                             p:claimsValidator-ref="%{idp.oidfed.subordinateStatement.claimsValidator:shibboleth.oidfed.DefaultSubordinateStatementClaimsValidator}"
-                            p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier"/>
+                            p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier">
+                            <property name="headerValidator">
+                                <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.DefaultTrustChainHeaderValidationCondition"
+                                    p:objectMapper-ref="shibboleth.oidfed.JWTPayloadJSONObjectMapper" />
+                            </property>
+                        </bean>
                     </util:list>
                 </property>
             </bean>
@@ -405,7 +415,11 @@
                         </bean>
                         <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.DefaultEntityStatementContentValidationCondition"
                             p:claimsValidator-ref="%{idp.oidfed.remoteResolver.claimsValidator:shibboleth.oidfed.DefaultResolveEntityResponseClaimsValidator}"
-                            p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier"/>
+                            p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier">
+                            <property name="headerValidator">
+                                <bean parent="shibboleth.BiConditions.Expression" c:expression="true"/>
+                            </property>
+                        </bean>
                     </util:list>
                 </property>
             </bean>
@@ -534,7 +548,11 @@
                         </bean>
                         <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.DefaultEntityStatementContentValidationCondition"
                             p:claimsValidator-ref="%{idp.oidfed.trustMarkStatus.claimsValidator:shibboleth.oidfed.DefaultTrustMarkStatusClaimsValidator}"
-                            p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier"/>
+                            p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier">
+                            <property name="headerValidator">
+                                <bean parent="shibboleth.BiConditions.Expression" c:expression="true"/>
+                            </property>
+                        </bean>
                     </util:list>
                 </property>
             </bean>
@@ -621,7 +639,11 @@
                         </bean>
                         <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.DefaultEntityStatementContentValidationCondition"
                             p:claimsValidator-ref="%{idp.oidfed.trustMark.claimsValidator:shibboleth.oidfed.DefaultTrustMarkClaimsValidator}"
-                            p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier"/>
+                            p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier">
+                            <property name="headerValidator">
+                                <bean parent="shibboleth.BiConditions.Expression" c:expression="true"/>
+                            </property>
+                        </bean>
                     </util:list>
                 </property>
             </bean>
@@ -877,17 +899,56 @@
         </property>
     </bean>
 
+    <bean id="shibboleth.oidfed.DefaultProvidedTrustChainResolver"
+        class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.DefaultProvidedTrustChainResolver">
+        <property name="profileIdLookupStrategy">
+            <bean class="net.shibboleth.idp.plugin.oidc.op.profile.logic.DefaultMetadataLookupExtensionContextProfileIDLookupFunction"/>
+        </property>
+        <property name="profileSpecificResolvers">
+            <util:map value-type="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.ProvidedTrustChainResolver">
+                <entry key="#{T(net.shibboleth.oidc.profile.config.OIDCSSOProfileConfiguration).PROFILE_ID}">
+                    <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.navigate.DefaultAuthorizationRequestObjectProvidedTrustChainResolver"
+                        p:objectMapper-ref="shibboleth.oidfed.JWTPayloadJSONObjectMapper" />
+                </entry>
+                <entry key="#{T(net.shibboleth.oidc.profile.oauth2.config.OAuth2PushedAuthorizationRequestConfiguration).PROFILE_ID}">
+                    <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.navigate.DefaultPushedAuthorizationRequestObjectProvidedTrustChainResolver"
+                        p:objectMapper-ref="shibboleth.oidfed.JWTPayloadJSONObjectMapper" />
+                </entry>
+            </util:map>
+        </property>
+    </bean>
+
+    <util:map id="DefaultAutoRegisteredTokenClaimsSetDecorators" value-type="java.util.List">
+        <entry key="#{null}">
+            <util:list>
+                <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.AutoRegisteredTrustChainIDsClaimsSetDecorator"/>
+            </util:list>
+        </entry>
+        <entry key="store">
+            <util:list>
+                <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.AutoRegisteredTrustChainIDsClaimsSetDecorator"/>
+                <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.AutoRegisteredClientInformationStoreClaimsSetDecorator"
+                    p:clientInformationManager-ref="#{'%{idp.oidfed.autoreg.clientInformationManager:shibboleth.oidc.ClientInformationManager}'.trim()}" />
+            </util:list>
+        </entry>
+    </util:map>
+
     <bean id="AutoRegisteredTrustChainAuthorizationCodeClaimsSetDecorator"
-        class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.AutoRegisteredTrustChainAuthorizationCodeClaimsSetDecorator" />
+        class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.AutoRegisteredTrustChainAuthorizationCodeClaimsSetDecorator"
+        p:claimsSetDecorators-ref="DefaultAutoRegisteredTokenClaimsSetDecorators"/>
 
     <bean id="AutoRegisteredTrustChainAccessTokenClaimsSetDecorator"
-        class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.AutoRegisteredTrustChainAccessTokenClaimsSetDecorator" />
+        class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.AutoRegisteredTrustChainAccessTokenClaimsSetDecorator"
+        p:claimsSetDecorators-ref="DefaultAutoRegisteredTokenClaimsSetDecorators"/>
 
     <bean id="AutoRegisteredTrustChainRefreshTokenClaimsSetDecorator"
-        class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.AutoRegisteredTrustChainRefreshTokenClaimsSetDecorator" />
+        class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.AutoRegisteredTrustChainRefreshTokenClaimsSetDecorator"
+        p:claimsSetDecorators-ref="DefaultAutoRegisteredTokenClaimsSetDecorators"/>
+
 
     <bean id="AutoRegisteredTrustChainRequestUriClaimsSetDecorator"
-        class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.AutoRegisteredTrustChainRequestUriClaimsSetDecorator" />
+        class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.AutoRegisteredTrustChainRequestUriClaimsSetDecorator"
+        p:claimsSetDecorators-ref="DefaultAutoRegisteredTokenClaimsSetDecorators"/>
 
     <bean id="AutomaticRegistrationCondition"
         class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.logic.DefaultAutomaticRegistrationActivationCondition">
diff --git a/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup-ext/oidfed/metadata-lookup-ext-oidfed-beans.xml b/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup-ext/oidfed/metadata-lookup-ext-oidfed-beans.xml
index eff94cc..1419a94 100644
--- a/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup-ext/oidfed/metadata-lookup-ext-oidfed-beans.xml
+++ b/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup-ext/oidfed/metadata-lookup-ext-oidfed-beans.xml
@@ -14,6 +14,75 @@
         </property>
     </bean>
 
+    <bean id="ValidateProvidedTrustChain" class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.ValidateProvidedTrustChain"
+        scope="prototype"
+        p:metadataPolicyMergingStrategy-ref="#{'%{idp.oidfed.autoregister.TrustChainMetadataPolicyMergingStrategy:DefaultTrustChainMetadataPolicyMergingStrategy}'.trim()}"
+        p:metadataPolicyEnforcer-ref="#{'%{idp.oidfed.autoregister.MetadataPolicyEnforcer:DefaultMetadataPolicyEnforcer}'.trim()}"
+        p:arraysAsSpaceSeparatedList="%{idp.oidfed.policy.arraysAsSpaceSeparatedList:scope}"
+        p:metadataValidationCondition-ref="#{'%{idp.oidfed.MetadataValidationCondition:DefaultMetadataValidationCondition}'.trim()}"
+        p:federationPolicyConstraints-ref="%{idp.oidfed.FederationPolicyConstraints:shibboleth.oidfed.DefaultFederationPolicyConstraints}"
+        p:providedTrustChainResolver="#{getObject('shibboleth.oidfed.ProvidedTrustChainResolver') ?: getObject('shibboleth.oidfed.DefaultProvidedTrustChainResolver')}"
+        p:mandatoryProvidedTrustChainCondition-ref="shibboleth.Conditions.FALSE"
+        p:ignoreProvidedTrustChainCondition-ref="shibboleth.Conditions.FALSE"
+        p:validProvidedTrustChainEventId="ValidProvidedTrustChain">
+        <property name="metadataLookupStrategy">
+            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.navigate.DefaultCombinedMetadataFromTrustChainLookupStrategy" />
+        </property>
+        <property name="providedTrustChainValidationStrategy">
+            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.logic.DefaultProvidedTrustChainValidationStrategy"
+                p:metadataCache-ref="#{'%{idp.oidfed.autoregister.ProvidedTrustChainMetadataCache:DefaultAutoRegisterFetchThroughProvidedTrustChainMetadataCache}'.trim()}" />
+        </property>
+    </bean>
+
+    <bean id="DefaultAutoRegisterFetchThroughProvidedTrustChainMetadataCache" parent="shibboleth.oidc.CacheBuilder">
+        <constructor-arg>
+            <bean p:cacheId="DefaultAutoRegisterFetchThroughProvidedTrustChainMetadataCache" parent="FetchThroughProvidedTrustChainMetadataCacheBuilderSpec"/>
+        </constructor-arg>
+    </bean>
+
+    <bean id="FetchThroughProvidedTrustChainMetadataCacheBuilderSpec"
+        class="net.shibboleth.oidc.metadata.cache.impl.FetchThroughMetadataCacheBuilderSpec">
+        <property name="criteriaToIdentifierStrategy">
+            <bean parent="shibboleth.Functions.Expression" c:expression="#input?.get(T(net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.trustchain.ProvidedTrustChainContainerCriterion))?.getValue()?.getEntityId()" />
+        </property>
+        <property name="identifierExtractionStrategy">
+            <bean parent="shibboleth.Functions.Expression" c:expression="#input?.getEntityId()"/>
+        </property>
+        <property name="fetchStrategy">
+            <bean parent="shibboleth.Functions.Expression" c:expression="#input?.get(T(net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.trustchain.ProvidedTrustChainContainerCriterion))?.getValue()" />
+        </property>
+        <property name="metadataFilterStrategy">
+            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.trustchain.DefaultTrustChainSignatureValidationFilterStrategy"
+                p:customFilterStrategies="#{getObject('%{idp.oidfed.cache.providedTrustChain.customFilterStrategies:}'.trim())}">
+                <property name="trustEngine">
+                    <bean class="net.shibboleth.oidc.security.impl.ExplicitKeySignedJWTTrustEngine">
+                        <constructor-arg index="0">
+                            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.security.credential.DefaultPayloadSignatureValidationCredentialResolver" />
+                        </constructor-arg>
+                        <constructor-arg index="1">
+                            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.security.credential.DefaultPayloadJOSEObjectCredentialResolver" />
+                        </constructor-arg>
+                    </bean>
+                </property>
+                <property name="entityStatementSignatureValidationFilterStrategy">
+                    <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.local.DefaultEntityStatementSignatureValidationFilterStrategy">
+                        <property name="trustEngine">
+                            <bean class="net.shibboleth.oidc.security.impl.ExplicitKeySignedJWTTrustEngine">
+                                <constructor-arg index="0">
+                                    <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.security.credential.DefaultLocalTrustAnchorCredentialResolver"
+                                        c:cache-ref="shibboleth.oidfed.LocalTrustAnchorsMetadataCache" />
+                                 </constructor-arg>
+                                 <constructor-arg index="1">
+                                     <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.security.credential.DefaultPayloadJOSEObjectCredentialResolver" />
+                                 </constructor-arg>
+                            </bean>
+                        </property>
+                    </bean>
+                </property>
+            </bean>
+        </property>
+    </bean>
+
     <bean id="TrustChainCandidatesExist" parent="shibboleth.Conditions.Expression"
         c:expression="#input.ensureInboundMessageContext().containsSubcontext(T(net.shibboleth.idp.plugin.oidc.op.oidfed.profile.context.RelyingPartyTrustChainContext)) and #input.ensureInboundMessageContext().ensureSubcontext(T(net.shibboleth.idp.plugin.oidc.op.oidfed.profile.context.RelyingPartyTrustChainContext)).getPolicyCompliantTrustChains() != null and #input.ensureInboundMessageContext().ensureSubcontext(T(net.shibboleth.idp.plugin.oidc.op.oidfed.profile.context.RelyingPartyTrust [...]
 
@@ -135,6 +204,14 @@
             <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.config.navigate.MandatoryTrustMarksLookupFunction"
                 p:relyingPartyContextLookupStrategy-ref="AutomaticRegistrationRelyingPartyCreationStrategy"/>
         </property>
+        <property name="claimsSetDecorationTypeLookupStrategy">
+            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.config.navigate.ClaimsSetDecorationTypeLookupFunction"
+                p:relyingPartyContextLookupStrategy-ref="AutomaticRegistrationRelyingPartyCreationStrategy"/>
+        </property>
+        <property name="providedChainClaimsSetDecorationTypeLookupStrategy">
+            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.config.navigate.ProvidedChainClaimsSetDecorationTypeLookupFunction"
+                p:relyingPartyContextLookupStrategy-ref="AutomaticRegistrationRelyingPartyCreationStrategy"/>
+        </property>
     </bean>
 
 </beans>
diff --git a/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup-ext/oidfed/metadata-lookup-ext-oidfed-flow.xml b/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup-ext/oidfed/metadata-lookup-ext-oidfed-flow.xml
index 44c78c8..60ecb5d 100644
--- a/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup-ext/oidfed/metadata-lookup-ext-oidfed-flow.xml
+++ b/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup-ext/oidfed/metadata-lookup-ext-oidfed-flow.xml
@@ -7,9 +7,18 @@
             <set name="conversationScope.automaticallyRegistered" value="false" />
         </on-entry>
         <if test="AutomaticRegistrationCondition.test(opensamlProfileRequestContext) and !opensamlProfileRequestContext.ensureInboundMessageContext().containsSubcontext(T(net.shibboleth.oidc.metadata.context.OIDCMetadataContext))"
-            then="ChooseResolutionMethod" else="proceed" />
+            then="CheckProvidedTrustChain" else="proceed" />
     </decision-state>
 
+    <action-state id="CheckProvidedTrustChain">
+        <evaluate expression="ValidateProvidedTrustChain" />
+        <evaluate expression="'proceed'" />
+        <transition on="proceed" to="ChooseResolutionMethod"/>
+        <transition on="ValidProvidedTrustChain" to="CheckIfDoAutomaticRegistration">
+            <set name="flowScope.transitionForReselectTrustChain" value="'DoAutomaticRegistration'" />
+        </transition>
+    </action-state>
+
     <decision-state id="ChooseResolutionMethod">
         <if test="UseResolverApiCondition.test(opensamlProfileRequestContext)"
             then="CallResolveEntityApi" else="ResolveTrustChains" />
@@ -61,6 +70,7 @@
     <end-state id="InvalidMetadataAgainstPolicy"/>
     <end-state id="NoTrustChainsResolved" />
     <end-state id="InvalidTrustChainAgainstConstraints" />
+    <end-state id="InvalidProvidedTrustChain" />
     <end-state id="HandleError"/>
 
     <global-transitions>
@@ -68,6 +78,7 @@
         <transition on="InvalidMetadataAgainstPolicy" to="InvalidMetadataAgainstPolicy" />
         <transition on="NoTrustChainsResolved" to="NoTrustChainsResolved" />
         <transition on="InvalidTrustChainAgainstConstraints" to="InvalidTrustChainAgainstConstraints" />
+        <transition on="InvalidProvidedTrustChain" to="InvalidProvidedTrustChain" />
         <transition on="HandleError" to="HandleError" />
     </global-transitions>
 
diff --git a/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/register/register-beans.xml b/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/register/register-beans.xml
index 7878418..79cdd84 100644
--- a/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/register/register-beans.xml
+++ b/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/register/register-beans.xml
@@ -44,19 +44,21 @@
         p:arraysAsSpaceSeparatedList="%{idp.oidfed.policy.arraysAsSpaceSeparatedList:scope}"
         p:metadataLookupStrategy-ref="DefaultCombinedMetadataFromTrustChainLookupStrategy"
         p:metadataValidationCondition-ref="#{'%{idp.oidfed.MetadataValidationCondition:DefaultMetadataValidationCondition}'.trim()}"
-        p:federationPolicyConstraints-ref="%{idp.oidfed.FederationPolicyConstraints:shibboleth.oidfed.DefaultFederationPolicyConstraints}">
+        p:federationPolicyConstraints-ref="%{idp.oidfed.FederationPolicyConstraints:shibboleth.oidfed.DefaultFederationPolicyConstraints}"
+        p:mandatoryProvidedTrustChainCondition-ref="shibboleth.Conditions.TRUE"
+        p:ignoreProvidedTrustChainCondition-ref="shibboleth.Conditions.FALSE">
         <property name="providedTrustChainValidationStrategy">
             <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.logic.DefaultProvidedTrustChainValidationStrategy"
-                p:metadataCache-ref="#{'%{idp.oidfed.register.ProvidedTrustChainMetadataCache:FetchThroughProvidedTrustChainMetadataCache}'.trim()}" />
+                p:metadataCache-ref="#{'%{idp.oidfed.register.ProvidedTrustChainMetadataCache:DefaultFetchThroughProvidedTrustChainMetadataCache}'.trim()}" />
         </property>
-        <property name="providedTrustChainLookupStrategy">
-            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.navigate.ExplicitClientRegistrationRequestTrustChainLookupFunction" />
+        <property name="providedTrustChainResolver">
+            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.navigate.ExplicitClientRegistrationRequestTrustChainResolver" />
         </property>
     </bean>
 
-    <bean id="FetchThroughProvidedTrustChainMetadataCache" parent="shibboleth.oidc.CacheBuilder">
+    <bean id="DefaultFetchThroughProvidedTrustChainMetadataCache" parent="shibboleth.oidc.CacheBuilder">
         <constructor-arg>
-            <bean p:cacheId="FetchThroughProvidedTrustChainMetadataCache" parent="FetchThroughProvidedTrustChainMetadataCacheBuilderSpec"/>
+            <bean p:cacheId="DefaultFetchThroughProvidedTrustChainMetadataCache" parent="FetchThroughProvidedTrustChainMetadataCacheBuilderSpec"/>
         </constructor-arg>
     </bean>
 
@@ -145,7 +147,12 @@
                         </bean>
                         <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.DefaultEntityStatementContentValidationCondition"
                             p:claimsValidator-ref="%{idp.oidfed.cache.explicitRegistrationRequest.claimsValidator:shibboleth.oidfed.DefaultExplicitRegistrationRequestClaimsValidator}"
-                            p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier"/>
+                            p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier">
+                            <property name="headerValidator">
+                                <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.DefaultTrustChainHeaderValidationCondition"
+                                    p:objectMapper-ref="shibboleth.oidfed.JWTPayloadJSONObjectMapper" />
+                            </property>
+                        </bean>
                     </util:list>
                 </property>
             </bean>
diff --git a/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml b/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml
index 70a942e..abc34ff 100644
--- a/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml
+++ b/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml
@@ -20,7 +20,9 @@
 
     <bean id="OIDFED.AutomaticRegistration" parent="AbstractOIDFederationProfile" lazy-init="true"
         class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.config.impl.DefaultOIDFederationAutomaticRegistrationProfileConfiguration"
-        p:mandatoryTrustMarks="%{idp.oidfed.automaticRegistration.mandatoryTrustMarks:}">
+        p:mandatoryTrustMarks="%{idp.oidfed.automaticRegistration.mandatoryTrustMarks:}"
+        p:defaultClaimsSetDecorationType="%{idp.oidfed.automaticRegistration.defaultClaimsSetDecorationType:}"
+        p:providedChainClaimsSetDecorationType="%{idp.oidfed.automaticRegistration.providedClaimsSetDecorationType:store}">
         <property name="localMetadataPolicyLookupStrategy">
             <bean parent="shibboleth.Functions.Constant">
                 <constructor-arg name="target">
diff --git a/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oidfed/TrustChainTestUtil.java b/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oidfed/TrustChainTestUtil.java
index 14cbafc..518529b 100644
--- a/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oidfed/TrustChainTestUtil.java
+++ b/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oidfed/TrustChainTestUtil.java
@@ -140,12 +140,14 @@ public class TrustChainTestUtil {
 
     public static SignedJWT signedJwt(final JWSAlgorithm algorithm, final JWK jwk, final String type,
             final JWTClaimsSet claimsSet) {
-        final SignedJWT signedJwt = new SignedJWT(
-                new JWSHeader.Builder(algorithm)
-                    .type(new JOSEObjectType(type))
-                    .keyID(jwk.getKeyID())
-                    .build(),
+        return signedJwt(algorithm, jwk,
+                new JWSHeader.Builder(algorithm).type(new JOSEObjectType(type)).keyID(jwk.getKeyID()).build(),
                 claimsSet);
+    }
+
+    public static SignedJWT signedJwt(final JWSAlgorithm algorithm, final JWK jwk, final JWSHeader header,
+            final JWTClaimsSet claimsSet) {
+        final SignedJWT signedJwt = new SignedJWT(header, claimsSet);
         try {
             if (JWSAlgorithm.Family.RSA.contains(algorithm)) {
                 signedJwt.sign(new RSASSASigner(jwk.toRSAKey()));
@@ -158,4 +160,5 @@ public class TrustChainTestUtil {
         }
         return null;
     }
+
 }
diff --git a/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/AbstractFederationFlowTest.java b/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/AbstractFederationFlowTest.java
index bfd6a2b..9b26b9f 100644
--- a/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/AbstractFederationFlowTest.java
+++ b/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/AbstractFederationFlowTest.java
@@ -171,14 +171,17 @@ public class AbstractFederationFlowTest extends AbstractOidcFlowTest {
     }
 
     protected JWT signedRequestObject(final Map<String,Object> claims, final JWK jwk) {
+        return signedRequestObject(claims, new JWSHeader.Builder(JWSAlgorithm.RS256).keyID(jwk.getKeyID()).build(),
+                jwk);
+    }
+
+    protected JWT signedRequestObject(final Map<String,Object> claims, final JWSHeader header, final JWK jwk) {
         final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder();
         for (final String claim : claims.keySet()) {
             builder.claim(claim, claims.get(claim));
         }
         try {
-            final SignedJWT jwt =
-                    new SignedJWT(new JWSHeader.Builder(JWSAlgorithm.RS256).keyID(jwk.getKeyID()).build(),
-                            builder.build());
+            final SignedJWT jwt = new SignedJWT(header, builder.build());
             final RSASSASigner signer = new RSASSASigner(jwk.toRSAKey());
             jwt.sign(signer);
             return jwt;
diff --git a/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/AuthorizeFlowAutomaticRegistrationTest.java b/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/AuthorizeFlowAutomaticRegistrationTest.java
index 456e24a..762f251 100644
--- a/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/AuthorizeFlowAutomaticRegistrationTest.java
+++ b/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/AuthorizeFlowAutomaticRegistrationTest.java
@@ -40,6 +40,9 @@ import org.testng.annotations.Test;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JOSEObjectType;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.JWSHeader;
 import com.nimbusds.jose.jwk.JWKSet;
 import com.nimbusds.jwt.JWT;
 import com.nimbusds.jwt.JWTClaimsSet;
@@ -459,6 +462,71 @@ public class AuthorizeFlowAutomaticRegistrationTest extends AbstractFederationFl
         assertErrorCode(result, "invalid_request_object");
     }
 
+    @Test
+    public void testWithPushedValidTrustChain_signedRequestObject()
+            throws IOException, UnsupportedOperationException, URISyntaxException {
+        final String clientId = uniqueClientId();
+
+        final List<String> trustChain = List.of(
+                rpEntityConfiguration(clientId),
+                subordinateStatement(clientId, Map.of("openid_relying_party",
+                        new OIDCClientMetadata().toJSONObject())),
+                trustedAnchorConfiguration());
+        final JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.RS256)
+                .type(new JOSEObjectType("entity-statement+jwt"))
+                .keyID(rpKey.getKeyID())
+                .customParam("trust_chain", trustChain)
+                .build();
+
+        final FlowExecutionResult result =
+                launchAuthenticationRequest(clientId, "openid profile", signedRequestObject(Map.of(
+                        "iss", clientId,
+                        "client_id", clientId,
+                        "aud", issuer,
+                        "exp", Instant.now().plus(Duration.ofMinutes(5)).toEpochMilli(),
+                        "jti", UUID.randomUUID(),
+                        "response_type", "code",
+                        "scope", "openid profile",
+                        "redirect_uri", redirectUri), header, rpKey));
+        final AuthenticationResponse responseMessage = parseSuccessResponse(result, AuthenticationResponse.class);
+        final AuthenticationSuccessResponse successResponse = responseMessage.toSuccessResponse();
+        Assert.assertEquals(successResponse.getRedirectionURI().toString(), redirectUri);
+        Assert.assertNull(successResponse.getIDToken());
+        Assert.assertNull(successResponse.getAccessToken());
+        Assert.assertNotNull(successResponse.getAuthorizationCode());
+        Assert.assertEquals(unwrapTrustChainFromAuthorizeCode(successResponse), List.of(clientId, anchorId));
+    }
+
+    @Test
+    public void testWithPushedInvalidTrustChain_signedRequestObject()
+            throws IOException, UnsupportedOperationException, URISyntaxException {
+        final String clientId = uniqueClientId();
+
+        final List<String> trustChain = List.of(
+                "invalid" + rpEntityConfiguration(clientId),
+                subordinateStatement(clientId, Map.of("openid_relying_party",
+                        new OIDCClientMetadata().toJSONObject())),
+                trustedAnchorConfiguration());
+        final JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.RS256)
+                .type(new JOSEObjectType("entity-statement+jwt"))
+                .keyID(rpKey.getKeyID())
+                .customParam("trust_chain", trustChain)
+                .build();
+
+        final FlowExecutionResult result =
+                launchAuthenticationRequest(clientId, "openid profile", signedRequestObject(Map.of(
+                        "iss", clientId,
+                        "client_id", clientId,
+                        "aud", issuer,
+                        "exp", Instant.now().plus(Duration.ofMinutes(5)).toEpochMilli(),
+                        "jti", UUID.randomUUID(),
+                        "response_type", "code",
+                        "scope", "openid profile",
+                        "redirect_uri", redirectUri), header, rpKey));
+        Assert.assertEquals(result.getOutcome().getId(), "ErrorView");
+    }
+
+    
     @Test
     public void testWithValidTrustChain_leafKeySignedRequestObject()
             throws IOException, UnsupportedOperationException, URISyntaxException {
diff --git a/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/PushedAuthorizeFlowAutomaticRegistrationTest.java b/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/PushedAuthorizeFlowAutomaticRegistrationTest.java
index d9b8b8f..206f314 100644
--- a/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/PushedAuthorizeFlowAutomaticRegistrationTest.java
+++ b/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/PushedAuthorizeFlowAutomaticRegistrationTest.java
@@ -41,7 +41,9 @@ import org.testng.annotations.Test;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JOSEObjectType;
 import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.JWSHeader;
 import com.nimbusds.jose.jwk.JWKSet;
 import com.nimbusds.jwt.JWT;
 import com.nimbusds.jwt.JWTClaimsSet;
@@ -360,6 +362,79 @@ public class PushedAuthorizeFlowAutomaticRegistrationTest extends AbstractFedera
         assertErrorCode(result, OAuth2Error.UNAUTHORIZED_CLIENT_CODE);
     }
 
+    @Test
+    public void testWithPublicClientWithSignedRequestObject_providedTrustChain() throws Exception {
+        final OIDCClientMetadata metadata = new OIDCClientMetadata();
+        metadata.setRedirectionURI(new URI(redirectUri));
+        metadata.setResponseTypes(Set.of(ResponseType.CODE));
+        metadata.setJWKSet(new JWKSet(rpKey.toPublicJWK()));
+        metadata.setTokenEndpointAuthMethod(ClientAuthenticationMethod.NONE);
+        final String clientId = uniqueClientId();
+        
+        final List<String> trustChain = List.of(
+                rpEntityConfiguration(clientId, metadata),
+                subordinateStatement(clientId, Map.of("openid_relying_party",
+                        new OIDCClientMetadata().toJSONObject())),
+                trustedAnchorConfiguration());
+
+        final JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.RS256)
+                .type(new JOSEObjectType("entity-statement+jwt"))
+                .keyID(rpKey.getKeyID())
+                .customParam("trust_chain", trustChain)
+                .build();
+
+        setHttpFormRequest("POST", createRequestParameters(clientId, "openid", "code", signedRequestObject(Map.of(
+                "client_id", clientId,
+                "iss", clientId,
+                "aud", issuer,
+                "exp", Instant.now().plus(Duration.ofMinutes(5)).toEpochMilli(),
+                "jti", UUID.randomUUID(),
+                "response_type", "code",
+                "scope", "openid",
+                "redirect_uri", redirectUri), header, rpKey).serialize()));
+        final FlowExecutionResult result =
+                flowExecutor.launchExecution(PushedAuthorizeFlowTest.FLOW_ID, null, externalContext);
+        assertSuccessResponse(result, clientId);
+        final PushedAuthorizationSuccessResponse response =
+                parseSuccessResponse(result, PushedAuthorizationSuccessResponse.class);
+        verifyAuthorizeEndpoint(clientId, response.getRequestURI().toString());
+    }
+
+    @Test
+    public void testWithPublicClientWithSignedRequestObject_providedInvalidTrustChain() throws Exception {
+        final OIDCClientMetadata metadata = new OIDCClientMetadata();
+        metadata.setRedirectionURI(new URI(redirectUri));
+        metadata.setResponseTypes(Set.of(ResponseType.CODE));
+        metadata.setJWKSet(new JWKSet(rpKey.toPublicJWK()));
+        metadata.setTokenEndpointAuthMethod(ClientAuthenticationMethod.NONE);
+        final String clientId = uniqueClientId();
+        
+        final List<String> trustChain = List.of(
+                "invalid" + rpEntityConfiguration(clientId, metadata),
+                subordinateStatement(clientId, Map.of("openid_relying_party",
+                        new OIDCClientMetadata().toJSONObject())),
+                trustedAnchorConfiguration());
+
+        final JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.RS256)
+                .type(new JOSEObjectType("entity-statement+jwt"))
+                .keyID(rpKey.getKeyID())
+                .customParam("trust_chain", trustChain)
+                .build();
+
+        setHttpFormRequest("POST", createRequestParameters(clientId, "openid", "code", signedRequestObject(Map.of(
+                "client_id", clientId,
+                "iss", clientId,
+                "aud", issuer,
+                "exp", Instant.now().plus(Duration.ofMinutes(5)).toEpochMilli(),
+                "jti", UUID.randomUUID(),
+                "response_type", "code",
+                "scope", "openid",
+                "redirect_uri", redirectUri), header, rpKey).serialize()));
+        final FlowExecutionResult result =
+                flowExecutor.launchExecution(PushedAuthorizeFlowTest.FLOW_ID, null, externalContext);
+        assertErrorCode(result, OAuth2Error.INVALID_REQUEST_CODE);
+    }
+
     @Test
     public void testWithTestVectors() throws Exception {
         final List<Map<String, Object>> vectors = loadPolicyTestVectors();
diff --git a/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/cache/EntityConfigurationMetadataCacheTest.java b/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/cache/EntityConfigurationMetadataCacheTest.java
index 3aa17fd..43c44e5 100644
--- a/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/cache/EntityConfigurationMetadataCacheTest.java
+++ b/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/cache/EntityConfigurationMetadataCacheTest.java
@@ -34,10 +34,12 @@ import org.testng.annotations.Test;
 
 import com.nimbusds.jose.EncryptionMethod;
 import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JOSEObjectType;
 import com.nimbusds.jose.JWEAlgorithm;
 import com.nimbusds.jose.JWEObject;
 import com.nimbusds.jose.JWEHeader;
 import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.JWSHeader;
 import com.nimbusds.jose.Payload;
 import com.nimbusds.jose.crypto.RSAEncrypter;
 import com.nimbusds.jose.jwk.JWKSet;
@@ -351,6 +353,48 @@ public class EntityConfigurationMetadataCacheTest extends AbstractFederationFlow
         assertNoEntityConfiguration(entityId);
     }
 
+    @Test
+    public void testValidClaims_forbiddenHeader_trustChain()
+            throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+        final String entityId = uniqueClientId();
+        final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(entityId).subject(entityId)
+                .issueTime(Date.from(Instant.now()))
+                .expirationTime(Date.from(Instant.now().plusSeconds(300)))
+                .claim("jwks", new JWKSet(leafKey).toJSONObject(true))
+                .claim("metadata", Map.of("federation_entity", Collections.emptyMap()));
+        final JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.RS256)
+                .type(new JOSEObjectType("entity-statement+jwt"))
+                .keyID(leafKey.getKeyID())
+                .customParam("trust_chain", "forbidden")
+                .build();
+        final String entityConfiguration = TrustChainTestUtil.signedJwt(
+                JWSAlgorithm.RS256, leafKey, header, builder.build()).serialize();
+
+        mapResponse(entityConfigurationUrl(entityId), mockResponse(entityConfiguration));
+        assertNoEntityConfiguration(entityId);
+    }
+
+    @Test
+    public void testValidClaims_forbiddenHeader_peerTrustChain()
+            throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+        final String entityId = uniqueClientId();
+        final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(entityId).subject(entityId)
+                .issueTime(Date.from(Instant.now()))
+                .expirationTime(Date.from(Instant.now().plusSeconds(300)))
+                .claim("jwks", new JWKSet(leafKey).toJSONObject(true))
+                .claim("metadata", Map.of("federation_entity", Collections.emptyMap()));
+        final JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.RS256)
+                .type(new JOSEObjectType("entity-statement+jwt"))
+                .keyID(leafKey.getKeyID())
+                .customParam("peer_trust_chain", "forbidden")
+                .build();
+        final String entityConfiguration = TrustChainTestUtil.signedJwt(
+                JWSAlgorithm.RS256, leafKey, header, builder.build()).serialize();
+
+        mapResponse(entityConfigurationUrl(entityId), mockResponse(entityConfiguration));
+        assertNoEntityConfiguration(entityId);
+    }
+
     @Test
     public void testEmptyAuthorityHintsClaim()
             throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
diff --git a/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/cache/SubordinateStatementMetadataCacheTest.java b/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/cache/SubordinateStatementMetadataCacheTest.java
index 0f5b20a..76a68c1 100644
--- a/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/cache/SubordinateStatementMetadataCacheTest.java
+++ b/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/cache/SubordinateStatementMetadataCacheTest.java
@@ -34,10 +34,12 @@ import org.testng.annotations.Test;
 
 import com.nimbusds.jose.EncryptionMethod;
 import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JOSEObjectType;
 import com.nimbusds.jose.JWEAlgorithm;
 import com.nimbusds.jose.JWEHeader;
 import com.nimbusds.jose.JWEObject;
 import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.JWSHeader;
 import com.nimbusds.jose.Payload;
 import com.nimbusds.jose.crypto.RSAEncrypter;
 import com.nimbusds.jose.jwk.JWKSet;
@@ -277,6 +279,54 @@ public class SubordinateStatementMetadataCacheTest extends AbstractFederationFlo
         assertNoSubordinateStatement(entityId);
     }
 
+    @Test
+    public void testValidClaims_forbiddenHeader_trustChain()
+            throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+        final String entityId = uniqueClientId();
+        final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(anchorId).subject(entityId)
+                .issueTime(Date.from(Instant.now()))
+                .expirationTime(Date.from(Instant.now().plusSeconds(300)))
+                .claim("jwks", new JWKSet(leafKey).toJSONObject(true))
+                .claim("metadata", Map.of("federation_entity", Collections.emptyMap()));
+        final JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.RS256)
+                .type(new JOSEObjectType("entity-statement+jwt"))
+                .keyID(trustedAnchorKey.getKeyID())
+                .customParam("trust_chain", "forbidden")
+                .build();
+        final String subordinateStatement = TrustChainTestUtil.signedJwt(
+                JWSAlgorithm.RS256, trustedAnchorKey, header, builder.build()).serialize();
+
+        mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+        mapResponse(subordinateStatementUrl(anchorFetchEndpoint, entityId),
+                mockResponse(subordinateStatement));
+
+        assertNoSubordinateStatement(entityId);
+    }
+
+    @Test
+    public void testValidClaims_forbiddenHeader_peerTrustChain()
+            throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+        final String entityId = uniqueClientId();
+        final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(anchorId).subject(entityId)
+                .issueTime(Date.from(Instant.now()))
+                .expirationTime(Date.from(Instant.now().plusSeconds(300)))
+                .claim("jwks", new JWKSet(leafKey).toJSONObject(true))
+                .claim("metadata", Map.of("federation_entity", Collections.emptyMap()));
+        final JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.RS256)
+                .type(new JOSEObjectType("entity-statement+jwt"))
+                .keyID(trustedAnchorKey.getKeyID())
+                .customParam("peer_trust_chain", "forbidden")
+                .build();
+        final String subordinateStatement = TrustChainTestUtil.signedJwt(
+                JWSAlgorithm.RS256, trustedAnchorKey, header, builder.build()).serialize();
+
+        mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+        mapResponse(subordinateStatementUrl(anchorFetchEndpoint, entityId),
+                mockResponse(subordinateStatement));
+
+        assertNoSubordinateStatement(entityId);
+    }
+
     @Test
     public void testIssuedInFuture()
             throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {

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


More information about the commits mailing list