[java-idp-oidc] branch dev/JOIDC-222 updated: JOIDC-222 - Support for OpenID Federation

Henri Mikkonen henri.mikkonen at iki.fi
Mon Mar 17 15:36:46 UTC 2025


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

hjmikkon pushed a commit to branch dev/JOIDC-222
in repository java-idp-oidc.

View the commit online:
http://git.shibboleth.net/view/?p=java-idp-oidc.git;a=commit;h=457614caf9b8a96eb67c0894f94e6c1d96788de8

The following commit(s) were added to refs/heads/dev/JOIDC-222 by this push:
     new 457614ca JOIDC-222 - Support for OpenID Federation
457614ca is described below

commit 457614caf9b8a96eb67c0894f94e6c1d96788de8
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Mon Mar 17 17:36:11 2025 +0200

    JOIDC-222 - Support for OpenID Federation
    
    https://shibboleth.atlassian.net/browse/JOIDC-222
    
    Initial implementation for the explicit registration flow (oidfed/register)
    - new abstract-register flow serves both OIDC and OIDFed register flows
      - some of the actions of oidc/register were refactored to be able to serve both flows
    - new profile configuration: OIDFederationExplicitRegistrationConfiguration
      - Refactored automatic registration configuration to share the same abstract configuration
---
 .../OIDCClientRegistrationResponseContext.java     |  70 ++++--
 .../DefaultClientInformationLookupFunction.java    |  47 ++++
 .../DefaultRequestedMetadataLookupFunction.java    |  48 ++++
 ...ederationRegistrationProfileConfiguration.java} |  42 +---
 ...derationAutomaticRegistrationConfiguration.java | 131 +---------
 ...ederationExplicitRegistrationConfiguration.java | 195 +++++++++++++++
 ...derationAutomaticRegistrationConfiguration.java |  59 +----
 ...ederationExplicitRegistrationConfiguration.java |  50 ++++
 ...ederationRegistrationProfileConfiguration.java} |  28 +--
 .../TokenEndpointAuthMethodLookupFunction.java     |  55 +++++
 .../ExplicitClientRegistrationRequestDecoder.java  | 154 ++++++++++++
 .../impl/ExplicitClientRegistrationRequest.java    | 100 ++++++++
 .../impl/ExplicitClientRegistrationResponse.java   |  97 ++++++++
 ...ava => AbstractBuildEntityStatementAction.java} | 187 ++++++---------
 .../profile/impl/BuildEntityConfiguration.java     | 148 ++++++++++++
 .../impl/BuildExplicitRegistrationResponse.java    | 172 ++++++++++++++
 .../impl/FormExplicitRegistrationResponse.java     | 118 +++++++++
 ...ltSelectedTrustChainMetadataLookupStrategy.java |  76 ++++++
 ...tRegistrationRequestClientIDLookupFunction.java |  68 ++++++
 ...entRegistrationRequestJWKSetLookupFunction.java |  69 ++++++
 .../LocalMetadataPolicyLookupFunction.java         |   8 +-
 .../MandatoryTrustMarksLookupFunction.java         |   8 +-
 .../MaximumTrustMarkLifetimeLookupFunction.java    |   8 +-
 .../impl/AbstractOIDCClientRegistrationAction.java |  98 ++++++++
 .../op/profile/impl/BuildClientInformation.java    |  71 +-----
 .../oidc/op/profile/impl/CheckRedirectURIs.java    |  44 ++--
 ...rmOutboundClientInformationResponseMessage.java |  50 ++++
 .../op/profile/impl/StoreClientInformation.java    |  39 ++-
 .../oidc-abstract-register-beans.xml               | 131 ++++++++++
 .../oidc-abstract-register-flow.xml}               |  35 +--
 .../idp/flows/oidc/abstract/oidc-abstract-flow.xml |   5 +-
 .../idp/flows/oidc/register/register-beans.xml     |  98 +-------
 .../idp/flows/oidc/register/register-flow.xml      |  53 +----
 .../entity-configuration-beans.xml                 |   2 +-
 .../idp/flows/oidfed/register/register-beans.xml   | 264 +++++++++++++++++++++
 .../idp/flows/oidfed/register/register-flow.xml    |  49 ++++
 .../idp/service/relying-party/postconfig.xml       |   5 +
 .../profile/flow/oidfed/RegistrationFlowTest.java  | 101 ++++++++
 .../profile/impl/BuildClientInformationTest.java   |  19 +-
 .../op/profile/impl/CheckRedirectUrisTest.java     |   2 +-
 .../shibboleth/idp/module/conf/relying-party.xml   |   1 +
 41 files changed, 2319 insertions(+), 686 deletions(-)

diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/messaging/context/OIDCClientRegistrationResponseContext.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/messaging/context/OIDCClientRegistrationResponseContext.java
index 5ab7f7de..db612454 100644
--- a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/messaging/context/OIDCClientRegistrationResponseContext.java
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/messaging/context/OIDCClientRegistrationResponseContext.java
@@ -16,8 +16,11 @@ package net.shibboleth.idp.plugin.oidc.op.messaging.context;
 
 import java.time.Instant;
 
+import javax.annotation.Nullable;
+
 import org.opensaml.messaging.context.BaseContext;
 
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformation;
 import com.nimbusds.openid.connect.sdk.rp.OIDCClientMetadata;
 
 /**
@@ -27,31 +30,34 @@ import com.nimbusds.openid.connect.sdk.rp.OIDCClientMetadata;
 public class OIDCClientRegistrationResponseContext extends BaseContext  {
 
     /** Mandatory Unique Client Identifier. */
-    private String clientId;
+    @Nullable private String clientId;
     
     /** Optional client secret. */
-    private String clientSecret;
+    @Nullable private String clientSecret;
     
     /** Optional registration access token. */
-    private String regAccessToken;
+    @Nullable private String regAccessToken;
     
     /** Optional location of the client configuration endpoint. */
-    private String regClientUri;
+    @Nullable private String regClientUri;
     
     /** Optional time at which the client identifier was issued. */
-    private Instant clientIdIssuedAt;
+    @Nullable private Instant clientIdIssuedAt;
     
     /** Time at which the client secret will expire or 0 if it will not expire. Required if the secret was issued. */
-    private Instant clientSecretExpiresAt;
+    @Nullable private Instant clientSecretExpiresAt;
     
     /** The metadata for the client: the attributes supported by the OP must be included. */
-    private OIDCClientMetadata clientMetadata;
+    @Nullable private OIDCClientMetadata clientMetadata;
+
+    /** The client information object carrying client ID, secret and metadata. */
+    @Nullable private OIDCClientInformation clientInformation;
 
     /**
      * Get the client identifier.
      * @return The client identifier.
      */
-    public String getClientId() {
+    @Nullable public String getClientId() {
         return clientId;
     }
 
@@ -59,7 +65,7 @@ public class OIDCClientRegistrationResponseContext extends BaseContext  {
      * Set the client identifier.
      * @param id The client identifier.
      */
-    public void setClientId(final String id) {
+    public void setClientId(@Nullable final String id) {
         this.clientId = id;
     }
 
@@ -67,7 +73,7 @@ public class OIDCClientRegistrationResponseContext extends BaseContext  {
      * Get the client secret.
      * @return The client secret.
      */
-    public String getClientSecret() {
+    @Nullable public String getClientSecret() {
         return clientSecret;
     }
 
@@ -75,7 +81,7 @@ public class OIDCClientRegistrationResponseContext extends BaseContext  {
      * Set the client secret.
      * @param secret The client secret.
      */
-    public void setClientSecret(final String secret) {
+    public void setClientSecret(@Nullable final String secret) {
         this.clientSecret = secret;
     }
 
@@ -83,7 +89,7 @@ public class OIDCClientRegistrationResponseContext extends BaseContext  {
      * Get the registration access token.
      * @return The registration access token.
      */
-    public String getRegAccessToken() {
+    @Nullable public String getRegAccessToken() {
         return regAccessToken;
     }
 
@@ -91,7 +97,7 @@ public class OIDCClientRegistrationResponseContext extends BaseContext  {
      * Set the registration access token.
      * @param accessToken The registration access token.
      */
-    public void setRegAccessToken(final String accessToken) {
+    public void setRegAccessToken(@Nullable final String accessToken) {
         this.regAccessToken = accessToken;
     }
 
@@ -99,7 +105,7 @@ public class OIDCClientRegistrationResponseContext extends BaseContext  {
      * Get the location of the client configuration endpoint.
      * @return The location of the client configuration endpoint.
      */
-    public String getRegClientUri() {
+    @Nullable public String getRegClientUri() {
         return regClientUri;
     }
 
@@ -107,7 +113,7 @@ public class OIDCClientRegistrationResponseContext extends BaseContext  {
      * Set the location of the client configuration endpoint.
      * @param clientUri The location of the client configuration endpoint.
      */
-    public void setRegClientUri(final String clientUri) {
+    public void setRegClientUri(@Nullable final String clientUri) {
         this.regClientUri = clientUri;
     }
 
@@ -115,7 +121,7 @@ public class OIDCClientRegistrationResponseContext extends BaseContext  {
      * Get the time at which the client identifier was issued.
      * @return The time at which the client identifier was issued.
      */
-    public Instant getClientIdIssuedAt() {
+    @Nullable public Instant getClientIdIssuedAt() {
         return clientIdIssuedAt;
     }
 
@@ -123,7 +129,7 @@ public class OIDCClientRegistrationResponseContext extends BaseContext  {
      * Set the time at which the client identifier was issued.
      * @param idIssuedAt The time at which the client identifier was issued.
      */
-    public void setClientIdIssuedAt(final Instant idIssuedAt) {
+    public void setClientIdIssuedAt(@Nullable final Instant idIssuedAt) {
         this.clientIdIssuedAt = idIssuedAt;
     }
 
@@ -131,7 +137,7 @@ public class OIDCClientRegistrationResponseContext extends BaseContext  {
      * Get the time at which the client secret will expire.
      * @return The time at which the client secret will expire.
      */
-    public Instant getClientSecretExpiresAt() {
+    @Nullable public Instant getClientSecretExpiresAt() {
         return clientSecretExpiresAt;
     }
 
@@ -139,7 +145,7 @@ public class OIDCClientRegistrationResponseContext extends BaseContext  {
      * Set the time at which the client secret will expire.
      * @param secretExpiresAt The time at which the client secret will expire.
      */
-    public void setClientSecretExpiresAt(final Instant secretExpiresAt) {
+    public void setClientSecretExpiresAt(@Nullable final Instant secretExpiresAt) {
         this.clientSecretExpiresAt = secretExpiresAt;
     }
 
@@ -147,7 +153,7 @@ public class OIDCClientRegistrationResponseContext extends BaseContext  {
      * Get the metadata for the client: the attributes supported by the OP must be included.
      * @return The metadata for the client: the attributes supported by the OP must be included.
      */
-    public OIDCClientMetadata getClientMetadata() {
+    @Nullable public OIDCClientMetadata getClientMetadata() {
         return clientMetadata;
     }
     
@@ -155,7 +161,29 @@ public class OIDCClientRegistrationResponseContext extends BaseContext  {
      * Set the metadata for the client: the attributes supported by the OP must be included.
      * @param metadata The metadata for the client: the attributes supported by the OP must be included.
      */
-    public void setClientMetadata(final OIDCClientMetadata metadata) {
+    public void setClientMetadata(@Nullable final OIDCClientMetadata metadata) {
         this.clientMetadata = metadata;
     }
+
+    /**
+     * Set the client information object carrying client ID, secret and metadata.
+     * 
+     * @since 4.3.0
+     * 
+     * @param information The client information object carrying client ID, secret and metadata.
+     */
+    public void setClientInformation(@Nullable final OIDCClientInformation information) {
+        clientInformation = information;
+    }
+
+    /**
+     * Get the client information object carrying client ID, secret and metadata.
+     * 
+     * @since 4.3.0
+     * 
+     * @return The client information object carrying client ID, secret and metadata.
+     */
+    @Nullable public OIDCClientInformation getClientInformation() {
+        return clientInformation;
+    }
 }
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/messaging/context/navigate/DefaultClientInformationLookupFunction.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/messaging/context/navigate/DefaultClientInformationLookupFunction.java
new file mode 100644
index 00000000..75acdee2
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/messaging/context/navigate/DefaultClientInformationLookupFunction.java
@@ -0,0 +1,47 @@
+/*
+ * 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.messaging.context.navigate;
+
+import java.util.Objects;
+import java.util.Optional;
+import java.util.function.Function;
+
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformation;
+
+import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCClientRegistrationResponseContext;
+
+/**
+ * Default lookup function for fetching {@link OIDCClientInformation} from {@link OIDCClientRegistrationResponseContext}
+ * located under outbound message context.
+ * 
+ * @since 4.3.0
+ */
+public class DefaultClientInformationLookupFunction implements Function<ProfileRequestContext, OIDCClientInformation> {
+
+    /** {@inheritDoc} */
+    @Nullable public OIDCClientInformation apply(@Nullable final ProfileRequestContext input) {
+        return Optional.ofNullable(input)
+                .map(profileRequestContext -> profileRequestContext.getOutboundMessageContext())
+                .filter(Objects::nonNull)
+                .map(messageContext -> messageContext.getSubcontext(OIDCClientRegistrationResponseContext.class))
+                .filter(Objects::nonNull)
+                .map(oidcResponseContext -> oidcResponseContext.getClientInformation())
+                .orElse(null);
+    }
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/messaging/context/navigate/DefaultRequestedMetadataLookupFunction.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/messaging/context/navigate/DefaultRequestedMetadataLookupFunction.java
new file mode 100644
index 00000000..655cd40a
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/messaging/context/navigate/DefaultRequestedMetadataLookupFunction.java
@@ -0,0 +1,48 @@
+/*
+ * 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.messaging.context.navigate;
+
+import java.util.Objects;
+import java.util.Optional;
+import java.util.function.Function;
+
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientMetadata;
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientRegistrationRequest;
+
+/**
+ * Default lookup function for fetching requested {@link OIDCClientMetadata} from the inbound dynamic client
+ * registration request message ({@link OIDCClientRegistrationRequest}).
+ * 
+ * @since 4.3.0
+ */
+public class DefaultRequestedMetadataLookupFunction implements Function<ProfileRequestContext, OIDCClientMetadata> {
+
+    /** {@inheritDoc} */
+    @Nullable public OIDCClientMetadata apply(@Nullable final ProfileRequestContext input) {
+        return Optional.ofNullable(input)
+                .map(profileRequestContext -> profileRequestContext.getInboundMessageContext())
+                .filter(Objects::nonNull)
+                .map(messageContext -> messageContext.getMessage())
+                .filter(OIDCClientRegistrationRequest.class::isInstance)
+                .map(OIDCClientRegistrationRequest.class::cast)
+                .map(request -> request.getOIDCClientMetadata())
+                .orElse(null);
+                
+    }
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/config/DefaultOIDFederationAutomaticRegistrationConfiguration.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/config/AbstractOIDFederationRegistrationProfileConfiguration.java
similarity index 80%
copy from idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/config/DefaultOIDFederationAutomaticRegistrationConfiguration.java
copy to idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/config/AbstractOIDFederationRegistrationProfileConfiguration.java
index 42c52a7b..219e6d61 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/config/DefaultOIDFederationAutomaticRegistrationConfiguration.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/config/AbstractOIDFederationRegistrationProfileConfiguration.java
@@ -1,16 +1,3 @@
-/*
- * 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.config;
 
@@ -26,6 +13,7 @@ import org.opensaml.profile.context.ProfileRequestContext;
 
 import net.shibboleth.oidc.metadata.policy.MetadataPolicy;
 import net.shibboleth.profile.config.AbstractConditionalProfileConfiguration;
+import net.shibboleth.shared.annotation.ParameterName;
 import net.shibboleth.shared.annotation.constraint.NonnullElements;
 import net.shibboleth.shared.annotation.constraint.NotEmpty;
 import net.shibboleth.shared.annotation.constraint.NotLive;
@@ -36,14 +24,10 @@ import net.shibboleth.shared.logic.Constraint;
 import net.shibboleth.shared.logic.FunctionSupport;
 
 /**
- * Implementation of a profile configuration for the OpenID Federation Automatic Registration.
+ * Abstract implementation class for profile configurations related OpenID Federation client registration.
  */
-public class DefaultOIDFederationAutomaticRegistrationConfiguration extends AbstractConditionalProfileConfiguration
-        implements OIDFederationAutomaticRegistrationConfiguration {
-
-    /** OIDC provider information profile counter name. */
-    @Nonnull @NotEmpty
-    public static final String PROFILE_COUNTER = "net.shibboleth.idp.profiles.oidfed.automaticregistration";
+public class AbstractOIDFederationRegistrationProfileConfiguration extends AbstractConditionalProfileConfiguration
+    implements OIDFederationRegistrationProfileConfiguration {
 
     /** Lookup function to local metadata policy to be merged into the federation policy. */
     @Nonnull private Function<ProfileRequestContext,Map<String, MetadataPolicy>> localMetadataPolicyLookupStrategy;
@@ -56,18 +40,12 @@ public class DefaultOIDFederationAutomaticRegistrationConfiguration extends Abst
 
     /**
      * Constructor.
+     * 
+     * @param id ID of the communication profile, never null or empty
      */
-    public DefaultOIDFederationAutomaticRegistrationConfiguration() {
-        this(PROFILE_ID);
-    }
-
-    /**
-     * Creates a new configuration instance.
-     *
-     * @param profileId Unique profile identifier.
-     */
-    public DefaultOIDFederationAutomaticRegistrationConfiguration(@Nonnull @NotEmpty final String profileId) {
-        super(profileId);
+    public AbstractOIDFederationRegistrationProfileConfiguration(
+            @Nonnull @NotEmpty @ParameterName(name="id") final String id) {
+        super(id);
         localMetadataPolicyLookupStrategy = FunctionSupport.constant(CollectionSupport.emptyMap());
         mandatoryTrustMarksLookupStrategy = FunctionSupport.constant(CollectionSupport.emptyList());
         maximumTrustMarkLifetimeLookupStrategy = FunctionSupport.constant(Duration.ofDays(365));
@@ -169,4 +147,4 @@ public class DefaultOIDFederationAutomaticRegistrationConfiguration extends Abst
             @Nullable final Function<ProfileRequestContext,Duration> strategy) {
         maximumTrustMarkLifetimeLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null");
     }
-}
\ No newline at end of file
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/config/DefaultOIDFederationAutomaticRegistrationConfiguration.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/config/DefaultOIDFederationAutomaticRegistrationConfiguration.java
index 42c52a7b..01bc35d5 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/config/DefaultOIDFederationAutomaticRegistrationConfiguration.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/config/DefaultOIDFederationAutomaticRegistrationConfiguration.java
@@ -14,46 +14,21 @@
 
 package net.shibboleth.idp.plugin.oidc.op.oidfed.config;
 
-import java.time.Duration;
-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.oidc.metadata.policy.MetadataPolicy;
-import net.shibboleth.profile.config.AbstractConditionalProfileConfiguration;
-import net.shibboleth.shared.annotation.constraint.NonnullElements;
 import net.shibboleth.shared.annotation.constraint.NotEmpty;
-import net.shibboleth.shared.annotation.constraint.NotLive;
-import net.shibboleth.shared.annotation.constraint.Positive;
-import net.shibboleth.shared.annotation.constraint.Unmodifiable;
-import net.shibboleth.shared.collection.CollectionSupport;
-import net.shibboleth.shared.logic.Constraint;
-import net.shibboleth.shared.logic.FunctionSupport;
 
 /**
  * Implementation of a profile configuration for the OpenID Federation Automatic Registration.
  */
-public class DefaultOIDFederationAutomaticRegistrationConfiguration extends AbstractConditionalProfileConfiguration
-        implements OIDFederationAutomaticRegistrationConfiguration {
+public class DefaultOIDFederationAutomaticRegistrationConfiguration
+    extends AbstractOIDFederationRegistrationProfileConfiguration
+    implements OIDFederationAutomaticRegistrationConfiguration {
 
     /** OIDC provider information profile counter name. */
     @Nonnull @NotEmpty
     public static final String PROFILE_COUNTER = "net.shibboleth.idp.profiles.oidfed.automaticregistration";
 
-    /** Lookup function to local metadata policy to be merged into the federation policy. */
-    @Nonnull private Function<ProfileRequestContext,Map<String, MetadataPolicy>> localMetadataPolicyLookupStrategy;
-
-    /** Lookup function to mandatory trust marks. */
-    @Nonnull private Function<ProfileRequestContext,List<String>> mandatoryTrustMarksLookupStrategy;
-
-    /** Lookup function to supply maximum trust mark lifetime. */
-    @Nonnull private Function<ProfileRequestContext,Duration> maximumTrustMarkLifetimeLookupStrategy;
-
     /**
      * Constructor.
      */
@@ -68,105 +43,5 @@ public class DefaultOIDFederationAutomaticRegistrationConfiguration extends Abst
      */
     public DefaultOIDFederationAutomaticRegistrationConfiguration(@Nonnull @NotEmpty final String profileId) {
         super(profileId);
-        localMetadataPolicyLookupStrategy = FunctionSupport.constant(CollectionSupport.emptyMap());
-        mandatoryTrustMarksLookupStrategy = FunctionSupport.constant(CollectionSupport.emptyList());
-        maximumTrustMarkLifetimeLookupStrategy = FunctionSupport.constant(Duration.ofDays(365));
-    }
-
-    /** {@inheritDoc} */
-    @Override @Nonnull @NonnullElements @NotLive @Unmodifiable
-    public Map<String, MetadataPolicy> getLocalMetadataPolicy(
-            @Nullable final ProfileRequestContext profileRequestContext) {
-        final Map<String, MetadataPolicy> policy = localMetadataPolicyLookupStrategy.apply(profileRequestContext);
-        if (policy != null) {
-            return CollectionSupport.copyToMap(policy);
-        }
-        return CollectionSupport.emptyMap();
-    }
-
-    /**
-     * Set local metadata policy to be merged into the federation policy.
-     * 
-     * @param policy metadata policy
-     */
-    public void setLocalMetadataPolicy(
-            @Nonnull @NonnullElements @NotLive @Unmodifiable final Map<String, MetadataPolicy> policy) {
-        localMetadataPolicyLookupStrategy = FunctionSupport.constant(policy);
-    }
-
-    /**
-     * Sets lookup strategy for local metadata policy to be merged into the federation policy.
-     * 
-     * @param strategy lookup strategy
-     */
-    public void setLocalMetadataPolicyLookupStrategy(
-            @Nonnull final Function<ProfileRequestContext,Map<String, MetadataPolicy>> strategy) {
-        localMetadataPolicyLookupStrategy =
-                Constraint.isNotNull(strategy, "Local metadata policy lookup strategy cannot be null");
-    }
-
-    /** {@inheritDoc} */
-    @Override @Nonnull @NonnullElements @NotLive @Unmodifiable
-    public List<String> getMandatoryTrustMarks(@Nullable final ProfileRequestContext profileRequestContext) {
-        final List<String> trustMarks = mandatoryTrustMarksLookupStrategy.apply(profileRequestContext);
-        if (trustMarks != null) {
-            return CollectionSupport.copyToList(trustMarks);
-        }
-        return CollectionSupport.emptyList();
-    }
-
-    /**
-     * Set mandatory trust marks.
-     * 
-     * @param marks trust marks
-     */
-    public void setMandatoryTrustMarks(@Nonnull @NonnullElements @NotLive @Unmodifiable final List<String> marks) {
-        mandatoryTrustMarksLookupStrategy = FunctionSupport.constant(marks);
-    }
-
-    /**
-     * Sets lookup strategy for mandatory trust marks value.
-     * 
-     * @param strategy lookup strategy
-     */
-    public void setMandatoryTrustMarksLookupStrategy(
-            @Nonnull final Function<ProfileRequestContext,List<String>> strategy) {
-        mandatoryTrustMarksLookupStrategy =
-                Constraint.isNotNull(strategy, "Mandatory trust marks lookup strategy cannot be null");
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    @Positive @Nonnull
-    public Duration getMaximumTrustMarkLifetime(@Nullable final ProfileRequestContext profileRequestContext) {
-        final Duration lifetime = maximumTrustMarkLifetimeLookupStrategy.apply(profileRequestContext);
-
-        Constraint.isTrue(lifetime != null && !lifetime.isZero() && !lifetime.isNegative(),
-                "Maximum trust mark lifetime must be greater than 0");
-        assert lifetime != null;
-        return lifetime;
-    }
-
-    /**
-     * Set the maximum lifetime of a trust mark.
-     * 
-     * @param lifetime lifetime of a trust mark
-     */
-    public void setMaximumTrustMarkLifetime(@Positive @Nonnull final Duration lifetime) {
-        final Duration trustMarkLifetime = Constraint.isNotNull(lifetime, "Maximum trust mark lifetime cannot be null");
-        Constraint.isTrue(!trustMarkLifetime.isZero() && !trustMarkLifetime.isNegative(),
-                "Maximum trust mark lifetime must be greater than 0");
-
-        maximumTrustMarkLifetimeLookupStrategy = FunctionSupport.constant(trustMarkLifetime);
-    }
-
-    /**
-     * Set a lookup strategy for the maximum trust mark lifetime.
-     *
-     * @param strategy lookup strategy
-     */
-    public void setMaximumTrustMarkLifetimeLookupStrategy(
-            @Nullable final Function<ProfileRequestContext,Duration> strategy) {
-        maximumTrustMarkLifetimeLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null");
     }
 }
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/config/DefaultOIDFederationExplicitRegistrationConfiguration.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/config/DefaultOIDFederationExplicitRegistrationConfiguration.java
new file mode 100644
index 00000000..4cacb53a
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/config/DefaultOIDFederationExplicitRegistrationConfiguration.java
@@ -0,0 +1,195 @@
+/*
+ * 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.config;
+
+import java.util.Collection;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod;
+
+import net.shibboleth.shared.annotation.constraint.NonnullElements;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.annotation.constraint.NotLive;
+import net.shibboleth.shared.annotation.constraint.Unmodifiable;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.logic.FunctionSupport;
+import net.shibboleth.shared.logic.PredicateSupport;
+import net.shibboleth.shared.primitive.StringSupport;
+
+/**
+ * Implementation of a profile configuration for the OpenID Federation Explicit Registration.
+ */
+public class DefaultOIDFederationExplicitRegistrationConfiguration
+        extends AbstractOIDFederationRegistrationProfileConfiguration
+        implements OIDFederationExplicitRegistrationConfiguration {
+
+    /** OIDC provider information profile counter name. */
+    @Nonnull @NotEmpty
+    public static final String PROFILE_COUNTER = "net.shibboleth.idp.profiles.oidfed.explicitregistration";
+
+    /** Predicate used to indicate whether authorization code flow is supported by this profile. Default true. */
+    @Nonnull private Predicate<ProfileRequestContext> authorizationCodeFlowPredicate;
+
+    /** Predicate used to indicate whether implicit flow is supported by this profile. Default true. */
+    @Nonnull private Predicate<ProfileRequestContext> implicitFlowPredicate;
+
+    /** Predicate used to indicate whether refresh tokens are supported by this profile. Default true. */
+    @Nonnull private Predicate<ProfileRequestContext> refreshTokensPredicate;
+
+    /** Enabled token endpoint authentication methods. */
+    @Nonnull private Function<ProfileRequestContext,Set<String>> tokenEndpointAuthMethodsLookupStrategy;
+
+    /**
+     * Constructor.
+     */
+    public DefaultOIDFederationExplicitRegistrationConfiguration() {
+        this(PROFILE_ID);
+    }
+
+    /**
+     * Creates a new configuration instance.
+     *
+     * @param profileId Unique profile identifier.
+     */
+    public DefaultOIDFederationExplicitRegistrationConfiguration(@Nonnull @NotEmpty final String profileId) {
+        super(profileId);
+        authorizationCodeFlowPredicate = PredicateSupport.alwaysTrue();
+        implicitFlowPredicate = PredicateSupport.alwaysTrue();
+        refreshTokensPredicate = PredicateSupport.alwaysTrue();
+        tokenEndpointAuthMethodsLookupStrategy = FunctionSupport.constant(
+                CollectionSupport.setOf(
+                        ClientAuthenticationMethod.CLIENT_SECRET_BASIC.toString(),
+                        ClientAuthenticationMethod.CLIENT_SECRET_POST.toString(),
+                        ClientAuthenticationMethod.CLIENT_SECRET_JWT.toString(),
+                        ClientAuthenticationMethod.PRIVATE_KEY_JWT.toString()));
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean isAuthorizationCodeFlowEnabled(@Nullable final ProfileRequestContext profileRequestContext) {
+        return authorizationCodeFlowPredicate.test(profileRequestContext);
+    }
+
+    /**
+     * Set whether authorization code flow is supported by this profile.
+     * 
+     * @param flag flag to set
+     */
+    public void setAuthorizationCodeFlowEnabled(final boolean flag) {
+        authorizationCodeFlowPredicate = flag ? PredicateSupport.alwaysTrue() : PredicateSupport.alwaysFalse();
+    }
+
+    /**
+     * Set condition used to indicate whether authorization code flow is supported by this profile.
+     * 
+     * @param condition condition to set
+     */
+    public void setAuthorizationCodeFlowEnabledPredicate(@Nonnull final Predicate<ProfileRequestContext> condition) {
+        authorizationCodeFlowPredicate = Constraint.isNotNull(condition,
+                "Condition used to indicate whether authorization code flow is supported cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean isImplicitFlowEnabled(@Nullable final ProfileRequestContext profileRequestContext) {
+        return implicitFlowPredicate.test(profileRequestContext);
+    }
+    
+    /**
+     * Set whether hybrid flow is supported by this profile.
+     * 
+     * @param flag flag to set
+     */
+    public void setImplicitFlowEnabled(final boolean flag) {
+        implicitFlowPredicate = flag ? PredicateSupport.alwaysTrue() : PredicateSupport.alwaysFalse();
+    }
+
+    /**
+     * Set condition used to indicate whether hybrid flow is supported by this profile.
+     * 
+     * @param condition condition to set.
+     */
+    public void setImplicitFlowEnabledPredicate(@Nonnull final Predicate<ProfileRequestContext> condition) {
+        implicitFlowPredicate = Constraint.isNotNull(condition,
+                "Condition used to indicate whether hybrid flow is supported cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean isRefreshTokensEnabled(@Nullable final ProfileRequestContext profileRequestContext) {
+        return refreshTokensPredicate.test(profileRequestContext);
+    }
+
+    /**
+     * Set whether refresh tokens are supported by this profile.
+     * 
+     * @param flag flag to set
+     */
+    public void setRefreshTokensEnabled(final boolean flag) {
+        refreshTokensPredicate = flag ? PredicateSupport.alwaysTrue() : PredicateSupport.alwaysFalse();
+    }
+
+    /**
+     * Set condition used to indicate whether refresh tokens are supported by this profile.
+     * 
+     * @param condition condition to set
+     */
+    public void setRefreshTokensEnabledPredicate(@Nonnull final Predicate<ProfileRequestContext> condition) {
+        refreshTokensPredicate = Constraint.isNotNull(condition,
+                "Condition used to indicate whether refresh tokens are supported cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    @Nonnull @NonnullElements @NotLive @Unmodifiable public Set<String> getTokenEndpointAuthMethods(
+            @Nullable final ProfileRequestContext profileRequestContext) {
+        
+        final Collection<String> methods = tokenEndpointAuthMethodsLookupStrategy.apply(profileRequestContext);
+        if (methods != null) {
+            return CollectionSupport.copyToSet(methods);
+        }
+        return CollectionSupport.emptySet();
+    }
+
+    /**
+     * Set the enabled token endpoint authentication methods.
+     * 
+     * @param methods What to set.
+     */
+    public void setTokenEndpointAuthMethods(@Nonnull @NonnullElements final Collection<String> methods) {
+        Constraint.isNotNull(methods, "Collection of methods cannot be null");
+
+        tokenEndpointAuthMethodsLookupStrategy =
+                FunctionSupport.constant(Set.copyOf(StringSupport.normalizeStringCollection(methods)));
+    }
+
+    /**
+     * Set a lookup strategy for the enabled token endpoint authentication methods.
+     *
+     * @param strategy  lookup strategy
+     */
+    public void setTokenEndpointAuthMethodsLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,Set<String>> strategy) {
+        tokenEndpointAuthMethodsLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null");
+    }
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/config/OIDFederationAutomaticRegistrationConfiguration.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/config/OIDFederationAutomaticRegistrationConfiguration.java
index d396684b..fe07f232 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/config/OIDFederationAutomaticRegistrationConfiguration.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/config/OIDFederationAutomaticRegistrationConfiguration.java
@@ -14,69 +14,12 @@
 
 package net.shibboleth.idp.plugin.oidc.op.oidfed.config;
 
-import java.time.Duration;
-import java.util.List;
-import java.util.Map;
-
-import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
-
-import org.opensaml.profile.context.ProfileRequestContext;
-
-import net.shibboleth.oidc.metadata.policy.MetadataPolicy;
-import net.shibboleth.oidc.profile.config.OIDCProfileConfiguration;
-import net.shibboleth.shared.annotation.ConfigurationSetting;
-import net.shibboleth.shared.annotation.constraint.NonnullElements;
-import net.shibboleth.shared.annotation.constraint.NotLive;
-import net.shibboleth.shared.annotation.constraint.Positive;
-import net.shibboleth.shared.annotation.constraint.Unmodifiable;
-
 /** 
  * Profile configuration for an OpenID Federation Automatic Registration.
  */
-public interface OIDFederationAutomaticRegistrationConfiguration extends OIDCProfileConfiguration {
-    
-    /** OIDC base protocol URI. Sections 4 and 11 are the most relevant. */
-    public static final String PROTOCOL_URI = "https://openid.net/specs/openid-federation-1_0.html";
+public interface OIDFederationAutomaticRegistrationConfiguration extends OIDFederationRegistrationProfileConfiguration {
 
     /** ID for this profile configuration. */
     public static final String PROFILE_ID = "http://shibboleth.net/ns/profiles/oidfed/automaticregistration";
 
-    /**
-     * Get local metadata policy to be merged into the federation policy.
-     * 
-     * <p>Defaults to empty map.</p>
-     * 
-     * @param profileRequestContext profile request context
-     * 
-     * @return local metadata policy
-     */
-    @ConfigurationSetting(name="localMetadataPolicy")
-    @Nonnull @NonnullElements @NotLive @Unmodifiable
-    Map<String, MetadataPolicy> getLocalMetadataPolicy(@Nullable final ProfileRequestContext profileRequestContext);
-
-    /**
-     * Get the mandatory trust mark identifiers required by this profile configuration.
-     * 
-     * @param profileRequestContext profile request context
-     * 
-     * @return mandatory trust mark identifiers
-     */
-    @ConfigurationSetting(name="mandatoryTrustMarks")
-    @Nonnull @NonnullElements @NotLive @Unmodifiable List<String> getMandatoryTrustMarks(
-            @Nullable final ProfileRequestContext profileRequestContext);
-
-    /**
-     * Get maximum lifetime for trust marks.
-     * 
-     * <p>Defaults to one year.</p>
-     * 
-     * @param profileRequestContext profile request context
-     * 
-     * @return maximum lifetime
-     */
-    @ConfigurationSetting(name="maximumTrustMarkLifetime")
-    @Positive @Nonnull
-    Duration getMaximumTrustMarkLifetime(@Nullable final ProfileRequestContext profileRequestContext);
-
 }
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/config/OIDFederationExplicitRegistrationConfiguration.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/config/OIDFederationExplicitRegistrationConfiguration.java
new file mode 100644
index 00000000..7efcc2ea
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/config/OIDFederationExplicitRegistrationConfiguration.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.config;
+
+import java.util.Set;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import net.shibboleth.oidc.profile.oauth2.config.OAuth2FlowAwareProfileConfiguration;
+import net.shibboleth.shared.annotation.ConfigurationSetting;
+import net.shibboleth.shared.annotation.constraint.NonnullElements;
+import net.shibboleth.shared.annotation.constraint.NotLive;
+import net.shibboleth.shared.annotation.constraint.Unmodifiable;
+
+/** 
+ * Profile configuration for an OpenID Federation Explicit Registration.
+ */
+public interface OIDFederationExplicitRegistrationConfiguration
+    extends OIDFederationRegistrationProfileConfiguration, OAuth2FlowAwareProfileConfiguration {
+
+    /** ID for this profile configuration. */
+    public static final String PROFILE_ID = "http://shibboleth.net/ns/profiles/oidfed/explicitregistration";
+
+    /**
+     * Get the enabled token endpoint authentication methods.
+     * 
+     * @param profileRequestContext profile request context
+     * 
+     * @return enabled token endpoint authentication methods
+     */
+    @ConfigurationSetting(name="tokenEndpointAuthMethods")
+    @Nonnull @NonnullElements @NotLive @Unmodifiable Set<String> getTokenEndpointAuthMethods(
+            @Nullable final ProfileRequestContext profileRequestContext);
+
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/config/OIDFederationAutomaticRegistrationConfiguration.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/config/OIDFederationRegistrationProfileConfiguration.java
similarity index 66%
copy from idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/config/OIDFederationAutomaticRegistrationConfiguration.java
copy to idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/config/OIDFederationRegistrationProfileConfiguration.java
index d396684b..98277cc9 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/config/OIDFederationAutomaticRegistrationConfiguration.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/config/OIDFederationRegistrationProfileConfiguration.java
@@ -1,16 +1,3 @@
-/*
- * 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.config;
 
@@ -24,23 +11,20 @@ import javax.annotation.Nullable;
 import org.opensaml.profile.context.ProfileRequestContext;
 
 import net.shibboleth.oidc.metadata.policy.MetadataPolicy;
-import net.shibboleth.oidc.profile.config.OIDCProfileConfiguration;
+import net.shibboleth.oidc.profile.oauth2.config.OAuth2ProfileConfiguration;
 import net.shibboleth.shared.annotation.ConfigurationSetting;
 import net.shibboleth.shared.annotation.constraint.NonnullElements;
 import net.shibboleth.shared.annotation.constraint.NotLive;
 import net.shibboleth.shared.annotation.constraint.Positive;
 import net.shibboleth.shared.annotation.constraint.Unmodifiable;
 
-/** 
- * Profile configuration for an OpenID Federation Automatic Registration.
+/**
+ * Profile configuration for an OpenID Federation profiles related to client registration.
  */
-public interface OIDFederationAutomaticRegistrationConfiguration extends OIDCProfileConfiguration {
-    
-    /** OIDC base protocol URI. Sections 4 and 11 are the most relevant. */
-    public static final String PROTOCOL_URI = "https://openid.net/specs/openid-federation-1_0.html";
+public interface OIDFederationRegistrationProfileConfiguration extends OAuth2ProfileConfiguration {
 
-    /** ID for this profile configuration. */
-    public static final String PROFILE_ID = "http://shibboleth.net/ns/profiles/oidfed/automaticregistration";
+    /** OpenID Federation base protocol URI. */
+    public static final String PROTOCOL_URI = "https://openid.net/specs/openid-federation-1_0.html";
 
     /**
      * Get local metadata policy to be merged into the federation policy.
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/config/TokenEndpointAuthMethodLookupFunction.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/config/TokenEndpointAuthMethodLookupFunction.java
new file mode 100644
index 00000000..286349ee
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/config/TokenEndpointAuthMethodLookupFunction.java
@@ -0,0 +1,55 @@
+/*
+ * 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.config;
+
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod;
+
+import net.shibboleth.profile.context.RelyingPartyContext;
+import net.shibboleth.profile.context.navigate.AbstractRelyingPartyLookupFunction;
+
+/**
+ * A function that obtains
+ * {@link OIDFederationExplicitRegistrationConfiguration#getTokenEndpointAuthMethods(ProfileRequestContext)}
+ * if such a profile is available from a {@link RelyingPartyContext} obtained via a lookup function,
+ * by default a child of the {@link ProfileRequestContext}. That result is then transformed into a list
+ * of {@link ClientAuthenticationMethod}s.
+ * 
+ * <p>If a specific setting is unavailable, a null value is returned.</p>
+ */
+public class TokenEndpointAuthMethodLookupFunction
+        extends AbstractRelyingPartyLookupFunction<Set<ClientAuthenticationMethod>> {
+
+    /** {@inheritDoc} */
+    @Override
+    @Nullable public Set<ClientAuthenticationMethod> apply(@Nullable final ProfileRequestContext input) {
+        return Optional.ofNullable(getRelyingPartyContextLookupStrategy().apply(input))
+                .map(relyingPartyContext -> relyingPartyContext.getProfileConfig())
+                .filter(OIDFederationExplicitRegistrationConfiguration.class::isInstance)
+                .map(OIDFederationExplicitRegistrationConfiguration.class::cast)
+                .map(config -> config.getTokenEndpointAuthMethods(input).stream()
+                    .map(ClientAuthenticationMethod::new)
+                    .collect(Collectors.toUnmodifiableSet()))
+                .orElse(null);
+    }
+
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/decoding/impl/ExplicitClientRegistrationRequestDecoder.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/decoding/impl/ExplicitClientRegistrationRequestDecoder.java
new file mode 100644
index 00000000..da8276e8
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/decoding/impl/ExplicitClientRegistrationRequestDecoder.java
@@ -0,0 +1,154 @@
+/*
+ * 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.decoding.impl;
+
+import java.io.IOException;
+import java.net.URI;
+import java.util.List;
+import java.util.Objects;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.decoder.MessageDecodingException;
+import org.slf4j.Logger;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.base.MoreObjects;
+import com.nimbusds.oauth2.sdk.ParseException;
+import com.nimbusds.oauth2.sdk.http.HTTPRequest;
+import com.nimbusds.oauth2.sdk.http.JakartaServletUtils;
+import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
+
+import jakarta.servlet.http.HttpServletRequest;
+import net.shibboleth.idp.plugin.oidc.op.decoding.impl.RequestUtil;
+import net.shibboleth.idp.plugin.oidc.op.oauth2.decoding.impl.BaseOAuth2RequestDecoder;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.messaging.impl.ExplicitClientRegistrationRequest;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * Message decoder decoding OpenID Federation Explicit Registration request {@link ExplicitClientRegistrationRequest}.
+ * 
+ * @since 4.3.0
+ */
+public class ExplicitClientRegistrationRequestDecoder extends BaseOAuth2RequestDecoder<ExplicitClientRegistrationRequest> {
+
+    /** Class logger. */
+    @Nonnull
+    private final Logger log = LoggerFactory.getLogger(ExplicitClientRegistrationRequestDecoder.class);
+
+    /** Object mapper used for pretty-printing JSON in the request and decoding trust chain from the request. */
+    @NonnullAfterInit private ObjectMapper objectMapper;
+
+    /**
+     * Set the object mapper used for pretty-printing JSON in the request and decoding trust chain from the request.
+     * 
+     * @param mapper What to set.
+     */
+    public void setObjectMapper(@Nonnull final ObjectMapper mapper) {
+        checkSetterPreconditions();
+        objectMapper = Constraint.isNotNull(mapper, "Object mapper cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+
+        if (objectMapper == null) {
+            throw new ComponentInitializationException("Object mapper cannot be null");
+        }
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    protected ExplicitClientRegistrationRequest parseMessage() throws MessageDecodingException {
+        final HttpServletRequest request = getHttpServletRequest();
+        assert request != null;
+        if (!"POST".equalsIgnoreCase(request.getMethod())) {
+            throw new MessageDecodingException("This message decoder only supports the HTTP POST method");
+        }
+        try {
+            final HTTPRequest httpRequest = JakartaServletUtils.createHTTPRequest(request);
+            getProtocolMessageLogger().trace("Inbound request {}", RequestUtil.toString(httpRequest, objectMapper));
+            final URI uri = httpRequest.getURI();
+            if (uri == null) {
+                throw new MessageDecodingException("Could not parse request URI");
+            }
+            final String contentType = request.getContentType();
+            if ("application/entity-statement+jwt".equals(contentType)) {
+                final EntityStatement entityConfiguration = deserializeEntityStatement(httpRequest.getQuery());
+                if (entityConfiguration == null) {
+                    throw new MessageDecodingException("Could not deserialize entity configuration");
+                }
+                log.trace("Entity configuration claims set: {}", entityConfiguration.getClaimsSet());
+                return new ExplicitClientRegistrationRequest(uri, entityConfiguration);
+            } 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 = strings.stream()
+                        .map(string -> deserializeEntityStatement(string))
+                        .filter(Objects::nonNull)
+                        .toList();
+                if (trustChain == null || trustChain.isEmpty()) {
+                    throw new MessageDecodingException("Could not deserialize trust chain");
+                }
+                return new ExplicitClientRegistrationRequest(uri, trustChain);
+            } else {
+                log.warn("Unexpected content type {}", contentType);
+                throw new MessageDecodingException("Unexpected content type: " + contentType);
+            }
+        } catch (final IOException e) {
+            log.error("Could not create HTTP request from the request", e);
+            throw new MessageDecodingException(e);
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected String getMessageToLog(@Nullable final ExplicitClientRegistrationRequest message) {
+        return message == null ? null : MoreObjects.toStringHelper(this).omitNullValues()
+                .add("entityConfiguration", getSerializedEntityStatement(message.getEntityConfiguration()))
+                .add("trustChain", getSerializedTrustChain(message.getTrustChain()))
+                .add("endpointURI", getEndpointURI(message))
+                .toString();
+    }
+
+    @Nullable protected EntityStatement deserializeEntityStatement(@Nullable final String serialized) {
+        try {
+            if (serialized != null) {
+                return EntityStatement.parse(serialized);
+            }
+        } catch (final ParseException e) {
+            log.trace("Could not construct entity statement from {}", serialized, e);
+        }
+        log.warn("Could not deserialize entity statement {}", serialized);
+        return null;
+    }
+
+    @Nullable
+    protected String getSerializedTrustChain(@Nullable final List<EntityStatement> trustChain) {
+        return trustChain == null ? null :
+            String.join(",", trustChain.stream().map(es -> es.getSignedStatement().serialize()).toList());
+    }
+
+    @Nullable
+    protected String getSerializedEntityStatement(@Nullable final EntityStatement entityStatement) {
+        return entityStatement == null ? null : entityStatement.getSignedStatement().serialize();
+    }
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/messaging/impl/ExplicitClientRegistrationRequest.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/messaging/impl/ExplicitClientRegistrationRequest.java
new file mode 100644
index 00000000..5a9d9787
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/messaging/impl/ExplicitClientRegistrationRequest.java
@@ -0,0 +1,100 @@
+/*
+ * 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.messaging.impl;
+
+import java.net.URI;
+import java.util.List;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import com.nimbusds.oauth2.sdk.Request;
+import com.nimbusds.oauth2.sdk.http.HTTPRequest;
+import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
+
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.logic.Constraint;
+
+/**
+ * Request message to the OpenID federation explicit registration endpoint.
+ */
+public class ExplicitClientRegistrationRequest implements Request {
+
+    /** The endpoint URI of the request. */
+    @Nonnull private final URI endpointUri;
+
+    /** The entity configuration from the request. */
+    @Nullable private final EntityStatement entityConfiguration;
+
+    /** The trust chain from the request. */
+    @Nullable private final List<EntityStatement> trustChain;
+
+    /**
+     * Constructor.
+     *
+     * @param uri endpoint URI
+     * @param configuration client configuration
+     */
+    public ExplicitClientRegistrationRequest(@Nonnull final URI uri, @Nonnull final EntityStatement configuration) {
+        endpointUri = Constraint.isNotNull(uri, "Endpoint URI cannot be null");
+        entityConfiguration = Constraint.isNotNull(configuration, "Entity configuration cannot be null");
+        trustChain = null;
+    }
+
+    /**
+     * Constructor.
+     *
+     * @param uri endpoint URI
+     * @param chain trust chain
+     */
+    public ExplicitClientRegistrationRequest(@Nonnull final URI uri,
+            @Nonnull @NotEmpty final List<EntityStatement> chain) {
+        endpointUri = Constraint.isNotNull(uri, "Endpoint URI cannot be null");
+        Constraint.isNotEmpty(chain, "Trust chain cannot be empty");
+        trustChain = chain;
+        entityConfiguration = null;
+    }
+
+    /** {@inheritDoc} */
+    @Override @Nonnull
+    public URI getEndpointURI() {
+        return endpointUri;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public HTTPRequest toHTTPRequest() {
+        //TODO
+        return null;
+    }
+
+    /**
+     * Get the entity configuration from the request.
+     * 
+     * @return entity configuration
+     */
+    @Nullable public EntityStatement getEntityConfiguration() {
+        return entityConfiguration;
+    }
+
+    /**
+     * Get the trust chain from the request.
+     * 
+     * @return trust chain
+     */
+    @Nullable public List<EntityStatement> getTrustChain() {
+        return trustChain;
+    }
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/messaging/impl/ExplicitClientRegistrationResponse.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/messaging/impl/ExplicitClientRegistrationResponse.java
new file mode 100644
index 00000000..827e6d5d
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/messaging/impl/ExplicitClientRegistrationResponse.java
@@ -0,0 +1,97 @@
+/*
+ * 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.messaging.impl;
+
+import javax.annotation.Nonnull;
+
+import com.nimbusds.jwt.SignedJWT;
+import com.nimbusds.oauth2.sdk.ParseException;
+import com.nimbusds.oauth2.sdk.Response;
+import com.nimbusds.oauth2.sdk.http.HTTPResponse;
+import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
+
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.StringSupport;
+
+/**
+ * Response message to the OpenID federation explicit registration endpoint.
+ */
+public class ExplicitClientRegistrationResponse implements Response {
+
+    /** The entity statement included in the response. */
+    @Nonnull private final EntityStatement entityStatement;
+
+    /**
+     * 
+     * Constructor.
+     *
+     * @param statement entity statement
+     */
+    public ExplicitClientRegistrationResponse(@Nonnull final EntityStatement statement) {
+        entityStatement = Constraint.isNotNull(statement, "Entity statement cannot be null");
+    }
+
+    /**
+     * Get the entity statement included in the response.
+     *
+     * @return entity statement
+     */
+    @Nonnull public EntityStatement getEntityStatement() {
+        return entityStatement;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean indicatesSuccess() {
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public HTTPResponse toHTTPResponse() {
+        final HTTPResponse httpResponse = new HTTPResponse(HTTPResponse.SC_OK);
+        httpResponse.setEntityContentType(EntityStatement.CONTENT_TYPE);
+        httpResponse.setContent(entityStatement.getSignedStatement().serialize());
+        return httpResponse;
+    }
+
+    /**
+     * Parses a federation explicit registration success response from the given HTTP response.
+     *
+     * @param httpResponse the HTTP response
+     * @return explicit registration success response
+     * @throws ParseException if HTTP response could not be parsed into registration response
+     */
+    @Nonnull
+    public static ExplicitClientRegistrationResponse parse(@Nonnull final HTTPResponse httpResponse)
+        throws ParseException {
+        
+        httpResponse.ensureStatusCode(HTTPResponse.SC_OK);
+        httpResponse.ensureEntityContentType(EntityStatement.CONTENT_TYPE);
+        final String content = httpResponse.getContent();
+        
+        if (StringSupport.trimOrNull(content) == null) {
+            throw new ParseException("Message body is empty");
+        }
+
+        try {
+            final EntityStatement entityStatement = EntityStatement.parse(SignedJWT.parse(httpResponse.getContent()));
+            assert entityStatement != null;
+            return new ExplicitClientRegistrationResponse(entityStatement);
+        } catch (final java.text.ParseException e) {
+            throw new ParseException(e.getMessage(), e);
+        }
+    }
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/BuildEntityStatement.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/AbstractBuildEntityStatementAction.java
similarity index 65%
rename from idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/BuildEntityStatement.java
rename to idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/AbstractBuildEntityStatementAction.java
index a677aed5..b810b7dd 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/BuildEntityStatement.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/AbstractBuildEntityStatementAction.java
@@ -15,9 +15,7 @@
 package net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl;
 
 import java.text.ParseException;
-import java.time.Duration;
 import java.time.Instant;
-import java.util.ArrayList;
 import java.util.Date;
 import java.util.List;
 import java.util.Map;
@@ -27,32 +25,27 @@ import java.util.function.Function;
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.messaging.encoder.AbstractMessageEncoder;
+import org.opensaml.profile.action.ActionSupport;
 import org.opensaml.profile.action.EventIds;
 import org.opensaml.profile.context.ProfileRequestContext;
 import org.opensaml.profile.context.navigate.OutboundMessageContextLookup;
-import org.opensaml.security.credential.Credential;
 import org.slf4j.Logger;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
-import com.nimbusds.jose.jwk.JWK;
-import com.nimbusds.jose.jwk.JWKSet;
 import com.nimbusds.jwt.JWT;
 import com.nimbusds.jwt.JWTClaimsSet;
 import com.nimbusds.jwt.PlainJWT;
-import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
 
 import net.shibboleth.idp.plugin.oidc.op.encoding.impl.ResponseUtil;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.navigate.AuthorityHintsLookupFunction;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.navigate.EntityStatementClaimsSetManipulationStrategyLookupFunction;
-import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.navigate.EntityStatementLifetimeLookupFunction;
 import net.shibboleth.idp.profile.AbstractProfileAction;
 import net.shibboleth.idp.profile.IdPEventIds;
-import net.shibboleth.oidc.profile.config.navigate.JWTSignatureSigningConfigurationLookupFunction;
-import net.shibboleth.oidc.security.CredentialConversionUtil;
-import net.shibboleth.oidc.security.jose.SignatureSigningConfiguration;
 import net.shibboleth.profile.context.navigate.IssuerLookupFunction;
 import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
-import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
 import net.shibboleth.shared.component.ComponentInitializationException;
 import net.shibboleth.shared.logic.Constraint;
 import net.shibboleth.shared.logic.FunctionSupport;
@@ -60,44 +53,29 @@ import net.shibboleth.shared.primitive.LoggerFactory;
 import net.shibboleth.shared.security.IdentifierGenerationStrategy;
 import net.shibboleth.shared.security.impl.SecureRandomIdentifierGenerationStrategy;
 
-import org.opensaml.messaging.context.navigate.ChildContextLookup;
-import org.opensaml.messaging.encoder.AbstractMessageEncoder;
-import org.opensaml.profile.action.ActionSupport;
-
 /**
- * Action that creates an Entity Statement, and stores it to an {@link EntityStatementContext}.
- *
- * @event {@link EventIds#PROCEED_EVENT_ID}
- * @event {@link EventIds#INVALID_PROFILE_CTX}
- * @event {@link EventIds#IO_ERROR}
- * @event {@link IdPEventIds#INVALID_PROFILE_CONFIG}
- * 
- * @since 4.3.0
+ * Abstract action used by actions that build {@link EntityStatement}s.
  */
-public class BuildEntityStatement extends AbstractProfileAction {
+public abstract class AbstractBuildEntityStatementAction extends AbstractProfileAction {
 
     /** Class logger. */
-    @Nonnull private Logger log = LoggerFactory.getLogger(BuildEntityStatement.class);
+    @Nonnull private Logger log = LoggerFactory.getLogger(AbstractBuildEntityStatementAction.class);
 
     /** Used to log protocol messages. */
-    @Nonnull private Logger protocolMessageLog =
+    @Nonnull protected Logger protocolMessageLog =
             LoggerFactory.getLogger(AbstractMessageEncoder.BASE_PROTOCOL_MESSAGE_LOGGER_CATEGORY + ".OIDFED");
 
-    /** Strategy used to obtain the response issuer value. */
+    /** Strategy used to obtain the issuer value. */
     @Nonnull private Function<ProfileRequestContext,String> issuerLookupStrategy;
-    
-    /** Strategy used to obtain the entity statement lifetime. */
-    @Nonnull private Function<ProfileRequestContext,Duration> entityStatementLifetimeLookupStrategy;
+
+    /** Strategy used to obtain the subject value. */
+    @Nonnull private Function<ProfileRequestContext,String> subjectLookupStrategy;
 
     /** Strategy used to locate the {@link IdentifierGenerationStrategy} to use. */
     @Nonnull private Function<ProfileRequestContext,IdentifierGenerationStrategy> idGeneratorLookupStrategy;
 
-    /** Strategy used to create the subcontext to hold the statement. */
-    @Nonnull private Function<ProfileRequestContext,EntityStatementContext> entityStatementContextCreationStrategy;
-
-    /** Strategy used to locate the {@link SignatureSigningConfiguration}s to fetch JWK set from. */
-    @Nonnull private
-    Function<ProfileRequestContext,List<SignatureSigningConfiguration>> signingConfigurationsLookupStrategy;
+    /** Strategy used to locate the subcontext to hold the statement. */
+    @Nonnull private Function<ProfileRequestContext,EntityStatementContext> entityStatementContextLookupStrategy;
 
     /** Strategy used to locate authority hints. */
     @Nonnull private Function<ProfileRequestContext,List<String>> authorityHintsLookupStrategy;
@@ -114,47 +92,30 @@ public class BuildEntityStatement extends AbstractProfileAction {
     @NonnullAfterInit private ObjectMapper objectMapper;
 
     /** The generator to use. */
-    @Nullable private IdentifierGenerationStrategy idGenerator;
+    @NonnullBeforeExec protected IdentifierGenerationStrategy idGenerator;
     
     /** Entity statement context. */
-    @Nullable private EntityStatementContext entityStatementCtx;
-
-    /** OIDC provider metadata to publish. */
-    @Nullable private OIDCProviderMetadata metadata;
+    @NonnullBeforeExec protected EntityStatementContext entityStatementCtx;
 
     /** Constructor. */
-    public BuildEntityStatement() {
-        entityStatementLifetimeLookupStrategy = new EntityStatementLifetimeLookupFunction();
+    public AbstractBuildEntityStatementAction() {
         issuerLookupStrategy = new IssuerLookupFunction();
+        subjectLookupStrategy = new IssuerLookupFunction();
         
         idGeneratorLookupStrategy = FunctionSupport.constant(new SecureRandomIdentifierGenerationStrategy());
 
-        final Function<ProfileRequestContext,EntityStatementContext> esccs =
+        final Function<ProfileRequestContext,EntityStatementContext> escls =
                 new ChildContextLookup<>(EntityStatementContext.class, true).compose(
                         new OutboundMessageContextLookup());
-        assert esccs != null;
-        entityStatementContextCreationStrategy = esccs; 
+        assert escls != null;
+        entityStatementContextLookupStrategy = escls; 
 
-        signingConfigurationsLookupStrategy = new JWTSignatureSigningConfigurationLookupFunction();
         authorityHintsLookupStrategy = new AuthorityHintsLookupFunction();
 
         entityStatementClaimsSetManipulationStrategyLookupStrategy =
                 new EntityStatementClaimsSetManipulationStrategyLookupFunction();
     }
 
-    /**
-     * Set the strategy used to obtain the entity statement lifetime.
-     * 
-     * @param strategy lookup strategy
-     */
-    public void setEntityStatementLifetimeLookupStrategy(
-            @Nonnull final Function<ProfileRequestContext,Duration> strategy) {
-        ifInitializedThrowUnmodifiabledComponentException();
-        
-        entityStatementLifetimeLookupStrategy =
-                Constraint.isNotNull(strategy, "Entity statement lifetime lookup strategy cannot be null");
-    }
-
     /**
      * Set the strategy used to locate the {@link IdentifierGenerationStrategy} to use.
      * 
@@ -178,18 +139,29 @@ public class BuildEntityStatement extends AbstractProfileAction {
         
         issuerLookupStrategy = Constraint.isNotNull(strategy, "Issuer lookup strategy cannot be null");
     }
-    
+
+    /**
+     * Set the strategy used to locate the subject value to use.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setSubjectLookupStrategy(@Nonnull final Function<ProfileRequestContext,String> strategy) {
+        ifInitializedThrowUnmodifiabledComponentException();
+        
+        subjectLookupStrategy = Constraint.isNotNull(strategy, "Subject lookup strategy cannot be null");
+    }
+
     /**
-     * Set the strategy used to create the {@link EntityStatementContext} to use.
+     * Set the strategy used to lookup the {@link EntityStatementContext} to use.
      * 
-     * @param strategy creation strategy
+     * @param strategy lookup strategy
      */
-    public void setEntityStatementContextCreationStrategy(
+    public void setEntityStatementContextLookupStrategy(
             @Nonnull final Function<ProfileRequestContext,EntityStatementContext> strategy) {
         ifInitializedThrowUnmodifiabledComponentException();
         
-        entityStatementContextCreationStrategy =
-                Constraint.isNotNull(strategy, "EntityStatementContext creation strategy cannot be null");
+        entityStatementContextLookupStrategy =
+                Constraint.isNotNull(strategy, "EntityStatementContext lookup strategy cannot be null");
     }
     
     /**
@@ -240,30 +212,13 @@ public class BuildEntityStatement extends AbstractProfileAction {
             return false;
         }
 
-        entityStatementCtx = entityStatementContextCreationStrategy.apply(profileRequestContext);
+        entityStatementCtx = entityStatementContextLookupStrategy.apply(profileRequestContext);
         if (entityStatementCtx == null) {
-            log.error("{} Unable to create EntityStatementContext", getLogPrefix());
+            log.error("{} Unable to fetch EntityStatementContext", getLogPrefix());
             ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
             return false;
         }
 
-        assert entityStatementCtx != null;
-        metadata = entityStatementCtx.getOPMetadata();
-        if (metadata == null) {
-            log.error("{} Could not resolve provider metadata", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext, EventIds.IO_ERROR);
-            return false;
-        }
-
-        final Duration lifetime = entityStatementLifetimeLookupStrategy.apply(profileRequestContext);
-        if (lifetime == null) {
-            log.error("{} No lifetime supplied for entity statement", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_PROFILE_CONFIG);
-            return false;
-        }
-        assert entityStatementCtx != null;
-        entityStatementCtx.setLifetime(lifetime);
-
         manipulationStrategy =
                 entityStatementClaimsSetManipulationStrategyLookupStrategy.apply(profileRequestContext);
 
@@ -275,39 +230,21 @@ public class BuildEntityStatement extends AbstractProfileAction {
     protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
 
         final String issuer = issuerLookupStrategy.apply(profileRequestContext);
+        final String subject = subjectLookupStrategy.apply(profileRequestContext);
 
         final Instant now = Instant.now();
-        assert entityStatementCtx != null;
-        final Instant dateExp = now.plus(entityStatementCtx.getLifetime());
-        assert dateExp != null;
-
-        final List<SignatureSigningConfiguration> signingConfigurations =
-                signingConfigurationsLookupStrategy.apply(profileRequestContext);
-        if (signingConfigurations == null || signingConfigurations.isEmpty()) {
-            log.error("{} Could not fetch any signature signing configurations", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_PROFILE_CONFIG);
-            return;
-        }
 
-        final List<JWK> jwks = new ArrayList<>();
-        for (final SignatureSigningConfiguration signingConfiguration : signingConfigurations) {
-            for (final Credential credential : signingConfiguration.getSigningCredentials()) {
-                final JWK jwk = CredentialConversionUtil.credentialToKey(credential);
-                if (jwk != null) {
-                    jwks.add(jwk);
-                    log.debug("{} Included {} to the keyset", getLogPrefix(), jwk.toJSONString());
-                }
-            }
-        }
-        final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
+        final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder()
                 .issuer(issuer)
-                .subject(issuer)
+                .subject(subject)
                 .issueTime(Date.from(now))
-                .expirationTime(Date.from(dateExp))
-                .claim("jwks", new JWKSet(jwks).toJSONObject(true))
-                .claim("authority_hints", authorityHintsLookupStrategy.apply(profileRequestContext))
-                .claim("metadata", buildOpenIDProviderClaim())
-                .build();
+                .claim("authority_hints", authorityHintsLookupStrategy.apply(profileRequestContext));
+        assert builder != null;
+        if (!populateClaimsSetBuilder(builder, profileRequestContext)) {
+            return;
+        }
+        final JWTClaimsSet claimsSet = builder.build();
+
         assert claimsSet != null;
         if (manipulationStrategy != null) {
             log.debug("{} Manipulation strategy has been set, applying it to the claims set {}", getLogPrefix(),
@@ -337,11 +274,24 @@ public class BuildEntityStatement extends AbstractProfileAction {
         logAndConstructEntityStatement(claimsSet);
     }
 
-    @Nonnull protected Map<String, Object> buildOpenIDProviderClaim() {
-        assert metadata != null;
-        return CollectionSupport.singletonMap("openid_provider", metadata.toJSONObject());
-    }
+    /**
+     * Populates the claims set builder with claims specific to the action extending this abstract action. If any
+     * problem occures during population, the profile request context should be populated with an appropriate
+     * event.
+     * 
+     * @param builder the claims set builder
+     * @param profileRequestContext profile request context
+     * @return true if population was successful, false otherwise
+     */
+    protected abstract boolean populateClaimsSetBuilder(@Nonnull final JWTClaimsSet.Builder builder,
+            @Nonnull final ProfileRequestContext profileRequestContext);
 
+    /**
+     * Logs the entity statement contents via protocol message logger and constructs a plain (i.e. non-signed) JWT out
+     * of it and includes it to the {@link EntityStatementContext#setJWT(JWT)}.
+     * 
+     * @param claimsSet the claims set
+     */
     protected void logAndConstructEntityStatement(@Nonnull final JWTClaimsSet claimsSet) {
         log.trace("{} Building JWT from the claims set {}", getLogPrefix(), claimsSet);
         final JWT jwt = new PlainJWT(claimsSet);
@@ -355,5 +305,4 @@ public class BuildEntityStatement extends AbstractProfileAction {
         assert entityStatementCtx != null;
         entityStatementCtx.setJWT(jwt);
     }
-        
-}
\ No newline at end of file
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/BuildEntityConfiguration.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/BuildEntityConfiguration.java
new file mode 100644
index 00000000..21a4dd2d
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/BuildEntityConfiguration.java
@@ -0,0 +1,148 @@
+/*
+ * 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.time.Duration;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.security.credential.Credential;
+import org.slf4j.Logger;
+
+import com.nimbusds.jose.jwk.JWK;
+import com.nimbusds.jose.jwk.JWKSet;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
+
+import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.navigate.EntityStatementLifetimeLookupFunction;
+import net.shibboleth.idp.profile.IdPEventIds;
+import net.shibboleth.oidc.profile.config.navigate.JWTSignatureSigningConfigurationLookupFunction;
+import net.shibboleth.oidc.security.CredentialConversionUtil;
+import net.shibboleth.oidc.security.jose.SignatureSigningConfiguration;
+import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+import org.opensaml.profile.action.ActionSupport;
+
+/**
+ * Action that creates an Entity Statement, and stores it to an {@link EntityStatementContext}.
+ *
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#INVALID_PROFILE_CTX}
+ * @event {@link EventIds#IO_ERROR}
+ * @event {@link IdPEventIds#INVALID_PROFILE_CONFIG}
+ * 
+ * @since 4.3.0
+ */
+public class BuildEntityConfiguration extends AbstractBuildEntityStatementAction {
+
+    /** Class logger. */
+    @Nonnull private Logger log = LoggerFactory.getLogger(BuildEntityConfiguration.class);
+
+    /** Strategy used to locate the {@link SignatureSigningConfiguration}s to fetch JWK set from. */
+    @Nonnull private
+    Function<ProfileRequestContext,List<SignatureSigningConfiguration>> signingConfigurationsLookupStrategy;
+
+    /** Strategy used to obtain the entity statement lifetime. */
+    @Nonnull private Function<ProfileRequestContext,Duration> entityConfigurationLifetimeLookupStrategy;
+
+    /** OIDC provider metadata to publish. */
+    @NonnullBeforeExec private OIDCProviderMetadata metadata;
+
+    /** Constructor. */
+    public BuildEntityConfiguration() {
+        signingConfigurationsLookupStrategy = new JWTSignatureSigningConfigurationLookupFunction();
+        entityConfigurationLifetimeLookupStrategy = new EntityStatementLifetimeLookupFunction();
+    }
+
+    /**
+     * Set the strategy used to obtain the entity configuration lifetime.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setEntityConfigurationLifetimeLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,Duration> strategy) {
+        ifInitializedThrowUnmodifiabledComponentException();
+        
+        entityConfigurationLifetimeLookupStrategy =
+                Constraint.isNotNull(strategy, "Entity configuration lifetime lookup strategy cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        if (!super.doPreExecute(profileRequestContext)) {
+            return false;
+        }
+
+        metadata = entityStatementCtx.getOPMetadata();
+        if (metadata == null) {
+            log.error("{} Could not resolve provider metadata", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return false;
+        }
+
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected boolean populateClaimsSetBuilder(@Nonnull final JWTClaimsSet.Builder builder,
+            @Nonnull final ProfileRequestContext profileRequestContext) {
+        final List<SignatureSigningConfiguration> signingConfigurations =
+                signingConfigurationsLookupStrategy.apply(profileRequestContext);
+        if (signingConfigurations == null || signingConfigurations.isEmpty()) {
+            log.error("{} Could not fetch any signature signing configurations", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_PROFILE_CONFIG);
+            return false;
+        }
+
+        final List<JWK> jwks = new ArrayList<>();
+        for (final SignatureSigningConfiguration signingConfiguration : signingConfigurations) {
+            for (final Credential credential : signingConfiguration.getSigningCredentials()) {
+                final JWK jwk = CredentialConversionUtil.credentialToKey(credential);
+                if (jwk != null) {
+                    jwks.add(jwk);
+                    log.debug("{} Included {} to the keyset", getLogPrefix(), jwk.toJSONString());
+                }
+            }
+        }
+
+        final Duration lifetime = entityConfigurationLifetimeLookupStrategy.apply(profileRequestContext);
+        if (lifetime == null) {
+            log.error("{} No lifetime supplied for entity statement", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_PROFILE_CONFIG);
+            return false;
+        }
+        final Instant now = Instant.now();
+        final Instant dateExp = now.plus(lifetime);
+        assert dateExp != null;
+
+        builder.expirationTime(Date.from(dateExp));
+        builder.claim("jwks", new JWKSet(jwks).toJSONObject(true));
+        builder.claim("metadata", CollectionSupport.singletonMap("openid_provider", metadata.toJSONObject()));
+        return true;
+   }
+
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/BuildExplicitRegistrationResponse.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/BuildExplicitRegistrationResponse.java
new file mode 100644
index 00000000..368ea369
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/BuildExplicitRegistrationResponse.java
@@ -0,0 +1,172 @@
+/*
+ * 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.time.Instant;
+import java.util.Date;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
+import org.opensaml.profile.context.navigate.OutboundMessageContextLookup;
+import org.slf4j.Logger;
+
+import com.nimbusds.jose.jwk.JWKSet;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformation;
+
+import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCClientRegistrationResponseContext;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.navigate.ExplicitClientRegistrationRequestJWKSetLookupFunction;
+import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * An action that uses the information from {@link OIDCClientRegistrationResponseContext} attached to the message
+ * context for creating a new JWT to be used for creating a response to OpenID Federation Explicit Registration.
+ */
+public class BuildExplicitRegistrationResponse extends AbstractBuildEntityStatementAction {
+
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(BuildExplicitRegistrationResponse.class);
+
+    /**
+     * Strategy used to locate the {@link OIDCClientRegistrationResponseContext}.
+     */
+    @Nonnull private Function<ProfileRequestContext,OIDCClientRegistrationResponseContext> oidcResponseContextLookupStrategy;
+
+    /** Strategy used to locate the JWK set to be included in the response entity statement. */
+    @Nonnull private Function<ProfileRequestContext,JWKSet> jwkSetLookupStrategy;
+
+    /** Strategy used to lookup the trust chain context. */
+    @Nonnull private Function<ProfileRequestContext, RelyingPartyTrustChainContext> trustChainContextLookupStrategy;
+
+    /** The {@link OIDCClientRegistrationResponseContext} to operate on. */
+    @NonnullBeforeExec private OIDCClientRegistrationResponseContext oidcResponseContext;
+
+    /** Trust chain context to operate on. */
+    @NonnullBeforeExec private RelyingPartyTrustChainContext trustChainContext;
+
+    /** Constructor. */
+    public BuildExplicitRegistrationResponse() {
+        final Function<ProfileRequestContext, OIDCClientRegistrationResponseContext> ocrrls =
+                new ChildContextLookup<>(OIDCClientRegistrationResponseContext.class)
+                    .compose(new OutboundMessageContextLookup());
+        assert ocrrls != null;
+        oidcResponseContextLookupStrategy = ocrrls;
+        jwkSetLookupStrategy = new ExplicitClientRegistrationRequestJWKSetLookupFunction();
+        final Function<ProfileRequestContext, RelyingPartyTrustChainContext> tcls =
+                new ChildContextLookup<>(RelyingPartyTrustChainContext.class).compose(
+                        new InboundMessageContextLookup());
+        assert tcls != null;
+        trustChainContextLookupStrategy = tcls;
+    }
+    
+    /**
+     * Set the strategy used to locate the {@link OIDCClientRegistrationResponseContext}.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setOidcResponseContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,OIDCClientRegistrationResponseContext> strategy) {
+        ifInitializedThrowUnmodifiabledComponentException();
+        
+        oidcResponseContextLookupStrategy = Constraint.isNotNull(strategy,
+                "OIDCClientRegistrationResponseContext lookup strategy cannot be null");
+    }
+
+    /**
+     * Set the strategy used to locate the JWK set to be included in the response entity statement.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setJwkSetLookupStrategy(@Nonnull final Function<ProfileRequestContext,JWKSet> strategy) {
+        checkSetterPreconditions();
+        jwkSetLookupStrategy = Constraint.isNotNull(strategy, "JWK set loookup strategy cannot be null");
+    }
+
+    /**
+     * 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");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        if (!super.doPreExecute(profileRequestContext)) {
+            return false;
+        }
+
+        oidcResponseContext = oidcResponseContextLookupStrategy.apply(profileRequestContext);
+        if (oidcResponseContext == null) {
+            log.debug("{} No OIDCClientRegistrationResponseContext associated with this profile request",
+                    getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return false;                        
+        }
+
+        trustChainContext = trustChainContextLookupStrategy.apply(profileRequestContext);
+        if (trustChainContext == null || trustChainContext.getPolicyCompliantTrustChains() == null) {
+            log.error("{} Unable to locate policy-compliant trust chains", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
+            return false;
+        }
+
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected boolean populateClaimsSetBuilder(@Nonnull final JWTClaimsSet.Builder builder,
+            @Nonnull final ProfileRequestContext profileRequestContext) {
+        final OIDCClientInformation clientInformation = oidcResponseContext.getClientInformation();
+        if (clientInformation == null) {
+            log.debug("{} No client information set in the OIDC response context", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return false;                        
+        }
+        builder.claim("metadata",
+                CollectionSupport.singletonMap("openid_relying_party", clientInformation.toJSONObject()));
+        final JWKSet jwkSet = jwkSetLookupStrategy.apply(profileRequestContext);
+        if (jwkSet == null) {
+            log.error("{} Coud not resolve JWK set to be included in the response statement", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
+            return false;
+        }
+        builder.claim("jwks", jwkSet.toJSONObject(true));
+        final Instant expirationTime = trustChainContext.getSelectedMetadataExpiration();
+        if (expirationTime == null) {
+            log.error("{} Coud not resolve expiration time from the selected trust chain context", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
+            return false;
+        }
+        builder.expirationTime(Date.from(expirationTime));
+        return true;
+    }
+
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/FormExplicitRegistrationResponse.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/FormExplicitRegistrationResponse.java
new file mode 100644
index 00000000..425971d1
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/FormExplicitRegistrationResponse.java
@@ -0,0 +1,118 @@
+/*
+ * 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.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.OutboundMessageContextLookup;
+import org.slf4j.Logger;
+
+import com.nimbusds.jwt.SignedJWT;
+import com.nimbusds.oauth2.sdk.ParseException;
+import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
+
+import net.shibboleth.idp.plugin.oidc.op.oidfed.messaging.impl.ExplicitClientRegistrationResponse;
+import net.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * This action builds a response for the OpenID federation explicit registration request. The response contains an
+ * {@link EntityStatement}.
+ * 
+ * @since 4.3.0
+ */
+public class FormExplicitRegistrationResponse extends AbstractProfileAction {
+
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(FormExplicitRegistrationResponse.class);
+
+    /** Strategy used to locate the subcontext to hold the statement. */
+    @Nonnull private Function<ProfileRequestContext,EntityStatementContext> entityStatementContextLookupStrategy;
+
+    /** JWT used to build entity statement. */
+    @Nullable private SignedJWT jwt;
+
+    /**
+     * Constructor.
+     */
+    public FormExplicitRegistrationResponse() {
+        final Function<ProfileRequestContext,EntityStatementContext> escls =
+                new ChildContextLookup<>(EntityStatementContext.class).compose(
+                        new OutboundMessageContextLookup());
+        assert escls != null;
+        entityStatementContextLookupStrategy = escls;
+    }
+
+    /**
+     * Set the strategy used to locate the subcontext to hold the statement
+     * 
+     * @param strategy What to set.
+     */
+    public void setEntityStatementContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,EntityStatementContext> strategy) {
+        checkSetterPreconditions();
+        entityStatementContextLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null!");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+        if (!super.doPreExecute(profileRequestContext)) {
+            return false;
+        }
+        final EntityStatementContext entityStatementContext =
+                entityStatementContextLookupStrategy.apply(profileRequestContext);
+        if (entityStatementContext == null) {
+            log.error("{} Could not resolve entity statement context", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return false;
+        }
+        if (entityStatementContext.getJWT() instanceof SignedJWT signedJwt) {
+            jwt = signedJwt;
+        } else {
+            log.error("{} No signed JWT found from the entity statement context", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return false;
+        }
+
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        final EntityStatement entityStatement;
+        try {
+            entityStatement = EntityStatement.parse(jwt);
+        } catch (ParseException e) {
+            log.error("{} Could not parse entity statement from JWT", getLogPrefix(), e);
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return;
+        }
+        assert entityStatement != null;
+        final ExplicitClientRegistrationResponse response = new ExplicitClientRegistrationResponse(entityStatement);
+        log.debug("{} Response message set to the outbound message context", getLogPrefix());
+        profileRequestContext.ensureOutboundMessageContext().setMessage(response);
+    }
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/DefaultSelectedTrustChainMetadataLookupStrategy.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/DefaultSelectedTrustChainMetadataLookupStrategy.java
new file mode 100644
index 00000000..6780b041
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/DefaultSelectedTrustChainMetadataLookupStrategy.java
@@ -0,0 +1,76 @@
+/*
+ * 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 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 com.nimbusds.openid.connect.sdk.rp.OIDCClientMetadata;
+
+import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.RelyingPartyTrustChainContext;
+import net.shibboleth.shared.annotation.ParameterName;
+import net.shibboleth.shared.logic.Constraint;
+
+/**
+ * Default strategy for looking up the metadata the selected trust chain. The selected trust chain is fetched via
+ * {@link RelyingPartyTrustChainContext#getSelectedTrustChain()}.
+ */
+public class DefaultSelectedTrustChainMetadataLookupStrategy
+    implements Function<ProfileRequestContext,OIDCClientMetadata> {
+
+    /** Strategy used to locate the trust chain context. */
+    @Nonnull private Function<ProfileRequestContext, RelyingPartyTrustChainContext> trustChainContextLookupStrategy;
+
+    /**
+     * Constructor.
+     */
+    public DefaultSelectedTrustChainMetadataLookupStrategy() {
+        final Function<ProfileRequestContext, RelyingPartyTrustChainContext> tcls =
+                new ChildContextLookup<>(RelyingPartyTrustChainContext.class, true).compose(
+                        new InboundMessageContextLookup());
+        assert tcls != null;
+        trustChainContextLookupStrategy = tcls;
+    }
+
+    /**
+     * Constructor.
+     *
+     * @param trustChainContextStrategy strategy used to locate the trust chain context
+     * @param trustChainIDsStrategy strategy used to get entity IDs from a trust chain
+     */
+    public DefaultSelectedTrustChainMetadataLookupStrategy(
+            @Nonnull @ParameterName(name = "trustChainContextLookupStrategy")
+            final Function<ProfileRequestContext, RelyingPartyTrustChainContext> trustChainContextStrategy) {
+        trustChainContextLookupStrategy =
+                Constraint.isNotNull(trustChainContextStrategy, "TrustChainContextLookupStrategy cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    @Override @Nullable
+    public OIDCClientMetadata apply(@Nullable final ProfileRequestContext input) {
+        return Optional.ofNullable(trustChainContextLookupStrategy.apply(input))
+                .map(trustChainContext -> trustChainContext.getSelectedTrustChain())
+                .map(pair -> pair.getSecond())
+                .map(clientInfo -> clientInfo.getOIDCMetadata())
+                .orElse(null);
+    }
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/ExplicitClientRegistrationRequestClientIDLookupFunction.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/ExplicitClientRegistrationRequestClientIDLookupFunction.java
new file mode 100644
index 00000000..ffb28564
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/ExplicitClientRegistrationRequestClientIDLookupFunction.java
@@ -0,0 +1,68 @@
+/*
+ * 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.List;
+import java.util.Optional;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.ThreadSafe;
+
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.messaging.context.navigate.ContextDataLookupFunction;
+
+import com.nimbusds.oauth2.sdk.id.ClientID;
+import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
+
+import net.shibboleth.idp.plugin.oidc.op.oidfed.messaging.impl.ExplicitClientRegistrationRequest;
+
+/**
+ * A function that returns client from the entity configuration in the explicit registration request.
+ * 
+ * @since 4.3.0
+ */
+ at ThreadSafe
+public class ExplicitClientRegistrationRequestClientIDLookupFunction
+    implements ContextDataLookupFunction<MessageContext, ClientID> {
+
+    /** {@inheritDoc} */
+    @Nullable
+    public ClientID apply(@Nullable final MessageContext input) {
+        return Optional.ofNullable(input)
+                .map(messageContext -> messageContext.getMessage())
+                .filter(ExplicitClientRegistrationRequest.class::isInstance)
+                .map(ExplicitClientRegistrationRequest.class::cast)
+                .map(request -> getClientID(request))
+                .orElse(null);
+    }
+    
+    /**
+     * Get the client ID from the explicit registration request.
+     * 
+     * @param request the explicit registration request
+     * @return the client ID, or null if it cannot be parsed
+     */
+    @Nullable protected ClientID getClientID(@Nullable final ExplicitClientRegistrationRequest request) {
+        if (request == null) {
+            return null;
+        }
+        final EntityStatement entityConfiguration = request.getEntityConfiguration();
+        if (entityConfiguration != null) {
+            return new ClientID(entityConfiguration.getEntityID());
+        }
+        final List<EntityStatement> trustChain = request.getTrustChain();
+        return trustChain != null && !trustChain.isEmpty() ? new ClientID(trustChain.get(0).getEntityID()) : null;
+    }
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/ExplicitClientRegistrationRequestJWKSetLookupFunction.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/ExplicitClientRegistrationRequestJWKSetLookupFunction.java
new file mode 100644
index 00000000..d574b93e
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/ExplicitClientRegistrationRequestJWKSetLookupFunction.java
@@ -0,0 +1,69 @@
+/*
+ * 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.List;
+import java.util.Optional;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.ThreadSafe;
+
+import org.opensaml.messaging.context.navigate.ContextDataLookupFunction;
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import com.nimbusds.jose.jwk.JWKSet;
+import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
+
+import net.shibboleth.idp.plugin.oidc.op.oidfed.messaging.impl.ExplicitClientRegistrationRequest;
+
+/**
+ * A function that returns {@link JWKSet} from the entity configuration in the explicit registration request.
+ * 
+ * @since 4.3.0
+ */
+ at ThreadSafe
+public class ExplicitClientRegistrationRequestJWKSetLookupFunction
+    implements ContextDataLookupFunction<ProfileRequestContext, JWKSet> {
+
+    /** {@inheritDoc} */
+    @Nullable
+    public JWKSet apply(@Nullable final ProfileRequestContext input) {
+        return Optional.ofNullable(input)
+                .map(profileRequesContext -> profileRequesContext.getInboundMessageContext())
+                .map(messageContext -> messageContext.getMessage())
+                .filter(ExplicitClientRegistrationRequest.class::isInstance)
+                .map(ExplicitClientRegistrationRequest.class::cast)
+                .map(request -> getJWKSet(request))
+                .orElse(null);
+    }
+
+    /**
+     * Get the JSON Web Key Set from the explicit registration request.
+     * 
+     * @param request explicit registration request
+     * @return the JWKSet, or null if it cannot be parsed
+     */
+    @Nullable protected JWKSet getJWKSet(@Nullable final ExplicitClientRegistrationRequest request) {
+        if (request == null) {
+            return null;
+        }
+        final EntityStatement entityConfiguration = request.getEntityConfiguration();
+        if (entityConfiguration != null) {
+            return entityConfiguration.getClaimsSet().getJWKSet();
+        }
+        final List<EntityStatement> trustChain = request.getTrustChain();
+        return trustChain != null && !trustChain.isEmpty() ? trustChain.get(0).getClaimsSet().getJWKSet() : null;
+    }
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/LocalMetadataPolicyLookupFunction.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/LocalMetadataPolicyLookupFunction.java
index 54acc5dd..41cc47d8 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/LocalMetadataPolicyLookupFunction.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/LocalMetadataPolicyLookupFunction.java
@@ -23,12 +23,12 @@ import org.opensaml.profile.context.ProfileRequestContext;
 import net.shibboleth.profile.config.ProfileConfiguration;
 import net.shibboleth.profile.context.RelyingPartyContext;
 import net.shibboleth.profile.context.navigate.AbstractRelyingPartyLookupFunction;
-import net.shibboleth.idp.plugin.oidc.op.oidfed.config.OIDFederationAutomaticRegistrationConfiguration;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.config.OIDFederationRegistrationProfileConfiguration;
 import net.shibboleth.oidc.metadata.policy.MetadataPolicy;
 
 /**
  * A function that obtains
- * {@link OIDFederationAutomaticRegistrationConfiguration#getLocalMetadataPolicy(ProfileRequestContext)}.
+ * {@link OIDFederationRegistrationProfileConfiguration#getLocalMetadataPolicy(ProfileRequestContext)}.
  * 
  * <p>If a specific setting is unavailable, a null value is returned.</p>
  */
@@ -40,8 +40,8 @@ public class LocalMetadataPolicyLookupFunction extends AbstractRelyingPartyLooku
         final RelyingPartyContext rpc = getRelyingPartyContextLookupStrategy().apply(input);
         if (rpc != null) {
             final ProfileConfiguration pc = rpc.getProfileConfig();
-            if (pc instanceof OIDFederationAutomaticRegistrationConfiguration ofarc) {
-                return ofarc.getLocalMetadataPolicy(input);
+            if (pc instanceof OIDFederationRegistrationProfileConfiguration ofrpc) {
+                return ofrpc.getLocalMetadataPolicy(input);
             }
         }
         return null;
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/MandatoryTrustMarksLookupFunction.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/MandatoryTrustMarksLookupFunction.java
index 3a4b8f39..78542677 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/MandatoryTrustMarksLookupFunction.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/MandatoryTrustMarksLookupFunction.java
@@ -23,11 +23,11 @@ import org.opensaml.profile.context.ProfileRequestContext;
 import net.shibboleth.profile.config.ProfileConfiguration;
 import net.shibboleth.profile.context.RelyingPartyContext;
 import net.shibboleth.profile.context.navigate.AbstractRelyingPartyLookupFunction;
-import net.shibboleth.idp.plugin.oidc.op.oidfed.config.OIDFederationAutomaticRegistrationConfiguration;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.config.OIDFederationRegistrationProfileConfiguration;
 
 /**
  * A function that obtains
- * {@link OIDFederationAutomaticRegistrationConfiguration#getMandatoryTrustMarks(ProfileRequestContext)}.
+ * {@link OIDFederationRegistrationProfileConfiguration#getMandatoryTrustMarks(ProfileRequestContext)}.
  * 
  * <p>If a specific setting is unavailable, a null value is returned.</p>
  */
@@ -39,8 +39,8 @@ public class MandatoryTrustMarksLookupFunction extends AbstractRelyingPartyLooku
         final RelyingPartyContext rpc = getRelyingPartyContextLookupStrategy().apply(input);
         if (rpc != null) {
             final ProfileConfiguration pc = rpc.getProfileConfig();
-            if (pc instanceof OIDFederationAutomaticRegistrationConfiguration ofarc) {
-                return ofarc.getMandatoryTrustMarks(input);
+            if (pc instanceof OIDFederationRegistrationProfileConfiguration ofrpc) {
+                return ofrpc.getMandatoryTrustMarks(input);
             }
         }
         return null;
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/MaximumTrustMarkLifetimeLookupFunction.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/MaximumTrustMarkLifetimeLookupFunction.java
index c39ed5d7..99986547 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/MaximumTrustMarkLifetimeLookupFunction.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/MaximumTrustMarkLifetimeLookupFunction.java
@@ -23,11 +23,11 @@ import org.opensaml.profile.context.ProfileRequestContext;
 import net.shibboleth.profile.config.ProfileConfiguration;
 import net.shibboleth.profile.context.RelyingPartyContext;
 import net.shibboleth.profile.context.navigate.AbstractRelyingPartyLookupFunction;
-import net.shibboleth.idp.plugin.oidc.op.oidfed.config.OIDFederationAutomaticRegistrationConfiguration;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.config.OIDFederationRegistrationProfileConfiguration;
 
 /**
  * A function that returns
- * {@link OIDFederationAutomaticRegistrationConfiguration#getMaximumTrustMarkLifetime(ProfileRequestContext)} if such a
+ * {@link OIDFederationRegistrationProfileConfiguration#getMaximumTrustMarkLifetime(ProfileRequestContext)} if such a
  * profile is available from a {@link RelyingPartyContext} obtained via a lookup function, by default a child of the
  * {@link ProfileRequestContext}.
  * 
@@ -41,8 +41,8 @@ public class MaximumTrustMarkLifetimeLookupFunction extends AbstractRelyingParty
         final RelyingPartyContext rpc = getRelyingPartyContextLookupStrategy().apply(input);
         if (rpc != null) {
             final ProfileConfiguration pc = rpc.getProfileConfig();
-            if (pc instanceof OIDFederationAutomaticRegistrationConfiguration ofarc) {
-                return ofarc.getMaximumTrustMarkLifetime(input);
+            if (pc instanceof OIDFederationRegistrationProfileConfiguration ofrpc) {
+                return ofrpc.getMaximumTrustMarkLifetime(input);
             }
         }
         
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/AbstractOIDCClientRegistrationAction.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/AbstractOIDCClientRegistrationAction.java
new file mode 100644
index 00000000..8e53926f
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/AbstractOIDCClientRegistrationAction.java
@@ -0,0 +1,98 @@
+/*
+ * 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.profile.impl;
+
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.OutboundMessageContextLookup;
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCClientRegistrationResponseContext;
+import net.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * Abstract action for dynamic client registration actions dealing with {@link OIDCClientRegistrationResponseContext}.
+ */
+public abstract class AbstractOIDCClientRegistrationAction extends AbstractProfileAction {
+
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(AbstractOIDCClientRegistrationAction.class);
+
+    /** The strategy used to locate the {@link OIDCClientRegistrationResponseContext}. */
+    @Nonnull
+    private Function<ProfileRequestContext,OIDCClientRegistrationResponseContext> oidcResponseContextLookupStrategy;
+
+    /** The {@link OIDCClientRegistrationResponseContext} to operate on. */
+    @NonnullBeforeExec private OIDCClientRegistrationResponseContext oidcResponseContext;
+    
+    /** Constructor. */
+    public AbstractOIDCClientRegistrationAction() {
+        final Function<ProfileRequestContext, OIDCClientRegistrationResponseContext> ocrrls =
+                new ChildContextLookup<>(OIDCClientRegistrationResponseContext.class)
+                    .compose(new OutboundMessageContextLookup());
+        assert ocrrls != null;
+        oidcResponseContextLookupStrategy = ocrrls;
+    }
+
+    /**
+     * Set the strategy used to locate the {@link OIDCClientRegistrationResponseContext}.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setOidcResponseContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,OIDCClientRegistrationResponseContext> strategy) {
+        checkSetterPreconditions();
+        oidcResponseContextLookupStrategy = Constraint.isNotNull(strategy, 
+                "The output OIDCClientRegistrationResponseContext lookup strategy cannot be null");
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        
+        if (!super.doPreExecute(profileRequestContext)) {
+            return false;
+        }
+
+        oidcResponseContext = oidcResponseContextLookupStrategy.apply(profileRequestContext);
+        if (oidcResponseContext == null) {
+            log.debug("{} No OIDCClientRegistrationResponseContext associated with this profile request",
+                    getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
+            return false;                        
+        }
+
+        return true;
+    }
+
+    /**
+     * Get the {@link OIDCClientRegistrationResponseContext}. Cannot be null after
+     * {@link this#doPreExecute(ProfileRequestContext)} has returned true.
+     * 
+     * @return registration context
+     */
+    @NonnullBeforeExec protected OIDCClientRegistrationResponseContext getRegistrationContext() {
+        return oidcResponseContext;
+    }
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/BuildClientInformation.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/BuildClientInformation.java
index bf5a948a..8bdfad3f 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/BuildClientInformation.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/BuildClientInformation.java
@@ -16,12 +16,9 @@ package net.shibboleth.idp.plugin.oidc.op.profile.impl;
 
 import java.time.Instant;
 import java.util.Date;
-import java.util.function.Function;
 
 import javax.annotation.Nonnull;
 
-import org.opensaml.messaging.context.MessageContext;
-import org.opensaml.messaging.context.navigate.ChildContextLookup;
 import org.opensaml.profile.action.ActionSupport;
 import org.opensaml.profile.action.EventIds;
 import org.opensaml.profile.context.ProfileRequestContext;
@@ -32,12 +29,9 @@ import com.nimbusds.oauth2.sdk.auth.Secret;
 import com.nimbusds.oauth2.sdk.client.ClientInformationResponse;
 import com.nimbusds.oauth2.sdk.id.ClientID;
 import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformation;
-import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformationResponse;
 import com.nimbusds.openid.connect.sdk.rp.OIDCClientMetadata;
 
 import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCClientRegistrationResponseContext;
-import net.shibboleth.idp.profile.AbstractProfileAction;
-import net.shibboleth.shared.logic.Constraint;
 import net.shibboleth.shared.primitive.LoggerFactory;
 import net.shibboleth.shared.primitive.StringSupport;
 
@@ -45,70 +39,15 @@ import net.shibboleth.shared.primitive.StringSupport;
  * An action that uses the information from {@link OIDCClientRegistrationResponseContext} attached to the message
  * context for creating a new {@link ClientInformationResponse}. It will be set as the outbound message.
  */
-public class BuildClientInformation extends AbstractProfileAction {
+public class BuildClientInformation extends AbstractOIDCClientRegistrationAction {
 
     /** Class logger. */
     @Nonnull private final Logger log = LoggerFactory.getLogger(BuildClientInformation.class);
     
-    /**
-     * Strategy used to locate the {@link OIDCClientRegistrationResponseContext} associated with a given 
-     * {@link MessageContext}.
-     */
-    @Nonnull private Function<MessageContext,OIDCClientRegistrationResponseContext> oidcResponseContextLookupStrategy;
-
-    /** The {@link MessageContext} to operate on. */
-    private MessageContext messageContext;
-
-    /** The {@link OIDCClientRegistrationResponseContext} to operate on. */
-    private OIDCClientRegistrationResponseContext oidcResponseContext;
-
-    /** Constructor. */
-    public BuildClientInformation() {
-        oidcResponseContextLookupStrategy = new ChildContextLookup<>(OIDCClientRegistrationResponseContext.class);
-    }
-    
-    /**
-     * Set the strategy used to locate the {@link OIDCClientRegistrationResponseContext} associated with a given
-     * {@link MessageContext}.
-     * 
-     * @param strategy strategy used to locate the {@link OIDCClientRegistrationResponseContext} associated with a 
-     *         given {@link MessageContext}
-     */
-    public void setOidcResponseContextLookupStrategy(
-            @Nonnull final Function<MessageContext,OIDCClientRegistrationResponseContext> strategy) {
-        ifInitializedThrowUnmodifiabledComponentException();
-        
-        oidcResponseContextLookupStrategy = Constraint.isNotNull(strategy,
-                "OIDCClientRegistrationResponseContext lookup strategy cannot be null");
-    }
-    
-    /** {@inheritDoc} */
-    @Override
-    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
-        if (!super.doPreExecute(profileRequestContext)) {
-            return false;
-        }
-
-        messageContext = profileRequestContext.getOutboundMessageContext();
-        if (messageContext == null) {
-            log.error("{} No message context found", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
-            return false;
-        }
-
-        oidcResponseContext = oidcResponseContextLookupStrategy.apply(messageContext);
-        if (oidcResponseContext == null) {
-            log.error("{} No OIDC response context found", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
-            return false;
-        }
-
-        return true;
-    }
-
     /** {@inheritDoc} */
     @Override
     protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        final OIDCClientRegistrationResponseContext oidcResponseContext = getRegistrationContext();
 
         final String id = oidcResponseContext.getClientId();
         if (StringSupport.trimOrNull(id) == null) {
@@ -150,9 +89,7 @@ public class BuildClientInformation extends AbstractProfileAction {
         
         final OIDCClientInformation clientInformation = new OIDCClientInformation(clientId, new Date(), 
                 metadata, clientSecret);
-        final OIDCClientInformationResponse response = new OIDCClientInformationResponse(clientInformation, true);
-        messageContext.setMessage(response);
-        log.info("{} Client information successfully added to the outbound context", getLogPrefix());
-        
+        oidcResponseContext.setClientInformation(clientInformation);
+        log.info("{} Client information successfully populated to the response context", getLogPrefix());
     }    
 }
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/CheckRedirectURIs.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/CheckRedirectURIs.java
index 54fa8ca1..a5ff735a 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/CheckRedirectURIs.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/CheckRedirectURIs.java
@@ -21,6 +21,7 @@ import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 import java.util.Set;
+import java.util.function.Function;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -44,11 +45,12 @@ import com.fasterxml.jackson.databind.ObjectMapper;
 import com.nimbusds.oauth2.sdk.GrantType;
 import com.nimbusds.openid.connect.sdk.rp.ApplicationType;
 import com.nimbusds.openid.connect.sdk.rp.OIDCClientMetadata;
-import com.nimbusds.openid.connect.sdk.rp.OIDCClientRegistrationRequest;
 
+import net.shibboleth.idp.plugin.oidc.op.messaging.context.navigate.DefaultRequestedMetadataLookupFunction;
 import net.shibboleth.idp.profile.AbstractProfileAction;
 import net.shibboleth.oidc.profile.core.OidcEventIds;
 import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
 import net.shibboleth.shared.component.ComponentInitializationException;
 import net.shibboleth.shared.logic.Constraint;
 import net.shibboleth.shared.primitive.LoggerFactory;
@@ -73,9 +75,6 @@ public class CheckRedirectURIs extends AbstractProfileAction {
     @Nonnull
     private final Logger log = LoggerFactory.getLogger(CheckRedirectURIs.class);
     
-    /** The OIDCClientRegistrationRequest to check redirect URIs from. */
-    @Nullable private OIDCClientRegistrationRequest request;
-
     /** The {@link HttpClient} to use. */
     @NonnullAfterInit private HttpClient httpClient;
     
@@ -85,9 +84,16 @@ public class CheckRedirectURIs extends AbstractProfileAction {
     /** JSON object mapper. */
     @NonnullAfterInit private ObjectMapper objectMapper;
 
+    /** Lookup strategy for requested metadata */
+    @Nonnull private Function<ProfileRequestContext, OIDCClientMetadata> requestMetadataLookupStrategy;
+
+    /** Requested metadata to operate on. */
+    @NonnullBeforeExec private OIDCClientMetadata metadata;
+
     /** Constructor. */
     public CheckRedirectURIs() {
         super();
+        requestMetadataLookupStrategy = new DefaultRequestedMetadataLookupFunction();
     }
     
     /**
@@ -125,6 +131,18 @@ public class CheckRedirectURIs extends AbstractProfileAction {
         objectMapper = Constraint.isNotNull(mapper, "Object mapper cannot be null");
     }
 
+    /**
+     * Set the lookup strategy for requested metadata.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setRequestMetadataLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext, OIDCClientMetadata> strategy) {
+        checkSetterPreconditions();
+        
+        requestMetadataLookupStrategy =
+                Constraint.isNotNull(strategy, "Request metadata lookup strategy cannot be null");
+    }
     /** {@inheritDoc} */
     public void doInitialize() throws ComponentInitializationException {
         super.doInitialize();
@@ -150,13 +168,12 @@ public class CheckRedirectURIs extends AbstractProfileAction {
             ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
             return false;            
         }
-        final Object message = profileRequestContext.ensureInboundMessageContext().getMessage();
-        if (message == null || !(message instanceof OIDCClientRegistrationRequest)) {
-            log.debug("{} No inbound message associated with this profile request", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
-            return false;                        
+        metadata = requestMetadataLookupStrategy.apply(profileRequestContext);
+        if (metadata == null) {
+            log.warn("{} No client metadata found in the request", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MESSAGE);
+            return false;
         }
-        request = (OIDCClientRegistrationRequest) message;
         return true;
     }
 
@@ -165,13 +182,6 @@ public class CheckRedirectURIs extends AbstractProfileAction {
     /** {@inheritDoc} */
     @Override
     protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
-        assert request != null;
-        final OIDCClientMetadata metadata = request.getOIDCClientMetadata();
-        if (metadata == null) {
-            log.warn("{} No client metadata found in the request", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MESSAGE);
-            return;
-        }
         final Set<URI> redirectURIs = metadata.getRedirectionURIs();
         if (redirectURIs == null || redirectURIs.isEmpty()) {
             log.warn("{} No redirection URIs found in the request", getLogPrefix());
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/FormOutboundClientInformationResponseMessage.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/FormOutboundClientInformationResponseMessage.java
new file mode 100644
index 00000000..73151370
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/FormOutboundClientInformationResponseMessage.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.profile.impl;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformation;
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformationResponse;
+
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * Action that forms outbound dynamic client registration response.
+ */
+public class FormOutboundClientInformationResponseMessage extends AbstractOIDCClientRegistrationAction {
+
+    /** Class logger. */
+    @Nonnull private Logger log = LoggerFactory.getLogger(FormOutboundClientInformationResponseMessage.class);
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        final OIDCClientInformation clientInformation = getRegistrationContext().getClientInformation();
+        if (clientInformation == null) {
+            log.error("{} Could not find client information from context data", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return;
+        }
+        final OIDCClientInformationResponse response = new OIDCClientInformationResponse(clientInformation, true);
+        profileRequestContext.ensureOutboundMessageContext().setMessage(response);
+        log.info("{} Client information successfully added to the outbound context", getLogPrefix());
+    }
+
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/StoreClientInformation.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/StoreClientInformation.java
index eef8bfaa..b74e4457 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/StoreClientInformation.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/StoreClientInformation.java
@@ -28,10 +28,10 @@ import org.slf4j.Logger;
 
 import com.nimbusds.oauth2.sdk.client.ClientInformation;
 import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformation;
-import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformationResponse;
 
 import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCClientRegistrationResponseContext;
 import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCClientRegistrationTokenClaimsContext;
+import net.shibboleth.idp.plugin.oidc.op.messaging.context.navigate.DefaultClientInformationLookupFunction;
 import net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.DefaultOIDCClientRegistrationTokenClaimsContextLookupFunction;
 import net.shibboleth.idp.plugin.oidc.op.token.support.RegistrationClaimsSet;
 import net.shibboleth.idp.profile.AbstractProfileAction;
@@ -60,16 +60,17 @@ public class StoreClientInformation extends AbstractProfileAction {
     /** Strategy used to locate the {@link OIDCClientRegistrationTokenClaimsContext} associated with the request. */
     @Nonnull private Function<ProfileRequestContext,OIDCClientRegistrationTokenClaimsContext>
         registrationTokenContextLookupStrategy;
-    
+
+    /** Strategy used to locate {@link OIDCClientInformation} to be stored. */
+    @Nonnull private Function<ProfileRequestContext,OIDCClientInformation> clientInformationLookupStrategy;
+
     /** The OIDCClientRegistrationTokenClaimsContext from which to optionally obtain client ID. */
     @Nullable private OIDCClientRegistrationTokenClaimsContext registrationTokenCtx;
     
-    /** The response message. */
-    @Nullable private OIDCClientInformationResponse response;
-    
     /** Constructor. */
     public StoreClientInformation() {
         registrationTokenContextLookupStrategy = new DefaultOIDCClientRegistrationTokenClaimsContextLookupFunction();
+        clientInformationLookupStrategy = new DefaultClientInformationLookupFunction();
     }
     
     /**
@@ -116,6 +117,17 @@ public class StoreClientInformation extends AbstractProfileAction {
                 "OIDCClientRegistrationTokenClaimsContext lookup strategy cannot be null");
     }
 
+    /**
+     * Set the strategy used to locate {@link OIDCClientInformation} to be stored.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setClientInformationLookupStrategy(@Nonnull final Function<ProfileRequestContext,OIDCClientInformation> strategy) {
+        checkSetterPreconditions();
+        
+        clientInformationLookupStrategy = Constraint.isNotNull(strategy, "Client information lookup strategy cannot be null");
+    }
+
     /** {@inheritDoc} */
     @Override
     protected void doInitialize() throws ComponentInitializationException {
@@ -133,25 +145,11 @@ public class StoreClientInformation extends AbstractProfileAction {
             return false;
         }
         
-        if (profileRequestContext.getOutboundMessageContext() == null) {
-            log.error("{} Unable to locate outbound message context", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
-            return false;
-        }
-        
-        final Object message = profileRequestContext.ensureOutboundMessageContext().getMessage();
-        if (message == null || !(message instanceof OIDCClientInformationResponse)) {
-            log.error("{} Unable to locate outbound message", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
-            return false;
-        }
-        
         registrationTokenCtx = registrationTokenContextLookupStrategy.apply(profileRequestContext);
         if (registrationTokenCtx != null && registrationTokenCtx.getClaimsSet() == null) {
             registrationTokenCtx = null;
         }
         
-        response = (OIDCClientInformationResponse) message;
         return true;
     }
     
@@ -159,8 +157,7 @@ public class StoreClientInformation extends AbstractProfileAction {
     @Override
     protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
 
-        assert response != null;
-        final OIDCClientInformation clientInformation = response.getOIDCClientInformation();
+        final OIDCClientInformation clientInformation = clientInformationLookupStrategy.apply(profileRequestContext);
         if (clientInformation == null) {
             log.error("{} Unable to locate client information from the response message", getLogPrefix());
             ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/abstract-register/oidc-abstract-register-beans.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/abstract-register/oidc-abstract-register-beans.xml
new file mode 100644
index 00000000..1d01bc04
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/abstract-register/oidc-abstract-register-beans.xml
@@ -0,0 +1,131 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans" xmlns:c="http://www.springframework.org/schema/c"
+    xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p"
+    xmlns:util="http://www.springframework.org/schema/util" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
+                           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
+                           http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"
+    default-init-method="initialize" default-destroy-method="destroy">
+
+    <bean id="shibboleth.oidc.browserProfile" class="java.lang.Boolean" c:_0="false" />
+
+    <bean id="InitializeOutboundMessageContext"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.InitializeOutboundRegistrationResponseMessageContext"
+        scope="prototype">
+    </bean>
+
+    <bean id="InitializeRelyingPartyContext"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.InitializeUnverifiedRelyingPartyContext"
+        scope="prototype" />
+
+    <bean id="AddRedirectUrisToClientMetadata"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddRedirectUrisToClientMetadata"
+        scope="prototype"
+        p:oidcInputMetadataLookupStrategy-ref="shibboleth.oidc.InputMetadataLookupStrategy"/>
+
+    <bean id="AddTokenEndpointAuthMethodsToClientMetadata"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddTokenEndpointAuthMethodsToClientMetadata"
+        scope="prototype"
+        p:oidcInputMetadataLookupStrategy-ref="shibboleth.oidc.InputMetadataLookupStrategy"
+        p:tokenEndpointAuthMethodsLookupStrategy-ref="shibboleth.oidc.TokenEndpointAuthMethodsLookupStrategy"/>
+
+    <bean id="AddApplicationTypeToClientMetadata"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddApplicationTypeToClientMetadata"
+        scope="prototype"
+        p:oidcInputMetadataLookupStrategy-ref="shibboleth.oidc.InputMetadataLookupStrategy"/>
+
+    <bean id="AddScopeToClientMetadata"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddScopeToClientMetadata"
+        scope="prototype"
+        p:oidcInputMetadataLookupStrategy-ref="shibboleth.oidc.InputMetadataLookupStrategy"
+        p:defaultScope-ref="shibboleth.oidc.DefaultScope" />
+
+    <bean id="AddContactsToClientMetadata"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddContactsToClientMetadata"
+        scope="prototype"
+        p:oidcInputMetadataLookupStrategy-ref="shibboleth.oidc.InputMetadataLookupStrategy"/>
+
+    <bean id="AddGrantTypeToClientMetadata"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddGrantTypeToClientMetadata"
+        scope="prototype"
+        p:oidcInputMetadataLookupStrategy-ref="shibboleth.oidc.InputMetadataLookupStrategy"/>
+
+    <bean id="AddSubjectTypeToClientMetadata"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddSubjectTypeToClientMetadata"
+        scope="prototype"
+        p:oidcInputMetadataLookupStrategy-ref="shibboleth.oidc.InputMetadataLookupStrategy"
+        p:defaultSubjectType-ref="shibboleth.oidc.DefaultSubjectType" />
+
+    <bean id="AddLogoUrisToClientMetadata"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddLogoUrisToClientMetadata"
+        scope="prototype"
+        p:oidcInputMetadataLookupStrategy-ref="shibboleth.oidc.InputMetadataLookupStrategy"/>
+
+    <bean id="AddPolicyUrisToClientMetadata"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddPolicyUrisToClientMetadata"
+        scope="prototype"
+        p:oidcInputMetadataLookupStrategy-ref="shibboleth.oidc.InputMetadataLookupStrategy"/>
+
+    <bean id="AddTosUrisToClientMetadata"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddTosUrisToClientMetadata"
+        scope="prototype"
+        p:oidcInputMetadataLookupStrategy-ref="shibboleth.oidc.InputMetadataLookupStrategy"/>
+
+    <bean id="AddClientNameToClientMetadata"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddClientNameToClientMetadata"
+        scope="prototype"
+        p:oidcInputMetadataLookupStrategy-ref="shibboleth.oidc.InputMetadataLookupStrategy"/>
+
+    <bean id="AddSecurityConfigurationToClientMetadata"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddSecurityConfigurationToClientMetadata"
+        scope="prototype"
+        p:oidcInputMetadataLookupStrategy-ref="shibboleth.oidc.InputMetadataLookupStrategy"/>
+
+    <bean id="AddRequestObjectSecurityConfigurationToClientMetadata"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddRequestObjectSecurityConfigurationToClientMetadata"
+        p:allowSignatureNone="%{idp.oidc.dynreg.allowNoneForRequestSigning:true}"
+        scope="prototype"
+        p:oidcInputMetadataLookupStrategy-ref="shibboleth.oidc.InputMetadataLookupStrategy"/>
+
+    <bean id="AddRequestUrisToClientMetadata"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddRequestUrisToClientMetadata"
+        scope="prototype"
+        p:oidcInputMetadataLookupStrategy-ref="shibboleth.oidc.InputMetadataLookupStrategy"/>
+
+    <bean id="AddLogoutParametersToClientMetadata"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddLogoutParametersToClientMetadata"
+        scope="prototype"
+        p:oidcInputMetadataLookupStrategy-ref="shibboleth.oidc.InputMetadataLookupStrategy"/>
+
+    <bean id="AddRemainingClaimsToClientMetadata"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddRemainingClaimsToClientMetadata"
+        scope="prototype"
+        p:oidcInputMetadataLookupStrategy-ref="shibboleth.oidc.InputMetadataLookupStrategy"/>
+
+    <bean id="AddResponseTypesToClientMetadata"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddResponseTypesToClientMetadata"
+        scope="prototype"
+        p:oidcInputMetadataLookupStrategy-ref="shibboleth.oidc.InputMetadataLookupStrategy"/>
+
+    <bean id="BuildClientInformation"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.BuildClientInformation"
+        scope="prototype" />
+
+    <bean id="oidc.messageEncoderFactory"
+        class="net.shibboleth.idp.plugin.oidc.op.encoding.impl.OIDCResponseEncoderFactory"
+        p:messageEncoder-ref="oidc.nimbusEncoder" scope="prototype" />
+
+    <bean id="oidc.nimbusEncoder"
+        class="net.shibboleth.idp.plugin.oidc.op.encoding.impl.NimbusResponseEncoder"
+        scope="prototype"
+        p:httpServletResponseSupplier-ref="shibboleth.HttpServletResponseSupplier"
+        p:objectMapper-ref="#{'%{idp.oidc.logging.objectMapper:shibboleth.oidc.JSONObjectMapper}'.trim()}"
+        init-method="" />
+
+    <bean id="EncodeMessage"
+        class="org.opensaml.profile.action.impl.EncodeMessage"
+        scope="prototype"
+        p:messageEncoderFactory-ref="oidc.messageEncoderFactory"
+        p:httpServletResponseSupplier-ref="shibboleth.HttpServletResponseSupplier" />
+
+</beans>
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/register/register-flow.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/abstract-register/oidc-abstract-register-flow.xml
similarity index 66%
copy from idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/register/register-flow.xml
copy to idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/abstract-register/oidc-abstract-register-flow.xml
index 637c5620..8c510e5c 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/register/register-flow.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/abstract-register/oidc-abstract-register-flow.xml
@@ -8,21 +8,6 @@
         <evaluate expression="PopulateMetricContext" />
         <evaluate expression="FlowStartPopulateAuditContext" />
         <evaluate expression="InitializeOutboundMessageContext" />
-        <evaluate expression="'proceed'" />
-        <transition on="proceed" to="DecodeMessage">
-            <set name="flowScope.transitionAfterDecode" value="'PostDecodeMessage'" />
-         </transition>
-    </action-state>
-
-    <action-state id="PostDecodeMessage">
-        <evaluate expression="InitializeRelyingPartyContext" />
-        <evaluate expression="ValidateRegistrationAccessToken" />
-        <evaluate expression="SelectRelyingPartyConfiguration" />
-        <evaluate expression="SelectProfileConfiguration" />
-        <evaluate expression="PopulateInboundInterceptContext" />
-        <evaluate expression="'proceed'" />
-        <transition on="proceed"
-            to="CheckInboundInterceptContext" />
     </action-state>
 
     <decision-state id="CheckInboundInterceptContext">
@@ -35,14 +20,6 @@
         <transition on="proceed" to="OutboundContextsAndSecurityParameters" />
     </subflow-state>
 
-    <action-state id="OutboundContextsAndSecurityParameters">
-        <evaluate expression="InitializeRegistrationMetadataPolicyContext" />
-        <evaluate expression="ValidateRegistrationRequestMetadata" />
-        <evaluate expression="CheckRedirectURIs" />
-        <evaluate expression="'proceed'" />
-        <transition on="proceed" to="BuildResponse" />
-    </action-state>
-    
     <action-state id="BuildResponse">
         <evaluate expression="GenerateClientID" />
         <evaluate expression="GenerateClientSecret" />
@@ -64,20 +41,18 @@
         <evaluate expression="AddRequestUrisToClientMetadata" />
         <evaluate expression="AddLogoutParametersToClientMetadata" />
         <evaluate expression="AddRemainingClaimsToClientMetadata" />
-        <evaluate expression="'proceed'" />
-        <transition on="proceed" to="BuildResponseMessage" />
-    </action-state>
-
-    <action-state id="BuildResponseMessage">
-        <transition on="proceed" to="StoreClientInformation" />
+        <evaluate expression="BuildClientInformation" />
     </action-state>
 
     <action-state id="StoreClientInformation">
+        <on-entry>
+            <set name="flowScope.transitionAfterOutboundIntercept" value="#null" />
+        </on-entry>
         <evaluate expression="StoreClientInformation" />
         <evaluate expression="'proceed'" />
         <transition on="proceed" to="CommitResponse" />
     </action-state>
 
-    <bean-import resource="register-beans.xml" />
+    <bean-import resource="classpath:/META-INF/net/shibboleth/idp/flows/oidc/abstract-register/oidc-abstract-register-beans.xml" />
 
 </flow>
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/abstract/oidc-abstract-flow.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/abstract/oidc-abstract-flow.xml
index 4b91bbdc..67fcc15b 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/abstract/oidc-abstract-flow.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/abstract/oidc-abstract-flow.xml
@@ -41,15 +41,16 @@
 
     <decision-state id="CheckOutboundInterceptContext">
         <on-entry>
+            <evaluate expression="flowRequestContext.getFlowScope().get('transitionAfterOutboundIntercept') != null ? flowRequestContext.getFlowScope().get('transitionAfterOutboundIntercept') : 'CommitResponse'" result="flowScope.postOutboundInterceptTransition"/>
             <evaluate expression="PopulateOutboundInterceptContext" />
         </on-entry>
         <if test="opensamlProfileRequestContext.ensureSubcontext(T(net.shibboleth.idp.profile.context.ProfileInterceptorContext)).getAvailableFlows().isEmpty()"
-            then="CommitResponse" else="DoOutboundInterceptSubflow" />
+            then="#{postOutboundInterceptTransition}" else="DoOutboundInterceptSubflow" />
     </decision-state>
 
     <subflow-state id="DoOutboundInterceptSubflow" subflow="intercept">
         <input name="calledAsSubflow" value="true" />
-        <transition on="proceed" to="CommitResponse" />
+        <transition on="proceed" to="#{postOutboundInterceptTransition}" />
         <transition to="HandleError" />
     </subflow-state>
 
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/register/register-beans.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/register/register-beans.xml
index 0795687d..141cc452 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/register/register-beans.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/register/register-beans.xml
@@ -19,11 +19,6 @@
     <util:constant id="shibboleth.metrics.ProfileCounter"
         static-field="net.shibboleth.oidc.profile.config.impl.DefaultOIDCDynamicRegistrationConfiguration.PROFILE_COUNTER" />
 
-    <bean id="InitializeOutboundMessageContext"
-        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.InitializeOutboundRegistrationResponseMessageContext"
-        scope="prototype">
-    </bean>
-
     <bean id="DecodeMessage" class="org.opensaml.profile.action.impl.DecodeMessage" scope="prototype">
         <constructor-arg>
             <bean
@@ -36,10 +31,6 @@
         </constructor-arg>
     </bean>
 
-    <bean id="InitializeRelyingPartyContext"
-        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.InitializeUnverifiedRelyingPartyContext"
-        scope="prototype" />
-
     <bean id="ValidateRegistrationAccessToken"
         class="net.shibboleth.idp.plugin.oidc.op.profile.impl.ValidateRegistrationAccessToken" scope="prototype"
         p:revocationCache-ref="shibboleth.oidc.RevocationCache"
@@ -85,22 +76,11 @@
         </property>
     </bean>
 
-    <bean id="AddRedirectUrisToClientMetadata"
-        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddRedirectUrisToClientMetadata"
-        scope="prototype" />
-
-    <bean id="AddTokenEndpointAuthMethodsToClientMetadata"
-        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddTokenEndpointAuthMethodsToClientMetadata"
-        scope="prototype" />
+    <bean id="shibboleth.oidc.InputMetadataLookupStrategy"
+        class="net.shibboleth.idp.plugin.oidc.op.messaging.context.navigate.OIDCPolicyEnforcedClientRegistrationRequestMetadataLookupFunction" />
 
-    <bean id="AddApplicationTypeToClientMetadata"
-        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddApplicationTypeToClientMetadata"
-        scope="prototype" />
-
-    <bean id="AddScopeToClientMetadata"
-        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddScopeToClientMetadata"
-        scope="prototype"
-        p:defaultScope-ref="shibboleth.oidc.DefaultScope" />
+    <bean id="shibboleth.oidc.TokenEndpointAuthMethodsLookupStrategy"
+        class="net.shibboleth.oidc.profile.config.navigate.TokenEndpointAuthMethodLookupFunction" />
 
     <bean id="shibboleth.oidc.DefaultScope"
         class="com.nimbusds.oauth2.sdk.Scope" factory-method="parse">
@@ -108,19 +88,6 @@
             value="#{'%{idp.oidc.dynreg.defaultScope:openid profile email address phone offline_access}'.trim()}" />
     </bean>
 
-    <bean id="AddContactsToClientMetadata"
-        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddContactsToClientMetadata"
-        scope="prototype" />
-
-    <bean id="AddGrantTypeToClientMetadata"
-        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddGrantTypeToClientMetadata"
-        scope="prototype" />
-
-    <bean id="AddSubjectTypeToClientMetadata"
-        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddSubjectTypeToClientMetadata"
-        scope="prototype"
-        p:defaultSubjectType-ref="shibboleth.oidc.DefaultSubjectType" />
-
     <bean id="shibboleth.oidc.DefaultSubjectType"
         class="com.nimbusds.openid.connect.sdk.SubjectType"
         factory-method="parse">
@@ -128,50 +95,14 @@
             value="#{'%{idp.oidc.dynreg.defaultSubjectType:public}'.trim()}" />
     </bean>
 
-    <bean id="AddResponseTypesToClientMetadata"
-        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddResponseTypesToClientMetadata"
-        scope="prototype" />
-
     <bean id="AddJwksToClientMetadata"
         class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddJwksToClientMetadata"
         scope="prototype"
         p:httpClient="#{getObject('shibboleth.oidc.NonBrowser.HttpClient') ?: getObject('shibboleth.InternalHttpClient')}"
         p:httpClientSecurityParameters="#{getObject('shibboleth.oidc.NonBrowser.HttpClientSecurityParameters')}"
+        p:oidcInputMetadataLookupStrategy-ref="shibboleth.oidc.InputMetadataLookupStrategy"
         p:validateRemoteJwkSetPredicate-ref="%{idp.oidc.dynreg.validateRemoteJwks:shibboleth.Conditions.TRUE}"/>
 
-    <bean id="AddLogoUrisToClientMetadata"
-        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddLogoUrisToClientMetadata"
-        scope="prototype" />
-
-    <bean id="AddPolicyUrisToClientMetadata"
-        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddPolicyUrisToClientMetadata"
-        scope="prototype" />
-
-    <bean id="AddTosUrisToClientMetadata"
-        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddTosUrisToClientMetadata"
-        scope="prototype" />
-
-    <bean id="AddClientNameToClientMetadata"
-        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddClientNameToClientMetadata"
-        scope="prototype" />
-
-    <bean id="AddSecurityConfigurationToClientMetadata"
-        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddSecurityConfigurationToClientMetadata"
-        scope="prototype" />
-
-    <bean id="AddRequestObjectSecurityConfigurationToClientMetadata"
-        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddRequestObjectSecurityConfigurationToClientMetadata"
-        p:allowSignatureNone="%{idp.oidc.dynreg.allowNoneForRequestSigning:true}" scope="prototype" />
-
-    <bean id="AddRequestUrisToClientMetadata" scope="prototype"
-        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddRequestUrisToClientMetadata" />
-
-    <bean id="AddLogoutParametersToClientMetadata" scope="prototype"
-        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddLogoutParametersToClientMetadata" />
-
-    <bean id="AddRemainingClaimsToClientMetadata" scope="prototype"
-        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddRemainingClaimsToClientMetadata" />
-
     <bean id="StoreClientInformation"
             class="net.shibboleth.idp.plugin.oidc.op.profile.impl.StoreClientInformation" scope="prototype"
             p:clientInformationManager-ref="#{'%{idp.oidc.dynreg.clientInformationManager:shibboleth.oidc.ClientInformationManager}'.trim()}">
@@ -192,26 +123,9 @@
     </bean>
 
     <bean id="FormOutboundMessage"
-        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.BuildClientInformation"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.FormOutboundClientInformationResponseMessage"
         scope="prototype" />
 
-    <bean id="oidc.messageEncoderFactory"
-        class="net.shibboleth.idp.plugin.oidc.op.encoding.impl.OIDCResponseEncoderFactory"
-        p:messageEncoder-ref="oidc.nimbusEncoder" scope="prototype" />
-
-    <bean id="oidc.nimbusEncoder"
-        class="net.shibboleth.idp.plugin.oidc.op.encoding.impl.NimbusResponseEncoder"
-        scope="prototype"
-        p:httpServletResponseSupplier-ref="shibboleth.HttpServletResponseSupplier"
-        p:objectMapper-ref="#{'%{idp.oidc.logging.objectMapper:shibboleth.oidc.JSONObjectMapper}'.trim()}"
-        init-method="" />
-
-    <bean id="EncodeMessage"
-        class="org.opensaml.profile.action.impl.EncodeMessage"
-        scope="prototype"
-        p:messageEncoderFactory-ref="oidc.messageEncoderFactory"
-        p:httpServletResponseSupplier-ref="shibboleth.HttpServletResponseSupplier" />
-
     <bean id="PostResponsePopulateAuditContext" parent="shibboleth.AbstractPopulateAuditContext"
         p:fieldExtractors="#{getObject('shibboleth.oidc.RegistrationPostResponseAuditExtractors') ?: getObject('shibboleth.oidc.DefaultRegistrationPostResponseAuditExtractors')}" />
 
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/register/register-flow.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/register/register-flow.xml
index 637c5620..2b0d0041 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/register/register-flow.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/register/register-flow.xml
@@ -1,13 +1,9 @@
 <flow xmlns="http://www.springframework.org/schema/webflow"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:schemaLocation="http://www.springframework.org/schema/webflow http://www.springframework.org/schema/webflow/spring-webflow.xsd"
-    parent="oidc/abstract-api">
+    parent="oidc/abstract-register">
 
     <action-state id="InitializeMandatoryContexts">
-        <evaluate expression="InitializeProfileRequestContext" />
-        <evaluate expression="PopulateMetricContext" />
-        <evaluate expression="FlowStartPopulateAuditContext" />
-        <evaluate expression="InitializeOutboundMessageContext" />
         <evaluate expression="'proceed'" />
         <transition on="proceed" to="DecodeMessage">
             <set name="flowScope.transitionAfterDecode" value="'PostDecodeMessage'" />
@@ -21,20 +17,9 @@
         <evaluate expression="SelectProfileConfiguration" />
         <evaluate expression="PopulateInboundInterceptContext" />
         <evaluate expression="'proceed'" />
-        <transition on="proceed"
-            to="CheckInboundInterceptContext" />
+        <transition on="proceed" to="CheckInboundInterceptContext" />
     </action-state>
 
-    <decision-state id="CheckInboundInterceptContext">
-        <if test="opensamlProfileRequestContext.ensureSubcontext(T(net.shibboleth.idp.profile.context.ProfileInterceptorContext)).getAvailableFlows().isEmpty()"
-            then="OutboundContextsAndSecurityParameters" else="DoInboundInterceptSubflow" />
-    </decision-state>
-
-    <subflow-state id="DoInboundInterceptSubflow" subflow="intercept">
-        <input name="calledAsSubflow" value="true" />
-        <transition on="proceed" to="OutboundContextsAndSecurityParameters" />
-    </subflow-state>
-
     <action-state id="OutboundContextsAndSecurityParameters">
         <evaluate expression="InitializeRegistrationMetadataPolicyContext" />
         <evaluate expression="ValidateRegistrationRequestMetadata" />
@@ -44,38 +29,10 @@
     </action-state>
     
     <action-state id="BuildResponse">
-        <evaluate expression="GenerateClientID" />
-        <evaluate expression="GenerateClientSecret" />
-        <evaluate expression="AddRedirectUrisToClientMetadata" />
-        <evaluate expression="AddApplicationTypeToClientMetadata" />
-        <evaluate expression="AddScopeToClientMetadata" />
-        <evaluate expression="AddGrantTypeToClientMetadata" />
-        <evaluate expression="AddResponseTypesToClientMetadata" />
-        <evaluate expression="AddSubjectTypeToClientMetadata" />
-        <evaluate expression="AddContactsToClientMetadata" />
-        <evaluate expression="AddJwksToClientMetadata" />
-        <evaluate expression="AddTokenEndpointAuthMethodsToClientMetadata" />
-        <evaluate expression="AddLogoUrisToClientMetadata" />
-        <evaluate expression="AddPolicyUrisToClientMetadata" />
-        <evaluate expression="AddTosUrisToClientMetadata" />
-        <evaluate expression="AddClientNameToClientMetadata" />
-        <evaluate expression="AddSecurityConfigurationToClientMetadata" />
-        <evaluate expression="AddRequestObjectSecurityConfigurationToClientMetadata" />
-        <evaluate expression="AddRequestUrisToClientMetadata" />
-        <evaluate expression="AddLogoutParametersToClientMetadata" />
-        <evaluate expression="AddRemainingClaimsToClientMetadata" />
-        <evaluate expression="'proceed'" />
-        <transition on="proceed" to="BuildResponseMessage" />
-    </action-state>
-
-    <action-state id="BuildResponseMessage">
-        <transition on="proceed" to="StoreClientInformation" />
-    </action-state>
-
-    <action-state id="StoreClientInformation">
-        <evaluate expression="StoreClientInformation" />
         <evaluate expression="'proceed'" />
-        <transition on="proceed" to="CommitResponse" />
+        <transition on="proceed" to="FormResponseMessage">
+            <set name="flowScope.transitionAfterOutboundIntercept" value="'StoreClientInformation'" />
+        </transition>
     </action-state>
 
     <bean-import resource="register-beans.xml" />
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/entity-configuration/entity-configuration-beans.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/entity-configuration/entity-configuration-beans.xml
index a4dbb7d6..c6c06ee3 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/entity-configuration/entity-configuration-beans.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/entity-configuration/entity-configuration-beans.xml
@@ -68,7 +68,7 @@
         c:f-ref="shibboleth.MessageContextLookup.Outbound" />
 
     <bean id="BuildEntityStatement"
-        class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.BuildEntityStatement" scope="prototype"
+        class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.BuildEntityConfiguration" scope="prototype"
         p:identifierGeneratorLookupStrategy-ref="shibboleth.oidc.DefaultIdentifierGenerationStrategy"
         p:objectMapper-ref="#{'%{idp.oidc.logging.objectMapper:shibboleth.oidc.JSONObjectMapper}'.trim()}" />
 
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/register/register-beans.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/register/register-beans.xml
new file mode 100644
index 00000000..2b0bac38
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/register/register-beans.xml
@@ -0,0 +1,264 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans"
+    xmlns:c="http://www.springframework.org/schema/c"
+    xmlns:context="http://www.springframework.org/schema/context"
+    xmlns:p="http://www.springframework.org/schema/p"
+    xmlns:util="http://www.springframework.org/schema/util"
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
+                        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
+                        http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"
+    default-init-method="initialize" default-destroy-method="destroy">
+    
+    <bean id="shibboleth.oidc.profileId" class="java.lang.String"
+        c:_0="#{T(net.shibboleth.idp.plugin.oidc.op.oidfed.config.OIDFederationExplicitRegistrationConfiguration).PROFILE_ID}" />
+
+    <bean id="shibboleth.oidc.loggingId" class="java.lang.String"
+        c:_0="%{idp.service.logging.oidfeddynreg:OIDFED.Registration}" />
+
+    <bean id="shibboleth.ClientIDLookupStrategy"
+        class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.navigate.ExplicitClientRegistrationRequestClientIDLookupFunction" />
+
+    <util:constant id="shibboleth.metrics.ProfileCounter"
+        static-field="net.shibboleth.oidc.profile.config.impl.DefaultOIDCDynamicRegistrationConfiguration.PROFILE_COUNTER" />
+
+    <bean id="DecodeMessage" class="org.opensaml.profile.action.impl.DecodeMessage" scope="prototype">
+        <constructor-arg>
+            <bean
+                class="net.shibboleth.idp.plugin.oidc.op.oidfed.decoding.impl.ExplicitClientRegistrationRequestDecoder"
+                scope="prototype"
+                p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier"
+                p:removeIpAddressFromEndpointUri="%{idp.oidc.logging.removeIpAddressFromProtocolMessage:false}"
+                p:objectMapper-ref="#{'%{idp.oidc.logging.objectMapper:shibboleth.oidc.JSONObjectMapper}'.trim()}"
+                p:customRequestParser="#{getObject('%{idp.oidc.requestParser.FederationRegisterRequest:}'.trim())}"/>
+        </constructor-arg>
+    </bean>
+
+    <bean id="ExplicitRegistrationRelyingPartyCreationStrategy" parent="shibboleth.Functions.Expression"
+        c:expression="#input.ensureSubcontext(T(net.shibboleth.profile.context.RelyingPartyContext))" />
+
+    <bean id="ResolveTrustChains" class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.ResolveTrustChains"
+        scope="prototype"
+        p:trustChainCache-ref="#{'%{idp.oidfed.authorize.TrustChainMetadataCache:shibboleth.oidfed.TrustChainMetadataCache}'.trim()}"
+        p:clientIDLookupStrategy-ref="shibboleth.ClientIDLookupStrategy"
+        p:metadataPolicyMergingStrategy-ref="#{'%{idp.oidfed.authorize.TrustChainMetadataPolicyMergingStrategy:DefaultTrustChainMetadataPolicyMergingStrategy}'.trim()}"
+        p:metadataPolicyEnforcer-ref="#{'%{idp.oidfed.authorize.MetadataPolicyEnforcer:DefaultMetadataPolicyEnforcer}'.trim()}"
+        p:preSelectedTrustChainIdsLookupStrategy="#{getObject('shibboleth.oidfed.PreSelectedTrustChainIDsLookupStrategy') ?: getObject('shibboleth.oidfed.DefaultPreSelectedTrustChainIDsLookupStrategy')}"/>
+
+    <bean id="DefaultMetadataPolicyEnforcer"
+        class="net.shibboleth.oidc.metadata.policy.impl.DefaultMetadataPolicyEnforcer" />
+
+    <bean id="DefaultTrustChainMetadataPolicyMergingStrategy" 
+        class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.navigate.DefaultTrustChainMetadataPolicyMergingStrategy"
+        p:localMetadataPolicyStrategy-ref="#{'%{idp.oidfed.authorize.LocalMetadataPolicyStrategy:DefaultLocalMetadataPolicyStrategy}'.trim()}"/>
+
+    <bean id="DefaultLocalMetadataPolicyStrategy"
+        parent="shibboleth.Functions.Constant">
+        <constructor-arg name="target">
+            <util:map>
+                <entry key="scope">
+                    <bean class="net.shibboleth.oidc.metadata.policy.MetadataPolicy" p:defaultValue="openid" />
+                </entry>
+                <entry key="token_endpoint_auth_method">
+                    <bean class="net.shibboleth.oidc.metadata.policy.MetadataPolicy" p:defaultValue="private_key_jwt" />
+                </entry>
+            </util:map>
+        </constructor-arg>
+    </bean>
+
+    <bean id="SelectTrustChain" class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.SelectTrustChain"
+        scope="prototype"
+        p:relyingPartyContextCreationStrategy-ref="ExplicitRegistrationRelyingPartyCreationStrategy">
+        <property name="activationCondition">
+            <bean parent="shibboleth.Conditions.Expression"
+                c:expression="#input.ensureInboundMessageContext().containsSubcontext(T(net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.RelyingPartyTrustChainContext))" />
+        </property>
+    </bean>
+
+    <bean id="ResolveTrustMarks" class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.ResolveTrustMarks"
+        scope="prototype"
+        p:trustChainCache-ref="#{'%{idp.oidfed.authorize.TrustChainMetadataCache:shibboleth.oidfed.TrustChainMetadataCache}'.trim()}">
+        <property name="trustEngine">
+            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.TokenPayloadAsymmetricKeyTrustEngine">
+                <constructor-arg>
+                    <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultEntityStatementCredentialResolver" />
+                 </constructor-arg>
+            </bean>
+        </property>
+        <property name="trustedTrustMarkIssuersLookupStrategy">
+            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultTrustChainTrustedTrustMarkIssuersLookupStrategy"
+                p:objectMapper-ref="shibboleth.oidc.JSONObjectMapper" />
+        </property>
+    </bean>
+
+    <bean id="RelyingPartyTrustChainContextLookupStrategy" parent="shibboleth.Functions.Expression"
+        c:expression="#input.ensureInboundMessageContext().getSubcontext(T(net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.RelyingPartyTrustChainContext))" />
+
+    <bean id="SelectExplicitRegistrationRelyingPartyConfiguration"
+        class="net.shibboleth.idp.profile.impl.SelectRelyingPartyConfiguration" scope="prototype"
+        p:relyingPartyContextLookupStrategy-ref="ExplicitRegistrationRelyingPartyCreationStrategy"
+        p:relyingPartyConfigurationResolver-ref="shibboleth.RelyingPartyResolverService" />
+
+    <bean id="SelectExplicitRegistrationProfileConfiguration"
+        class="net.shibboleth.idp.profile.impl.SelectProfileConfiguration" scope="prototype"
+        p:relyingPartyContextLookupStrategy-ref="ExplicitRegistrationRelyingPartyCreationStrategy"
+        p:profileId="#{T(net.shibboleth.idp.plugin.oidc.op.oidfed.config.OIDFederationExplicitRegistrationConfiguration).PROFILE_ID}" />
+
+    <bean id="InitializeRegistrationMetadataPolicyContext"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.InitializeRegistrationMetadataPolicyContext"
+        scope="prototype"
+        p:metadataPolicyValidationStrategy="#{getObject('shibboleth.oidc.dynreg.MetadataPolicyValidator') ?: getObject('shibboleth.oidc.dynreg.DefaultMetadataPolicyValidator')}" />
+
+    <bean id="ValidateExplicitRegistrationProfileConfiguration"
+        class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.ValidateAutomaticRegistrationProfileConfiguration"
+        scope="prototype">
+        <property name="localMetadataPolicyMergingStrategy">
+            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.navigate.DefaultLocalMetadataPolicyMergingStrategy"
+                p:metadataPolicyEnforcer-ref="#{'%{idp.oidfed.authorize.MetadataPolicyEnforcer:DefaultMetadataPolicyEnforcer}'.trim()}"/>
+        </property>
+        <property name="mandatoryTrustMarksLookupStrategy">
+            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.navigate.MandatoryTrustMarksLookupFunction"
+                p:relyingPartyContextLookupStrategy-ref="ExplicitRegistrationRelyingPartyCreationStrategy"/>
+        </property>
+    </bean>
+
+    <bean id="ValidateRegistrationRequestMetadata"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.ValidateRegistrationRequestMetadata"
+        scope="prototype"
+        p:metadataPolicyEnforcer="#{getObject('shibboleth.oidc.dynreg.MetadataPolicyEnforcer') ?: getObject('shibboleth.oidc.dynreg.DefaultMetadataPolicyEnforcer')}"/>
+        
+    <bean id="CheckRedirectURIs"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.CheckRedirectURIs"
+        scope="prototype"
+        p:httpClient="#{getObject('shibboleth.oidc.NonBrowser.HttpClient') ?: getObject('shibboleth.InternalHttpClient')}"
+        p:httpClientSecurityParameters="#{getObject('shibboleth.oidc.NonBrowser.HttpClientSecurityParameters')}"
+        p:objectMapper-ref="shibboleth.oidc.JSONObjectMapper">
+        <property name="requestMetadataLookupStrategy">
+            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.navigate.DefaultSelectedTrustChainMetadataLookupStrategy" />
+        </property>
+    </bean>
+
+    <bean id="GenerateClientID"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.GenerateClientID"
+        scope="prototype"
+        p:xmlSafeIdentifier="%{idp.oidc.xmlSafeIdentifiers:true}"
+        p:relyingPartyContextLookupStrategy-ref="ExplicitRegistrationRelyingPartyCreationStrategy"
+        p:identifierGeneratorLookupStrategy="#{getObject('shibboleth.oidc.dynreg.ClientIDGenerationStrategy') ?: getObject('shibboleth.oidc.DefaultIdentifierGenerationStrategy')}"/>
+
+    <bean id="GenerateClientSecret"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.GenerateClientSecret" scope="prototype"
+        p:xmlSafeIdentifier="%{idp.oidc.xmlSafeIdentifiers:true}"
+        p:identifierGeneratorLookupStrategy="#{getObject('shibboleth.oidc.dynreg.ClientSecretGenerationStrategy') ?: getObject('shibboleth.oidc.DefaultIdentifierGenerationStrategy')}">
+        <property name="secretExpirationPeriodStrategy">
+            <bean class="net.shibboleth.oidc.profile.config.navigate.SecretExpirationPeriodLookupFunction" />
+        </property>
+    </bean>
+
+    <bean id="shibboleth.oidc.InputMetadataLookupStrategy"
+        class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.navigate.DefaultSelectedTrustChainMetadataLookupStrategy" />
+
+    <bean id="shibboleth.oidc.TokenEndpointAuthMethodsLookupStrategy"
+        class="net.shibboleth.idp.plugin.oidc.op.oidfed.config.TokenEndpointAuthMethodLookupFunction" />
+
+    <bean id="shibboleth.oidc.DefaultScope"
+        class="com.nimbusds.oauth2.sdk.Scope" factory-method="parse">
+        <constructor-arg type="java.lang.String"
+            value="#{'%{idp.oidfed.expreg.defaultScope:openid profile email address phone offline_access}'.trim()}" />
+    </bean>
+
+    <bean id="shibboleth.oidc.DefaultSubjectType"
+        class="com.nimbusds.openid.connect.sdk.SubjectType"
+        factory-method="parse">
+        <constructor-arg type="java.lang.String"
+            value="#{'%{idp.oidfed.expreg.defaultSubjectType:public}'.trim()}" />
+    </bean>
+
+    <bean id="AddJwksToClientMetadata"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddJwksToClientMetadata"
+        scope="prototype"
+        p:httpClient="#{getObject('shibboleth.oidc.NonBrowser.HttpClient') ?: getObject('shibboleth.InternalHttpClient')}"
+        p:httpClientSecurityParameters="#{getObject('shibboleth.oidc.NonBrowser.HttpClientSecurityParameters')}"
+        p:oidcInputMetadataLookupStrategy-ref="shibboleth.oidc.InputMetadataLookupStrategy"
+        p:validateRemoteJwkSetPredicate-ref="%{idp.oidfed.expreg.validateRemoteJwks:shibboleth.Conditions.TRUE}"/>
+
+    <bean id="StoreClientInformation"
+            class="net.shibboleth.idp.plugin.oidc.op.profile.impl.StoreClientInformation" scope="prototype"
+            p:clientInformationManager-ref="#{'%{idp.oidfed.expreg.clientInformationManager:shibboleth.oidc.ClientInformationManager}'.trim()}">
+        <property name="registrationValidityPeriodStrategy">
+            <bean parent="shibboleth.Functions.Expression"
+                c:expression="T(java.time.Duration).between(T(java.time.Instant).now(), #input.ensureInboundMessageContext().ensureSubcontext(T(net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.RelyingPartyTrustChainContext)).getSelectedMetadataExpiration())" />
+        </property>
+    </bean>
+
+    <bean id="BuildErrorResponseFromEvent"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.BuildRegistrationErrorResponseFromEvent"
+        scope="prototype"
+        p:httpServletResponseSupplier-ref="shibboleth.HttpServletResponseSupplier"
+        p:mappedErrors="#{getObject('shibboleth.oidc.register.MappedErrors') ?: getObject('shibboleth.oidc.register.DefaultMappedErrors')}">
+        <property name="eventContextLookupStrategy">
+            <bean
+                class="net.shibboleth.idp.profile.context.navigate.WebFlowCurrentEventLookupFunction" />
+        </property>
+    </bean>
+
+    <bean id="PopulateEntityStatementSignatureSigningParameters"
+        class="net.shibboleth.oidc.profile.impl.PopulateJWTSignatureSigningParameters" scope="prototype"
+        c:strategy-ref="shibboleth.MessageContextLookup.Outbound"
+        p:securityParametersContextLookupStrategy-ref="EntityStatementSecurityParametersContextLookupStrategy">
+        <property name="configurationLookupStrategy">
+            <bean lazy-init="true"
+                class="net.shibboleth.oidc.profile.config.navigate.JWTSignatureSigningConfigurationLookupFunction" />
+        </property>
+        <property name="signatureSigningParametersResolver">
+            <bean class="net.shibboleth.oidc.security.jose.impl.ClientInformationSignatureSigningParametersResolver">
+                <constructor-arg name="signatureAlgorithmLookupStrategy">
+                    <bean parent="shibboleth.Functions.Constant" c:target="" />
+                </constructor-arg>
+                <constructor-arg name="defaultAlgorithmValue" value="%{idp.oidfed.entity.sigalg:RS256}" />
+            </bean>
+        </property>
+    </bean>
+
+    <bean id="EntityStatementSecurityParametersContextLookupStrategy" parent="shibboleth.Functions.Compose"
+            c:g-ref="shibboleth.oidc.ChildLookupOrCreate.JWTSecurityParameters"
+            c:f-ref="shibboleth.ChildLookup.RelyingParty" />
+
+    <bean id="EntityStatementSecurityParametersCreationViaMessageContextStrategy" parent="shibboleth.Functions.Compose">
+        <constructor-arg name="g" ref="EntityStatementSecurityParametersContextLookupStrategy" />
+        <constructor-arg name="f">
+            <bean parent="shibboleth.Functions.Expression" c:expression="#input.getParent()" />
+        </constructor-arg>
+    </bean>
+
+    <bean id="BuildEntityStatement"
+        class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.BuildExplicitRegistrationResponse" scope="prototype"
+        p:identifierGeneratorLookupStrategy-ref="shibboleth.oidc.DefaultIdentifierGenerationStrategy"
+        p:objectMapper-ref="#{'%{idp.oidc.logging.objectMapper:shibboleth.oidc.JSONObjectMapper}'.trim()}" />
+
+    <bean id="SignEntityStatement" class="net.shibboleth.idp.profile.impl.WebFlowMessageHandlerAdaptor"
+            scope="prototype" c:executionDirection="OUTBOUND ">
+        <constructor-arg name="messageHandler">
+            <bean id="SignEntityStatementHandler"
+                class="net.shibboleth.oidc.security.impl.SignJWTHandler" scope="prototype" p:logName="Entity Statement"
+                p:securityParametersLookupStrategy-ref="EntityStatementSecurityParametersCreationViaMessageContextStrategy"
+                p:typeHeader="entity-statement+jwt">
+                <property name="claimsToSignLookupStrategy">
+                     <bean
+                        class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.JWTClaimsSetFromEntityStatementLookupFunction" />
+                </property>
+                <property name="jwtUpdateConsumer">
+                    <bean
+                        class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.EntityStatementUpdateStrategy" />
+                </property>
+            </bean>
+        </constructor-arg>
+    </bean>
+
+    <bean id="FormOutboundMessage"
+        class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.FormExplicitRegistrationResponse"
+        scope="prototype" />
+
+    <bean id="PostResponsePopulateAuditContext" parent="shibboleth.AbstractPopulateAuditContext"
+        p:fieldExtractors="#{getObject('shibboleth.oidc.RegistrationPostResponseAuditExtractors') ?: getObject('shibboleth.oidc.DefaultRegistrationPostResponseAuditExtractors')}" />
+
+</beans>
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/register/register-flow.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/register/register-flow.xml
new file mode 100644
index 00000000..493fae0c
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/register/register-flow.xml
@@ -0,0 +1,49 @@
+<flow xmlns="http://www.springframework.org/schema/webflow"
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://www.springframework.org/schema/webflow http://www.springframework.org/schema/webflow/spring-webflow.xsd"
+    parent="oidc/abstract-register">
+
+    <action-state id="InitializeMandatoryContexts">
+        <evaluate expression="'proceed'" />
+        <transition on="proceed" to="DecodeMessage">
+            <set name="flowScope.transitionAfterDecode" value="'ResolveTrustChains'" />
+         </transition>
+    </action-state>
+
+    <action-state id="ResolveTrustChains">
+        <evaluate expression="ResolveTrustChains" />
+        <evaluate expression="'proceed'" />
+        <transition on="proceed" to="SelectTrustChain" />
+    </action-state>
+
+    <action-state id="SelectTrustChain">
+        <evaluate expression="SelectTrustChain" />
+        <evaluate expression="ResolveTrustMarks" />
+        <evaluate expression="SelectExplicitRegistrationRelyingPartyConfiguration" />
+        <evaluate expression="SelectExplicitRegistrationProfileConfiguration" />
+        <evaluate expression="ValidateExplicitRegistrationProfileConfiguration" />
+        <evaluate expression="PopulateInboundInterceptContext" />
+        <evaluate expression="'proceed'" />
+        <transition on="ReselectTrustChain" to="SelectTrustChain" />
+        <transition on="proceed" to="CheckInboundInterceptContext" />
+    </action-state>
+
+    <action-state id="OutboundContextsAndSecurityParameters">
+        <evaluate expression="CheckRedirectURIs" />
+        <evaluate expression="'proceed'" />
+        <transition on="proceed" to="BuildResponse" />
+    </action-state>
+    
+    <action-state id="BuildResponse">
+        <evaluate expression="PopulateEntityStatementSignatureSigningParameters" />
+        <evaluate expression="BuildEntityStatement" />
+        <evaluate expression="SignEntityStatement" />
+        <evaluate expression="'proceed'" />
+        <transition on="proceed" to="FormResponseMessage">
+            <set name="flowScope.transitionAfterOutboundIntercept" value="'StoreClientInformation'" />
+        </transition>
+    </action-state>
+
+    <bean-import resource="register-beans.xml" />
+
+</flow>
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml
index 41448710..c467e4d3 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml
@@ -117,6 +117,11 @@
         p:mandatoryTrustMarks="%{idp.oidfed.automaticRegistration.mandatoryTrustMarks:}"
         p:securityConfiguration-ref="shibboleth.oidc.federation.DefaultSecurityConfiguration" />
 
+    <bean id="OIDFED.ExplicitRegistration" parent="AbstractOIDCProfile" lazy-init="true"
+        class="net.shibboleth.idp.plugin.oidc.op.oidfed.config.DefaultOIDFederationExplicitRegistrationConfiguration"
+        p:mandatoryTrustMarks="%{idp.oidfed.explicitRegistration.mandatoryTrustMarks:}"
+        p:securityConfiguration-ref="shibboleth.oidc.federation.DefaultSecurityConfiguration" />
+
     <!-- Metadata-driven variants. -->
     
     <bean id="AbstractMDDrivenOIDCProfile" parent="AbstractMDDrivenProfile" abstract="true">
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/RegistrationFlowTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/RegistrationFlowTest.java
new file mode 100644
index 00000000..f607eec4
--- /dev/null
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/RegistrationFlowTest.java
@@ -0,0 +1,101 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.profile.flow.oidfed;
+
+import org.opensaml.storage.StorageRecord;
+import org.opensaml.storage.StorageService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.webflow.executor.FlowExecutionResult;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformation;
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientMetadata;
+
+import net.minidev.json.JSONObject;
+import net.minidev.json.parser.JSONParser;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.messaging.impl.ExplicitClientRegistrationResponse;
+import net.shibboleth.oidc.metadata.impl.BaseStorageServiceClientInformationComponent;
+
+/**
+ * Flow tests for the OpenID federation explicit registration flow.
+ */
+public class RegistrationFlowTest extends AbstractFederationFlowTest {
+    
+    public static final String FLOW_ID = "oidfed/register";
+    
+    @Autowired
+    @Qualifier("shibboleth.StorageService")
+    StorageService storageService;
+    
+    public RegistrationFlowTest() {
+        super(FLOW_ID);
+    }
+    
+    @Test
+    public void testInvalidContentType() throws Exception {
+        setJsonRequest("POST", "{}");
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        assertErrorCode(result, "invalid_client_metadata");
+    }
+
+    @Test
+    public void testEmptyEntityConfiguration() throws Exception {
+        setRequest("POST", "", "application/entity-statement+jwt");
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        assertErrorCode(result, "invalid_client_metadata");
+    }
+
+    @Test
+    public void testValidEntityConfiguration_invalidType() throws Exception {
+        final String clientId = uniqueClientId();
+        configureMockHttpClient(clientId);
+        setRequest("POST", rpEntityConfiguration(clientId), "application/json");
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        assertErrorCode(result, "invalid_client_metadata");
+    }
+
+    @Test
+    public void testValidEntityConfiguration() throws Exception {
+        final String clientId = uniqueClientId();
+        configureMockHttpClient(clientId);
+        setRequest("POST", rpEntityConfiguration(clientId), "application/entity-statement+jwt");
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        final ExplicitClientRegistrationResponse parsedResponse =
+                parseSuccessResponse(result, ExplicitClientRegistrationResponse.class);
+        final EntityStatement entityStatement = parsedResponse.getEntityStatement();
+        final OIDCClientInformation clientInfo = entityStatement.getClaimsSet().getRPInformation();
+        final OIDCClientMetadata metadata = clientInfo.getOIDCMetadata();
+        final String providedClientId = clientInfo.getID().getValue();
+        assert providedClientId != null;
+        assert storageService != null;
+        final StorageRecord<String> storageRecord = storageService.read(BaseStorageServiceClientInformationComponent.CONTEXT_NAME, 
+                providedClientId);
+        Assert.assertNotNull(storageRecord, "Record with clientId " + providedClientId + " was null");
+        assert storageRecord != null;
+        final String record = storageRecord.getValue();
+        Assert.assertNotNull(record);
+        final JSONParser parser = new JSONParser(JSONParser.DEFAULT_PERMISSIVE_MODE);
+        final OIDCClientInformation storedInfo = OIDCClientInformation.parse((JSONObject) parser.parse(record));
+        Assert.assertEquals(storedInfo.getID(), clientInfo.getID());
+        Assert.assertEquals(storedInfo.getSecret(), clientInfo.getSecret());
+        Assert.assertEquals(storedInfo.getOIDCMetadata().getRedirectionURIStrings(), metadata.getRedirectionURIStrings());
+        //Assert.assertEquals(storedInfo.getOIDCMetadata().getRequestObjectURIs(), Set.of(new URI(requestUri)));
+        Assert.assertTrue(metadata.getRedirectionURIStrings().contains(redirectUri));
+        
+    }
+}
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/BuildClientInformationTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/BuildClientInformationTest.java
index 21603ff3..efa610f6 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/BuildClientInformationTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/BuildClientInformationTest.java
@@ -28,7 +28,6 @@ import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod;
 import com.nimbusds.oauth2.sdk.auth.Secret;
 import com.nimbusds.oauth2.sdk.id.ClientID;
 import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformation;
-import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformationResponse;
 import com.nimbusds.openid.connect.sdk.rp.OIDCClientMetadata;
 
 import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCClientRegistrationResponseContext;
@@ -71,13 +70,6 @@ public class BuildClientInformationTest {
         registrationCtx.setClientMetadata(metadata);
     }
 
-    @Test
-    public void noOutboundMessageContext() {
-        final ProfileRequestContext localPrc = new ProfileRequestContext();
-        action.execute(localPrc);
-        ActionTestingSupport.assertEvent(localPrc, EventIds.INVALID_PROFILE_CTX);
-    }
-    
     @Test
     public void noMetadataContext() {
         final ProfileRequestContext localPrc = new ProfileRequestContext();
@@ -168,19 +160,16 @@ public class BuildClientInformationTest {
     protected void assertSuccessfulResponse(boolean secret) {
         assert profileRequestCtx != null;
         ActionTestingSupport.assertProceedEvent(profileRequestCtx);
-        final OIDCClientInformationResponse response = (OIDCClientInformationResponse) messageCtx.getMessage();
-        Assert.assertNotNull(response);
-        assert response != null;
-        final OIDCClientInformation clientInformation = response.getOIDCClientInformation();
+        final OIDCClientInformation clientInformation = registrationCtx.getClientInformation();
         assert clientInformation != null;
         Assert.assertEquals(clientInformation.getID(), new ClientID(clientId));
         if (secret) {
-            assertSecret(response);        
+            assertSecret(clientInformation);
         }
     }
 
-    protected void assertSecret(final OIDCClientInformationResponse response) {
-        final Secret secret = response.getOIDCClientInformation().getSecret();
+    protected void assertSecret(final OIDCClientInformation information) {
+        final Secret secret = information.getSecret();
         Assert.assertNotNull(secret);
         Assert.assertEquals(secret.getValue(), clientSecret);
         Assert.assertNull(secret.getExpirationDate());
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/CheckRedirectUrisTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/CheckRedirectUrisTest.java
index 0a292b87..b2f47061 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/CheckRedirectUrisTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/CheckRedirectUrisTest.java
@@ -67,7 +67,7 @@ public class CheckRedirectUrisTest extends BaseOIDCRegistrationRequestTest {
     @Test
     public void testNoMessage() throws ComponentInitializationException {
         setUpContext(null);
-        ActionTestingSupport.assertEvent(action.execute(requestCtx), EventIds.INVALID_MSG_CTX);
+        ActionTestingSupport.assertEvent(action.execute(requestCtx), EventIds.INVALID_MESSAGE);
     }
     
     @Test
diff --git a/idp-oidc-extension-impl/src/test/resources/net/shibboleth/idp/module/conf/relying-party.xml b/idp-oidc-extension-impl/src/test/resources/net/shibboleth/idp/module/conf/relying-party.xml
index 83bc574e..0d2ac430 100644
--- a/idp-oidc-extension-impl/src/test/resources/net/shibboleth/idp/module/conf/relying-party.xml
+++ b/idp-oidc-extension-impl/src/test/resources/net/shibboleth/idp/module/conf/relying-party.xml
@@ -81,6 +81,7 @@
             <property name="profileConfigurations">
                 <list>
                     <bean parent="OIDFED.AutomaticRegistration" p:mandatoryTrustMarks=""/>
+                    <bean parent="OIDFED.ExplicitRegistration" p:mandatoryTrustMarks=""/>
                 </list>
             </property>
         </bean>

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


More information about the commits mailing list