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

Henri Mikkonen henri.mikkonen at iki.fi
Fri Sep 6 11:25:11 UTC 2024


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=62e69dbff8bfb524166b4cb774acb40b6d7c7168

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

commit 62e69dbff8bfb524166b4cb774acb40b6d7c7168
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Fri Sep 6 14:24:46 2024 +0300

    JOIDC-222 - Support for OpenID Federation
    
    https://shibboleth.atlassian.net/browse/JOIDC-222
    
    Initial implementation of the well-known/openid-federation endpoint
    - New OIDFED.Configuration profile
      - shibboleth.oidc.federation.DefaultSecurityConfiguration
      - shibboleth.oidc.federation.SigningConfiguration used for signing entity configuration (JWT)
        - By default all the asymmetric algorithms enabled
        - Credentials from 'shibboleth.oidfed.SigningCredentials'
    - New oidfed/entity-configuration flow
      - Exploits dynamic creation of openid-configuration as implemented in  oidc/configuration
      - Moved 'shibboleth.oidc.DefaultOpenIdConfigurationResolver' from configuration flow into global beans
      - Caching and entity statement (JWT) lifetime configurable via profile configuration settings
        - property idp.oidfed.entity.configurationCache for Storage Service (in-memory by default)
---
 .../DefaultOIDFederationEntityConfiguration.java   | 239 ++++++++++++++
 .../config/OIDFederationEntityConfiguration.java   |  94 ++++++
 .../oidfed/profile/impl/BuildEntityStatement.java  | 359 +++++++++++++++++++++
 .../profile/impl/EntityStatementContext.java       | 109 +++++++
 .../impl/EntityStatementUpdateStrategy.java        |  59 ++++
 ...ormOutboundFederationConfigurationResponse.java | 139 ++++++++
 .../impl/InitializeEntityStatementContext.java     | 144 +++++++++
 ...ClaimsSetFromEntityStatementLookupFunction.java |  87 +++++
 .../navigate/AuthorityHintsLookupFunction.java     |  48 +++
 ...achedEntityStatementLifetimeLookupFunction.java |  52 +++
 ...laimsSetManipulationStrategyLookupFunction.java |  53 +++
 .../EntityStatementLifetimeLookupFunction.java     |  51 +++
 .../META-INF/net.shibboleth.idp/postconfig.xml     |  11 +
 .../oidc-abstract-api-info-flow.xml                |   7 +-
 .../oidc/configuration/configuration-beans.xml     |  10 -
 .../entity-configuration-beans.xml                 |  98 ++++++
 .../entity-configuration-flow.xml                  |  54 ++++
 .../idp/service/relying-party/postconfig.xml       |  57 ++++
 .../flow/FederationConfigurationFlowTest.java      | 101 ++++++
 .../resources/credentials/fed-signing-es256.jwk    |  10 +
 .../resources/credentials/fed-signing-es384.jwk    |   9 +
 .../resources/credentials/fed-signing-es521.jwk    |   9 +
 .../test/resources/credentials/fed-signing-rs.jwk  |   8 +
 .../idp/module/conf/oidc-credentials.xml           |  23 ++
 .../shibboleth/idp/module/conf/relying-party.xml   |   1 +
 25 files changed, 1820 insertions(+), 12 deletions(-)

diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/config/DefaultOIDFederationEntityConfiguration.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/config/DefaultOIDFederationEntityConfiguration.java
new file mode 100644
index 00000000..44612cf4
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/config/DefaultOIDFederationEntityConfiguration.java
@@ -0,0 +1,239 @@
+/*
+ * 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.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+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 Entity Configuration.
+ */
+public class DefaultOIDFederationEntityConfiguration extends AbstractConditionalProfileConfiguration
+        implements OIDFederationEntityConfiguration {
+
+    /** OIDC provider information profile counter name. */
+    @Nonnull @NotEmpty public static final String PROFILE_COUNTER = "net.shibboleth.idp.profiles.oidfed.configuration";
+
+    /** Lookup function to override issuer value. */
+    @Nonnull private Function<ProfileRequestContext,String> issuerLookupStrategy;
+
+    /** Lookup function to authority hints. */
+    @Nonnull private Function<ProfileRequestContext,List<String>> authorityHintsLookupStrategy;
+
+    /** Lookup function to supply entity statement lifetime. */
+    @Nonnull private Function<ProfileRequestContext,Duration> entityStatementLifetimeLookupStrategy;
+
+    /** Lookup function to supply cached entity statement lifetime. */
+    @Nonnull private Function<ProfileRequestContext,Duration> cachedEntityStatementLifetimeLookupStrategy;
+
+    /** Lookup function to supply strategy bi-function for manipulating entity statement claims set. */ 
+    @Nonnull
+    private Function<ProfileRequestContext,BiFunction<ProfileRequestContext,Map<String,Object>,Map<String,Object>>>
+        entityStatementClaimsSetManipulationStrategyLookupStrategy;    
+
+    /**
+     * Constructor.
+     */
+    public DefaultOIDFederationEntityConfiguration() {
+        this(PROFILE_ID);
+    }
+    
+    /**
+     * Creates a new configuration instance.
+     *
+     * @param profileId Unique profile identifier.
+     */
+    public DefaultOIDFederationEntityConfiguration(@Nonnull @NotEmpty final String profileId) {
+        super(profileId);
+        issuerLookupStrategy = FunctionSupport.constant(null);
+        authorityHintsLookupStrategy = FunctionSupport.constant(CollectionSupport.emptyList());
+        entityStatementLifetimeLookupStrategy = FunctionSupport.constant(Duration.ofHours(24));
+        cachedEntityStatementLifetimeLookupStrategy = FunctionSupport.constant(Duration.ofSeconds(10));
+        entityStatementClaimsSetManipulationStrategyLookupStrategy = FunctionSupport.constant(null);
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    @Nullable @NotEmpty public String getIssuer(@Nullable final ProfileRequestContext profileRequestContext) {
+        return issuerLookupStrategy.apply(profileRequestContext);
+    }
+    
+    /**
+     * Set overridden issuer value.
+     * 
+     * @param issuer issuer value
+     */
+    public void setIssuer(@Nullable @NotEmpty final String issuer) {
+        issuerLookupStrategy = FunctionSupport.constant(issuer);
+    }
+    
+    /**
+     * Sets lookup strategy for overridden issuer value.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setIssuerLookupStrategy(@Nonnull final Function<ProfileRequestContext,String> strategy) {
+        issuerLookupStrategy = Constraint.isNotNull(strategy, "Issuer lookup strategy cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    @Override @Nonnull @NonnullElements @NotLive @Unmodifiable
+    public List<String> getAuthorityHints(@Nullable final ProfileRequestContext profileRequestContext) {
+        final List<String> authorityHints = authorityHintsLookupStrategy.apply(profileRequestContext);
+        if (authorityHints != null) {
+            return CollectionSupport.copyToList(authorityHints);
+        }
+        return CollectionSupport.emptyList();
+    }
+    
+    /**
+     * Set authority hints value.
+     * 
+     * @param hints authority hints
+     */
+    public void setAuthorityHints(@Nonnull @NonnullElements @NotLive @Unmodifiable final List<String> hints) {
+        authorityHintsLookupStrategy = FunctionSupport.constant(hints);
+    }
+    
+    /**
+     * Sets lookup strategy for authority hints value.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setAuthorityHintsLookupStrategy(@Nonnull final Function<ProfileRequestContext,List<String>> strategy) {
+        authorityHintsLookupStrategy = Constraint.isNotNull(strategy, "Authority hints lookup strategy cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    @Positive @Nonnull
+    public Duration getEntityStatementLifetime(@Nullable final ProfileRequestContext profileRequestContext) {
+        final Duration lifetime = entityStatementLifetimeLookupStrategy.apply(profileRequestContext);
+
+        Constraint.isTrue(lifetime != null && !lifetime.isZero() && !lifetime.isNegative(),
+                "Entity statement lifetime must be greater than 0");
+        assert lifetime != null;
+        return lifetime;
+    }
+
+    /**
+     * Set the lifetime of an entity statement.
+     * 
+     * @param lifetime lifetime of an entity statement
+     */
+    public void setEntityStatementLifetime(@Positive @Nonnull final Duration lifetime) {
+        final Duration statementLifetime = Constraint.isNotNull(lifetime, "Entity statement lifetime cannot be null");
+        Constraint.isTrue(!statementLifetime.isZero() && !statementLifetime.isNegative(),
+                "Entity statement lifetime must be greater than 0");
+
+        entityStatementLifetimeLookupStrategy = FunctionSupport.constant(statementLifetime);
+    }
+
+    /**
+     * Set a lookup strategy for the entity statement lifetime.
+     *
+     * @param strategy lookup strategy
+     */
+    public void setEntityStatementLifetimeLookupStrategy(
+            @Nullable final Function<ProfileRequestContext,Duration> strategy) {
+        entityStatementLifetimeLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    @Positive @Nonnull
+    public Duration getCachedEntityStatementLifetime(@Nullable final ProfileRequestContext profileRequestContext) {
+        final Duration lifetime = cachedEntityStatementLifetimeLookupStrategy.apply(profileRequestContext);
+
+        Constraint.isTrue(lifetime != null && !lifetime.isZero() && !lifetime.isNegative(),
+                "Entity statement lifetime must be greater than 0");
+        assert lifetime != null;
+        return lifetime;
+    }
+
+    /**
+     * Set the lifetime of a cached entity statement.
+     * 
+     * @param lifetime lifetime of a cached entity statement
+     */
+    public void setCachedEntityStatementLifetime(@Positive @Nonnull final Duration lifetime) {
+        final Duration statementLifetime = Constraint.isNotNull(lifetime,
+                "Cached entity statement lifetime cannot be null");
+        Constraint.isTrue(!statementLifetime.isZero() && !statementLifetime.isNegative(),
+                "Cached entity statement lifetime must be greater than 0");
+
+        cachedEntityStatementLifetimeLookupStrategy = FunctionSupport.constant(statementLifetime);
+    }
+
+    /**
+     * Set a lookup strategy for the cached entity statement lifetime.
+     *
+     * @param strategy lookup strategy
+     */
+    public void setCachedEntityStatementLifetimeLookupStrategy(
+            @Nullable final Function<ProfileRequestContext,Duration> strategy) {
+        cachedEntityStatementLifetimeLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    @Nullable
+    public BiFunction<ProfileRequestContext,Map<String,Object>,Map<String,Object>>
+        getEntityStatementClaimsSetManipulationStrategy(
+            @Nullable final ProfileRequestContext profileRequestContext) {
+        return entityStatementClaimsSetManipulationStrategyLookupStrategy.apply(profileRequestContext);
+    }
+
+    /**
+     * Set the bi-function for manipulating entity statement claims set.
+     * 
+     * @param strategy bi-function for manipulating entity statement claims set
+     */
+    public void setEntityStatementClaimsSetManipulationStrategy(
+            @Nullable final BiFunction<ProfileRequestContext,Map<String,Object>,Map<String,Object>> strategy) {
+        entityStatementClaimsSetManipulationStrategyLookupStrategy = FunctionSupport.constant(strategy);
+    }
+
+    /**
+     * Set a lookup strategy for the bi-function for manipulating entity statement claims set.
+     *
+     * @param strategy lookup strategy
+     */
+    public void setEntityStatementClaimsSetManipulationStrategyLookupStrategy(@Nonnull final 
+            Function<ProfileRequestContext,BiFunction<ProfileRequestContext,Map<String,Object>,Map<String,Object>>>
+            strategy) {
+        entityStatementClaimsSetManipulationStrategyLookupStrategy = 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/OIDFederationEntityConfiguration.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/config/OIDFederationEntityConfiguration.java
new file mode 100644
index 00000000..f797e681
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/config/OIDFederationEntityConfiguration.java
@@ -0,0 +1,94 @@
+/*
+ * 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.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiFunction;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import net.shibboleth.oidc.profile.config.OIDCProfileConfiguration;
+import net.shibboleth.profile.config.OverriddenIssuerProfileConfiguration;
+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 Entity Configuration.
+ */
+public interface OIDFederationEntityConfiguration extends OverriddenIssuerProfileConfiguration, 
+    OIDCProfileConfiguration {
+    
+    /** OIDC base protocol URI. Section 4 is relevant. */
+    public static final String PROTOCOL_URI = "https://openid.net/specs/openid-federation-1_0.html";
+
+    /** ID for this profile configuration. */
+    public static final String PROFILE_ID = "http://shibboleth.net/ns/profiles/oidfed/configuration";
+
+    /**
+     * Get the authority hints to be included to the entity configuration.
+     * 
+     * @param profileRequestContext profile request context
+     * 
+     * @return authority hints to be included to the entity configuration
+     */
+    @ConfigurationSetting(name="authorityHints")
+    @Nonnull @NonnullElements @NotLive @Unmodifiable List<String> getAuthorityHints(
+            @Nullable final ProfileRequestContext profileRequestContext);
+
+    /**
+     * Get entity statement lifetime.
+     * 
+     * <p>Defaults to 24 hours.</p>
+     * 
+     * @param profileRequestContext profile request context
+     * 
+     * @return entity statement lifetime
+     */
+    @ConfigurationSetting(name="entityStatementLifetime")
+    @Positive @Nonnull Duration getEntityStatementLifetime(@Nullable final ProfileRequestContext profileRequestContext);
+
+    /**
+     * Get cached entity statement lifetime.
+     * 
+     * <p>Defaults to 10 seconds.</p>
+     * 
+     * @param profileRequestContext profile request context
+     * 
+     * @return cached entity statement lifetime
+     */
+    @ConfigurationSetting(name="cachedEntityStatementLifetime")
+    @Positive @Nonnull
+    Duration getCachedEntityStatementLifetime(@Nullable final ProfileRequestContext profileRequestContext);
+
+    /**
+     * Get the bi-function for manipulating entity statement claims set.
+     * 
+     * @param profileRequestContext profile request context
+     * 
+     * @return the bi-function for manipulating entity statement claims set
+     */
+    @ConfigurationSetting(name="entityStatementClaimsSetManipulationStrategy")
+    @Nullable BiFunction<ProfileRequestContext,Map<String,Object>,Map<String,Object>>
+        getEntityStatementClaimsSetManipulationStrategy(
+                @Nullable final ProfileRequestContext profileRequestContext);
+}
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/BuildEntityStatement.java
new file mode 100644
index 00000000..a677aed5
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/BuildEntityStatement.java
@@ -0,0 +1,359 @@
+/*
+ * 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.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;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+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.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.logic.FunctionSupport;
+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
+ */
+public class BuildEntityStatement extends AbstractProfileAction {
+
+    /** Class logger. */
+    @Nonnull private Logger log = LoggerFactory.getLogger(BuildEntityStatement.class);
+
+    /** Used to log protocol messages. */
+    @Nonnull private Logger protocolMessageLog =
+            LoggerFactory.getLogger(AbstractMessageEncoder.BASE_PROTOCOL_MESSAGE_LOGGER_CATEGORY + ".OIDFED");
+
+    /** Strategy used to obtain the response 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 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 authority hints. */
+    @Nonnull private Function<ProfileRequestContext,List<String>> authorityHintsLookupStrategy;
+
+    /** Lookup function to supply strategy bi-function for manipulating entity statement claims set. */ 
+    @Nonnull
+    private Function<ProfileRequestContext,BiFunction<ProfileRequestContext,Map<String,Object>,Map<String,Object>>>
+        entityStatementClaimsSetManipulationStrategyLookupStrategy;
+
+    /** The strategy used for manipulating the entity statement claims set. */
+    @Nullable private BiFunction<ProfileRequestContext,Map<String,Object>,Map<String,Object>> manipulationStrategy;
+
+    /** Object mapper used for pretty-printing JWT contents. */
+    @NonnullAfterInit private ObjectMapper objectMapper;
+
+    /** The generator to use. */
+    @Nullable private IdentifierGenerationStrategy idGenerator;
+    
+    /** Entity statement context. */
+    @Nullable private EntityStatementContext entityStatementCtx;
+
+    /** OIDC provider metadata to publish. */
+    @Nullable private OIDCProviderMetadata metadata;
+
+    /** Constructor. */
+    public BuildEntityStatement() {
+        entityStatementLifetimeLookupStrategy = new EntityStatementLifetimeLookupFunction();
+        issuerLookupStrategy = new IssuerLookupFunction();
+        
+        idGeneratorLookupStrategy = FunctionSupport.constant(new SecureRandomIdentifierGenerationStrategy());
+
+        final Function<ProfileRequestContext,EntityStatementContext> esccs =
+                new ChildContextLookup<>(EntityStatementContext.class, true).compose(
+                        new OutboundMessageContextLookup());
+        assert esccs != null;
+        entityStatementContextCreationStrategy = esccs; 
+
+        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.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setIdentifierGeneratorLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,IdentifierGenerationStrategy> strategy) {
+        ifInitializedThrowUnmodifiabledComponentException();
+
+        idGeneratorLookupStrategy =
+                Constraint.isNotNull(strategy, "Identifier generation strategy cannot be null");
+    }
+    
+    /**
+     * Set the strategy used to locate the issuer value to use.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setIssuerLookupStrategy(@Nonnull final Function<ProfileRequestContext,String> strategy) {
+        ifInitializedThrowUnmodifiabledComponentException();
+        
+        issuerLookupStrategy = Constraint.isNotNull(strategy, "Issuer lookup strategy cannot be null");
+    }
+    
+    /**
+     * Set the strategy used to create the {@link EntityStatementContext} to use.
+     * 
+     * @param strategy creation strategy
+     */
+    public void setEntityStatementContextCreationStrategy(
+            @Nonnull final Function<ProfileRequestContext,EntityStatementContext> strategy) {
+        ifInitializedThrowUnmodifiabledComponentException();
+        
+        entityStatementContextCreationStrategy =
+                Constraint.isNotNull(strategy, "EntityStatementContext creation strategy cannot be null");
+    }
+    
+    /**
+     * Set the lookup function to supply strategy bi-function for manipulating entity statement claims set.
+     * 
+     * @param strategy What to set
+     */
+    public void setEntityStatementClaimsSetManipulationStrategyLookupStrategy(@Nonnull final
+            Function<ProfileRequestContext,BiFunction<ProfileRequestContext,Map<String,Object>,Map<String,Object>>>
+            strategy) {
+        ifInitializedThrowUnmodifiabledComponentException();
+
+        entityStatementClaimsSetManipulationStrategyLookupStrategy =
+                Constraint.isNotNull(strategy, "Manipulation strategy lookup strategy cannot be null");
+    }
+
+    /**
+     * Set the object mapper used for pretty-printing JWT contents.
+     * 
+     * @param mapper What to set.
+     */
+    public void setObjectMapper(@Nonnull final ObjectMapper mapper) {
+        checkSetterPreconditions();
+        objectMapper = Constraint.isNotNull(mapper, "Object mapper cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        
+        if (objectMapper == null) {
+            throw new ComponentInitializationException("Object mapper cannot be null");
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        if (!super.doPreExecute(profileRequestContext)) {
+            return false;
+        }
+
+        idGenerator = idGeneratorLookupStrategy.apply(profileRequestContext);
+        if (idGenerator == null) {
+            log.error("{} No identifier generation strategy", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return false;
+        }
+
+        entityStatementCtx = entityStatementContextCreationStrategy.apply(profileRequestContext);
+        if (entityStatementCtx == null) {
+            log.error("{} Unable to create 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);
+
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+        final String issuer = issuerLookupStrategy.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()
+                .issuer(issuer)
+                .subject(issuer)
+                .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();
+        assert claimsSet != null;
+        if (manipulationStrategy != null) {
+            log.debug("{} Manipulation strategy has been set, applying it to the claims set {}", getLogPrefix(),
+                    claimsSet.toJSONObject());
+            assert manipulationStrategy != null;
+            final Map<String, Object> result = manipulationStrategy.apply(profileRequestContext,
+                    claimsSet.toJSONObject());
+            if (result == null) {
+                log.debug("{} Manipulation strategy returned null, leaving statement claims set untouched.",
+                        getLogPrefix());
+            } else {
+                log.debug("{} Applying the manipulated claims into the entity statement claims set", getLogPrefix());
+                try {
+                    final JWTClaimsSet parsedSet = JWTClaimsSet.parse(result);
+                    assert parsedSet != null;
+                    logAndConstructEntityStatement(parsedSet);
+                    return;
+                } catch (final ParseException e) {
+                    log.error("{} The resulted claims set could not be transformed into ", getLogPrefix(), e);
+                    ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_PROFILE_CONFIG);
+                    return;
+                }
+            }
+        } else {
+            log.debug("{} No manipulation strategy configured", getLogPrefix());
+        }
+        logAndConstructEntityStatement(claimsSet);
+    }
+
+    @Nonnull protected Map<String, Object> buildOpenIDProviderClaim() {
+        assert metadata != null;
+        return CollectionSupport.singletonMap("openid_provider", metadata.toJSONObject());
+    }
+
+    protected void logAndConstructEntityStatement(@Nonnull final JWTClaimsSet claimsSet) {
+        log.trace("{} Building JWT from the claims set {}", getLogPrefix(), claimsSet);
+        final JWT jwt = new PlainJWT(claimsSet);
+        assert objectMapper != null;
+        try {
+            protocolMessageLog.trace("Entity statement payload contents:\n{}",
+                    ResponseUtil.getJwtProtocolMessage(jwt, objectMapper));
+        } catch (final ParseException e) {
+            log.error("{} Could not construct protocol log message", getLogPrefix(), e);
+        }
+        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/EntityStatementContext.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/EntityStatementContext.java
new file mode 100644
index 00000000..7b6af76e
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/EntityStatementContext.java
@@ -0,0 +1,109 @@
+/*
+ * 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 javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.BaseContext;
+
+import com.nimbusds.jwt.JWT;
+import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
+
+/**
+ * Subcontext carrying information used to produce entity statements.
+ * 
+ * @since 4.3.0
+ */
+public final class EntityStatementContext extends BaseContext {
+
+    /** OIDC provider metadata. */
+    @Nullable private OIDCProviderMetadata opMetadata;
+
+    /** Lifetime of the statement. */
+    @Nullable private Duration lifetime;
+
+    /** The entity statement. */
+    @Nullable private JWT jwt;
+
+    /**
+     * Get the OIDC provider metadata.
+     * 
+     * @return the metadata
+     */
+    @Nullable public OIDCProviderMetadata getOPMetadata() {
+        return opMetadata;
+    }
+
+    /**
+     * Set the OIDC provider metadata.
+     * 
+     * @param metadata the metadata
+     * 
+     * @return this context
+     */
+    @Nonnull public EntityStatementContext setOPMetadata(@Nullable final OIDCProviderMetadata metadata) {
+        opMetadata = metadata;
+        return this;
+    }
+    
+    /**
+     * Get the entity statement JWT.
+     * 
+     * <p>May be in various states prior to signing.</p>
+     * 
+     * @return the JWT
+     */
+    @Nullable public JWT getJWT() {
+        return jwt;
+    }
+
+    /**
+     * Set the entity statement JWT.
+     * 
+     * <p>May be in various states prior to signing.</p>
+     * 
+     * @param token the JWT
+     * 
+     * @return this context
+     */
+    @Nonnull public EntityStatementContext setJWT(@Nullable final JWT token) {
+        jwt = token;
+        return this;
+    }
+    
+    /**
+     * Get the statement lifetime.
+     * 
+     * @return lifetime
+     */
+    @Nullable public Duration getLifetime() {
+        return lifetime;
+    }
+    
+    /**
+     * Set the statement lifetime.
+     * 
+     * @param lt lifetime
+     * 
+     * @return this context
+     */
+    @Nonnull public EntityStatementContext setLifetime(@Nullable final Duration lt) {
+        lifetime = lt;
+        return this;
+    }
+}
\ 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/EntityStatementUpdateStrategy.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/EntityStatementUpdateStrategy.java
new file mode 100644
index 00000000..79312677
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/EntityStatementUpdateStrategy.java
@@ -0,0 +1,59 @@
+/*
+ * 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.BiConsumer;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+
+import com.nimbusds.jwt.JWT;
+
+/**
+ * Add the {@link JWT} back to the {@link EntityStatementContext}.
+ * 
+ * @since 4.3.0
+ */
+public class EntityStatementUpdateStrategy implements BiConsumer<JWT, MessageContext> {
+
+    /** Strategy used to locate the subcontext with the statement. */
+    @Nonnull private Function<MessageContext,EntityStatementContext> entityStatementContextLookupStrategy;
+
+    /**
+     * Constructor.
+     */
+    public EntityStatementUpdateStrategy() {
+        final Function<MessageContext,EntityStatementContext> escls =
+                new ChildContextLookup<>(EntityStatementContext.class);
+        assert escls != null;
+        entityStatementContextLookupStrategy = escls;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void accept(final JWT jwt, final MessageContext messageContext) {
+        if (messageContext == null) {
+            return;
+        }
+        final EntityStatementContext entityStatementCtx = entityStatementContextLookupStrategy.apply(messageContext);
+        if (entityStatementCtx == null) {
+            return;
+        }
+        entityStatementCtx.setJWT(jwt);
+    }
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/FormOutboundFederationConfigurationResponse.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/FormOutboundFederationConfigurationResponse.java
new file mode 100644
index 00000000..c9bd012b
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/FormOutboundFederationConfigurationResponse.java
@@ -0,0 +1,139 @@
+/*
+ * 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.io.IOException;
+import java.time.Duration;
+import java.time.Instant;
+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.opensaml.storage.StorageService;
+import org.slf4j.Logger;
+
+import com.nimbusds.jwt.SignedJWT;
+import com.nimbusds.oauth2.sdk.ParseException;
+import com.nimbusds.openid.connect.sdk.federation.config.FederationEntityConfigurationSuccessResponse;
+import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
+
+import net.shibboleth.idp.plugin.oidc.op.oidfed.config.OIDFederationEntityConfiguration;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.navigate.CachedEntityStatementLifetimeLookupFunction;
+import net.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * This action builds a response for the OpenID federation configuration request. The response contains an
+ * {@link EntityStatement}.
+ * 
+ * @since 4.3.0
+ */
+public class FormOutboundFederationConfigurationResponse extends AbstractProfileAction {
+
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(FormOutboundFederationConfigurationResponse.class);
+
+    /** Strategy used to locate the subcontext to hold the statement. */
+    @Nonnull private Function<ProfileRequestContext,EntityStatementContext> entityStatementContextLookupStrategy;
+
+    @Nonnull private Function<ProfileRequestContext,Duration> storageRecordLifetimeLookupStrategy;
+    @NonnullAfterInit private StorageService storageService;
+
+    /** JWT used to build entity statement. */
+    @Nullable private SignedJWT jwt;
+
+    public FormOutboundFederationConfigurationResponse() {
+        final Function<ProfileRequestContext,EntityStatementContext> escls =
+                new ChildContextLookup<>(EntityStatementContext.class).compose(
+                        new OutboundMessageContextLookup());
+        assert escls != null;
+        entityStatementContextLookupStrategy = escls;
+        storageRecordLifetimeLookupStrategy = new CachedEntityStatementLifetimeLookupFunction();
+    }
+
+    /**
+     * Set the strategy used to locate the subcontext to hold the statement
+     * 
+     * @param strategy What to set.
+     */
+    public void setMetadataResolver(@Nonnull final Function<ProfileRequestContext,EntityStatementContext> strategy) {
+        checkSetterPreconditions();
+        entityStatementContextLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null!");
+    }
+
+    public void setStorageService(@Nonnull final StorageService service) {
+        checkSetterPreconditions();
+        storageService = Constraint.isNotNull(service, "Storage service 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;
+        }
+        final Duration lifetime = storageRecordLifetimeLookupStrategy.apply(profileRequestContext);
+        final FederationEntityConfigurationSuccessResponse response =
+                new FederationEntityConfigurationSuccessResponse(entityStatement);
+        try {
+            assert jwt != null;
+            final String serializedJwt = jwt.serialize();
+            assert serializedJwt != null;
+            storageService.create(OIDFederationEntityConfiguration.PROFILE_ID, "entityStatement", serializedJwt, Instant.now().plus(lifetime).toEpochMilli());
+        } catch (final IOException e) {
+            log.warn("{} Could not store the entity statement into storage service", getLogPrefix(), e);
+        }
+        log.debug("{} Response stored into the cache", 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/impl/InitializeEntityStatementContext.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/InitializeEntityStatementContext.java
new file mode 100644
index 00000000..ec17c6eb
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/InitializeEntityStatementContext.java
@@ -0,0 +1,144 @@
+/*
+ * 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.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.OutboundMessageContextLookup;
+import org.slf4j.Logger;
+
+import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
+
+import net.shibboleth.idp.plugin.oidc.op.metadata.resolver.ProviderMetadataResolver;
+import net.shibboleth.idp.profile.AbstractProfileAction;
+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;
+import net.shibboleth.shared.resolver.ResolverException;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.action.ActionSupport;
+
+/**
+ * Action that creates and initializes the {@link EntityStatementContext}.
+ *
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#INVALID_PROFILE_CTX}
+ * @event {@link EventIds#IO_ERROR}
+ * 
+ * @since 4.3.0
+ */
+public class InitializeEntityStatementContext extends AbstractProfileAction {
+
+    /** Class logger. */
+    @Nonnull private Logger log = LoggerFactory.getLogger(InitializeEntityStatementContext.class);
+
+    /** The resolver for the metadata that is being distributed. */
+    @NonnullAfterInit private ProviderMetadataResolver metadataResolver;
+
+    /** Strategy used to create the subcontext to hold the statement. */
+    @Nonnull private Function<ProfileRequestContext,EntityStatementContext> entityStatementContextCreationStrategy;
+
+    /** Entity statement context. */
+    @Nullable private EntityStatementContext entityStatementCtx;
+
+    /** Constructor. */
+    public InitializeEntityStatementContext() {
+        final Function<ProfileRequestContext,EntityStatementContext> esccs =
+                new ChildContextLookup<>(EntityStatementContext.class, true).compose(
+                        new OutboundMessageContextLookup());
+        assert esccs != null;
+        entityStatementContextCreationStrategy = esccs; 
+    }
+
+    /**
+     * Set the resolver for the metadata that is being distributed.
+     * 
+     * @param resolver What to set.
+     */
+    public void setMetadataResolver(@Nonnull final ProviderMetadataResolver resolver) {
+        metadataResolver = Constraint.isNotNull(resolver, "The metadata resolver cannot be null!");
+    }
+
+    /**
+     * Set the strategy used to create the {@link EntityStatementContext} to use.
+     * 
+     * @param strategy creation strategy
+     */
+    public void setEntityStatementContextCreationStrategy(
+            @Nonnull final Function<ProfileRequestContext,EntityStatementContext> strategy) {
+        ifInitializedThrowUnmodifiabledComponentException();
+        
+        entityStatementContextCreationStrategy =
+                Constraint.isNotNull(strategy, "EntityStatementContext creation strategy cannot be null");
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+
+        if (metadataResolver == null) {
+            throw new ComponentInitializationException("The metadata resolver cannot be null!");
+        }
+    }
+
+ // Checkstyle: CyclomaticComplexity|MethodLength OFF
+    /** {@inheritDoc} */
+    @Override
+    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        if (!super.doPreExecute(profileRequestContext)) {
+            return false;
+        }
+
+        entityStatementCtx = entityStatementContextCreationStrategy.apply(profileRequestContext);
+        if (entityStatementCtx == null) {
+            log.error("{} Unable to create EntityStatementContext", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return false;
+        }
+
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+        final OIDCProviderMetadata metadata;
+        try {
+            metadata = metadataResolver.resolveSingle(profileRequestContext);
+        } catch (final ResolverException e) {
+            log.error("{} Could not resolve provider metadata", getLogPrefix(), e);
+            ActionSupport.buildEvent(profileRequestContext, EventIds.IO_ERROR);
+            return;
+        }
+        if (metadata == null) {
+            log.error("{} Could not resolve provider metadata", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.IO_ERROR);
+            return;
+        }
+
+        assert entityStatementCtx != null;
+        entityStatementCtx.setOPMetadata(metadata);
+    }
+        
+}
\ 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/JWTClaimsSetFromEntityStatementLookupFunction.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/JWTClaimsSetFromEntityStatementLookupFunction.java
new file mode 100644
index 00000000..b81ae0f7
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/JWTClaimsSetFromEntityStatementLookupFunction.java
@@ -0,0 +1,87 @@
+/*
+ * 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.text.ParseException;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.slf4j.Logger;
+
+import com.nimbusds.jwt.JWT;
+import com.nimbusds.jwt.JWTClaimsSet;
+
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/** 
+ * Extract the {@link JWTClaimsSet} from the JWT in {@link EntityStatementContext}.
+ * 
+ * @since 4.3.0
+ */
+public class JWTClaimsSetFromEntityStatementLookupFunction implements Function<MessageContext, JWTClaimsSet> {
+
+    /** Class logger. */
+    @Nonnull private Logger log = LoggerFactory.getLogger(JWTClaimsSetFromEntityStatementLookupFunction.class);
+
+    /** Strategy used to locate the subcontext with the token. */
+    @Nonnull private Function<MessageContext,EntityStatementContext> entityStatementContextLookupStrategy;
+
+    /**
+     * Constructor.
+     */
+    public JWTClaimsSetFromEntityStatementLookupFunction() {
+        // message context -> OIDC response context -> ATC
+        final Function<MessageContext,EntityStatementContext> escl = new ChildContextLookup<>(EntityStatementContext.class);
+        assert escl != null;
+        entityStatementContextLookupStrategy = escl;
+    }
+    
+    /**
+     * Set the strategy used to lookup the {@link EntityStatementContext} to use.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setEntityStatementContextCreationStrategy(
+            @Nonnull final Function<MessageContext,EntityStatementContext> strategy) {
+        entityStatementContextLookupStrategy =
+                Constraint.isNotNull(strategy, "EntityStatementContext lookup strategy cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    @Nullable public JWTClaimsSet apply(@Nullable final MessageContext messageContext) {
+        if (messageContext == null) {
+            return null;
+        }
+        final EntityStatementContext entityStatementCtx = entityStatementContextLookupStrategy.apply(messageContext);
+        if (entityStatementCtx == null) {
+            return null;
+        }
+        final JWT jwt = entityStatementCtx.getJWT();
+        try {
+            if (jwt != null) {
+                return jwt.getJWTClaimsSet();
+            }
+        } catch (final ParseException e) {
+            log.error("Could not fetch the claims set from entity statement", e);
+        }
+        return null;
+    }
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/AuthorityHintsLookupFunction.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/AuthorityHintsLookupFunction.java
new file mode 100644
index 00000000..a516768b
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/AuthorityHintsLookupFunction.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.oidfed.profile.navigate;
+
+import java.util.List;
+
+import javax.annotation.Nullable;
+
+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.OIDFederationEntityConfiguration;
+
+/**
+ * A function that obtains {@link OIDFederationEntityConfiguration#getAuthorityHints(ProfileRequestContext)}.
+ * 
+ * <p>If a specific setting is unavailable, a null value is returned.</p>
+ */
+public class AuthorityHintsLookupFunction extends AbstractRelyingPartyLookupFunction<List<String>> {
+
+    /** {@inheritDoc} */
+    @Override
+    @Nullable public List<String> apply(@Nullable final ProfileRequestContext input) {
+        final RelyingPartyContext rpc = getRelyingPartyContextLookupStrategy().apply(input);
+        if (rpc != null) {
+            final ProfileConfiguration pc = rpc.getProfileConfig();
+            if (pc instanceof OIDFederationEntityConfiguration ofec) {
+                return ofec.getAuthorityHints(input);
+            }
+        }
+        return 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/CachedEntityStatementLifetimeLookupFunction.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/CachedEntityStatementLifetimeLookupFunction.java
new file mode 100644
index 00000000..c3bd31a8
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/CachedEntityStatementLifetimeLookupFunction.java
@@ -0,0 +1,52 @@
+/*
+ * 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.time.Duration;
+
+import javax.annotation.Nullable;
+
+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.OIDFederationEntityConfiguration;
+
+/**
+ * A function that returns
+ * {@link OIDFederationEntityConfiguration#getCachedEntityStatementLifetime(ProfileRequestContext)} if such a profile
+ * is available from a {@link RelyingPartyContext} obtained via a lookup function, by default a child of the
+ * {@link ProfileRequestContext}.
+ * 
+ * <p>If a specific setting is unavailable, a null value is returned.</p>
+ */
+public class CachedEntityStatementLifetimeLookupFunction extends AbstractRelyingPartyLookupFunction<Duration> {
+
+    /** {@inheritDoc} */
+    @Override
+    @Nullable public Duration apply(@Nullable final ProfileRequestContext input) {
+        final RelyingPartyContext rpc = getRelyingPartyContextLookupStrategy().apply(input);
+        if (rpc != null) {
+            final ProfileConfiguration pc = rpc.getProfileConfig();
+            if (pc instanceof OIDFederationEntityConfiguration ofec) {
+                return ofec.getCachedEntityStatementLifetime(input);
+            }
+        }
+        
+        return 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/EntityStatementClaimsSetManipulationStrategyLookupFunction.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/EntityStatementClaimsSetManipulationStrategyLookupFunction.java
new file mode 100644
index 00000000..3cdb4397
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/EntityStatementClaimsSetManipulationStrategyLookupFunction.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.oidfed.profile.navigate;
+
+import java.util.Map;
+import java.util.function.BiFunction;
+
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import net.shibboleth.idp.plugin.oidc.op.oidfed.config.OIDFederationEntityConfiguration;
+import net.shibboleth.profile.config.ProfileConfiguration;
+import net.shibboleth.profile.context.RelyingPartyContext;
+import net.shibboleth.profile.context.navigate.AbstractRelyingPartyLookupFunction;
+
+/**
+ * A function that returns
+ * {@link OIDFederationEntityConfiguration#getEntityStatementClaimsSetManipulationStrategy(ProfileRequestContext)} if
+ * such a profile is available from a {@link RelyingPartyContext} obtained via a lookup function, by default a child of
+ * the {@link ProfileRequestContext}.
+ * 
+ * <p>If a specific setting is unavailable, a null value is returned.</p>
+ */
+public class EntityStatementClaimsSetManipulationStrategyLookupFunction  extends
+    AbstractRelyingPartyLookupFunction<BiFunction<ProfileRequestContext,Map<String,Object>,Map<String,Object>>> {
+
+    /** {@inheritDoc} */
+    @Override
+    @Nullable public BiFunction<ProfileRequestContext,Map<String,Object>,Map<String,Object>> apply(
+            @Nullable final ProfileRequestContext input) {
+        final RelyingPartyContext rpc = getRelyingPartyContextLookupStrategy().apply(input);
+        if (rpc != null) {
+            final ProfileConfiguration pc = rpc.getProfileConfig();
+            if (pc instanceof OIDFederationEntityConfiguration ofec) {
+                return ofec.getEntityStatementClaimsSetManipulationStrategy(input);
+            }
+        }
+        return 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/EntityStatementLifetimeLookupFunction.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/EntityStatementLifetimeLookupFunction.java
new file mode 100644
index 00000000..5163a85f
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/EntityStatementLifetimeLookupFunction.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.oidfed.profile.navigate;
+
+import java.time.Duration;
+
+import javax.annotation.Nullable;
+
+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.OIDFederationEntityConfiguration;
+
+/**
+ * A function that returns {@link OIDFederationEntityConfiguration#getEntityStatementLifetime(ProfileRequestContext)}
+ * if such a profile is available from a {@link RelyingPartyContext} obtained via a lookup function, by default a child
+ * of the {@link ProfileRequestContext}.
+ * 
+ * <p>If a specific setting is unavailable, a null value is returned.</p>
+ */
+public class EntityStatementLifetimeLookupFunction extends AbstractRelyingPartyLookupFunction<Duration> {
+
+    /** {@inheritDoc} */
+    @Override
+    @Nullable public Duration apply(@Nullable final ProfileRequestContext input) {
+        final RelyingPartyContext rpc = getRelyingPartyContextLookupStrategy().apply(input);
+        if (rpc != null) {
+            final ProfileConfiguration pc = rpc.getProfileConfig();
+            if (pc instanceof OIDFederationEntityConfiguration ofec) {
+                return ofec.getEntityStatementLifetime(input);
+            }
+        }
+        
+        return null;
+    }
+
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
index 1a456b6a..5137d750 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
@@ -872,4 +872,15 @@
                 p:customMetadataPolicyOperators="#{getObject('shibboleth.oidc.RedirectUriValidator.MetadataPolicyCustomOperators') ?: getObject('shibboleth.oidc.DefaultMetadataPolicyCustomOperators')}"/>
         </property>
     </bean>
+
+    <bean id="shibboleth.oidc.DefaultOpenIdConfigurationResolver"
+        class="net.shibboleth.idp.plugin.oidc.op.metadata.impl.DynamicFilesystemProviderMetadataResolver"
+        p:minRefreshDelay="%{idp.oidc.config.minRefreshDelay:PT5M}"
+        p:maxRefreshDelay="%{idp.oidc.config.maxRefreshDelay:PT4H}"
+        c:metadata="#{getObject('shibboleth.oidc.OpenIDConfiguration') ?: getObject('DefaultMetadataSkeleton')}"
+        p:dynamicValueResolvers-ref="#{'%{idp.oidc.discovery.resolver.values:shibboleth.oidc.discovery.DefaultDynamicValueResolvers}'.trim()}"/>
+
+    <bean id="DefaultMetadataSkeleton" class="org.springframework.core.io.FileSystemResource" lazy-init="true"
+        c:path="%{idp.oidc.discovery.template:%{idp.home}/static/openid-configuration.json}" />
+
 </beans>
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/abstract-api-info/oidc-abstract-api-info-flow.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/abstract-api-info/oidc-abstract-api-info-flow.xml
index 1f6bd85c..7ed5e680 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/abstract-api-info/oidc-abstract-api-info-flow.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/abstract-api-info/oidc-abstract-api-info-flow.xml
@@ -17,13 +17,16 @@
     </action-state>
 
     <decision-state id="CheckInboundInterceptContext">
+        <on-entry>
+            <evaluate expression="flowRequestContext.getFlowScope().get('transitionAfterInboundIntercept') != null ? flowRequestContext.getFlowScope().get('transitionAfterInboundIntercept') : 'BuildResponseMessage'" result="flowScope.transitionTargetAfterInboundIntercept"/>
+        </on-entry>
         <if test="opensamlProfileRequestContext.ensureSubcontext(T(net.shibboleth.idp.profile.context.ProfileInterceptorContext)).getAvailableFlows().isEmpty()"
-            then="BuildResponseMessage" else="DoInboundInterceptSubflow" />
+            then="#{transitionTargetAfterInboundIntercept}" else="DoInboundInterceptSubflow" />
     </decision-state>
 
     <subflow-state id="DoInboundInterceptSubflow" subflow="intercept">
         <input name="calledAsSubflow" value="true" />
-        <transition on="proceed" to="BuildResponseMessage" />
+        <transition on="proceed" to="#{transitionTargetAfterInboundIntercept}" />
     </subflow-state>
 
     <bean-import resource="classpath:/META-INF/net/shibboleth/idp/flows/oidc/abstract-api-info/oidc-abstract-api-info-beans.xml" />
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/configuration/configuration-beans.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/configuration/configuration-beans.xml
index e67bcd5e..58ab20ea 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/configuration/configuration-beans.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/configuration/configuration-beans.xml
@@ -28,14 +28,4 @@
     <bean id="FormOutboundMessage" class="net.shibboleth.idp.plugin.oidc.op.profile.impl.FormOutboundDiscoveryResponse"
         scope="prototype" p:metadataResolver-ref="#{'%{idp.oidc.discovery.resolver:shibboleth.oidc.DefaultOpenIdConfigurationResolver}'.trim()}" />
 
-    <bean id="shibboleth.oidc.DefaultOpenIdConfigurationResolver"
-        class="net.shibboleth.idp.plugin.oidc.op.metadata.impl.DynamicFilesystemProviderMetadataResolver"
-        p:minRefreshDelay="%{idp.oidc.config.minRefreshDelay:PT5M}"
-        p:maxRefreshDelay="%{idp.oidc.config.maxRefreshDelay:PT4H}"
-        c:metadata="#{getObject('shibboleth.oidc.OpenIDConfiguration') ?: getObject('DefaultMetadataSkeleton')}"
-        p:dynamicValueResolvers-ref="#{'%{idp.oidc.discovery.resolver.values:shibboleth.oidc.discovery.DefaultDynamicValueResolvers}'.trim()}"/>
-
-    <bean id="DefaultMetadataSkeleton" class="org.springframework.core.io.FileSystemResource" lazy-init="true"
-        c:path="%{idp.oidc.discovery.template:%{idp.home}/static/openid-configuration.json}" />
-
 </beans>
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
new file mode 100644
index 00000000..a4dbb7d6
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/entity-configuration/entity-configuration-beans.xml
@@ -0,0 +1,98 @@
+<?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.OIDFederationEntityConfiguration).PROFILE_ID}" />
+
+    <bean id="shibboleth.oidc.loggingId" class="java.lang.String" c:_0="%{idp.service.logging.oidfedconfig:OIDFED.Configuration}" />
+
+    <util:constant id="shibboleth.metrics.ProfileCounter"
+        static-field="net.shibboleth.idp.plugin.oidc.op.oidfed.config.DefaultOIDFederationEntityConfiguration.PROFILE_COUNTER" />
+
+    <bean id="BuildErrorResponseFromEvent"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.BuildJSONErrorResponseFromEvent" scope="prototype"
+        p:defaultStatusCode="500" p:defaultCode="server_error"
+        p:httpServletResponseSupplier-ref="shibboleth.HttpServletResponseSupplier">
+        <property name="eventContextLookupStrategy">
+            <bean class="net.shibboleth.idp.profile.context.navigate.WebFlowCurrentEventLookupFunction" />
+        </property>
+    </bean>
+
+    <alias alias="ResponseCacheStorageService" name="%{idp.oidfed.entity.configurationCache:shibboleth.StorageService}" />
+
+    <bean id="InitializeEntityStatementContext"
+        class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.InitializeEntityStatementContext"
+        p:metadataResolver-ref="#{'%{idp.oidc.discovery.resolver:shibboleth.oidc.DefaultOpenIdConfigurationResolver}'.trim()}"/>
+
+    <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="SelectOidcConfigurationProfileConfiguration"
+        class="net.shibboleth.idp.profile.impl.SelectProfileConfiguration" scope="prototype"
+        p:profileId="#{T(net.shibboleth.oidc.profile.config.OIDCProviderInformationConfiguration).PROFILE_ID}" />
+
+    <bean id="ConfigurationRelyingPartyCreationStrategy" parent="shibboleth.Functions.Compose"
+        c:g-ref="shibboleth.ChildLookupOrCreate.RelyingPartyContext"
+        c:f-ref="shibboleth.MessageContextLookup.Outbound" />
+
+    <bean id="BuildEntityStatement"
+        class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.BuildEntityStatement" 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.FormOutboundFederationConfigurationResponse"
+        scope="prototype" p:storageService-ref="ResponseCacheStorageService">
+    </bean>
+
+</beans>
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/entity-configuration/entity-configuration-flow.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/entity-configuration/entity-configuration-flow.xml
new file mode 100644
index 00000000..b89baf62
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/entity-configuration/entity-configuration-flow.xml
@@ -0,0 +1,54 @@
+<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-info">
+
+    <action-state id="InitializeMandatoryContexts">
+        <on-entry>
+            <set name="flowScope.transitionAfterInboundIntercept" value="'CheckIfValidCachedResponseExists'" />
+        </on-entry>
+    </action-state>
+
+    <decision-state id="CheckIfValidCachedResponseExists">
+        <on-entry>
+            <set name="requestScope.storageRecord"
+                value="ResponseCacheStorageService.read(T(net.shibboleth.idp.plugin.oidc.op.oidfed.config.OIDFederationEntityConfiguration).PROFILE_ID, 'entityStatement')"/>
+            <set name="requestScope.cachedEntityStatementJwt"
+                value="storageRecord == null ? null : T(com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement).parse(storageRecord.getValue())" />
+        </on-entry>
+        <if test="cachedEntityStatementJwt != null"
+            then="UseCachedEntityStatement"
+            else="InitializeEntityStatementContext" />
+    </decision-state>
+
+    <action-state id="UseCachedEntityStatement">
+        <on-entry>
+            <set name="requestScope.cachedResponseMessage"
+                value="new com.nimbusds.openid.connect.sdk.federation.config.FederationEntityConfigurationSuccessResponse(cachedEntityStatementJwt)" />
+            <evaluate expression="opensamlProfileRequestContext.ensureOutboundMessageContext().setMessage(cachedResponseMessage)"/>
+        </on-entry>
+        <evaluate expression="'proceed'"/>
+        <transition on="proceed" to="PopulateOutboundInterceptContext"/>
+    </action-state>
+
+    <action-state id="InitializeEntityStatementContext">
+        <evaluate expression="SelectOidcConfigurationProfileConfiguration" />
+        <evaluate expression="InitializeEntityStatementContext"/>
+        <evaluate expression="SelectProfileConfiguration" />
+        <evaluate expression="'proceed'" />
+        
+        <transition on="proceed" to="BuildEntityConfiguration" />
+    </action-state>
+
+    <action-state id="BuildEntityConfiguration">
+        <evaluate expression="PopulateEntityStatementSignatureSigningParameters" />
+        <evaluate expression="BuildEntityStatement" />
+        <evaluate expression="SignEntityStatement" />
+        <evaluate expression="'proceed'" />
+        
+        <transition on="proceed" to="BuildResponseMessage"/>
+    </action-state>
+
+    <bean-import resource="entity-configuration-beans.xml" />
+
+</flow>
\ No newline at end of file
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 4f8bfe96..6063ec50 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
@@ -102,6 +102,12 @@
         p:requestUriType="%{idp.oauth2.par.requestUriType:}"
         p:requestUriLifetime="%{idp.oauth2.par.requestUriLifetime:PT1M}"/>
 
+    <bean id="OIDFED.Configuration" parent="AbstractOIDCProfile" lazy-init="true"
+        class="net.shibboleth.idp.plugin.oidc.op.oidfed.config.DefaultOIDFederationEntityConfiguration"
+        p:issuer-ref="shibboleth.oidc.issuer"
+        p:authorityHints="%{idp.oidfed.entity.authorityHints:https://example.org}"
+        p:securityConfiguration-ref="shibboleth.oidc.federation.DefaultSecurityConfiguration" />
+
     <bean id="DefaultLogoutHintMatchingPredicate"
           class="net.shibboleth.idp.plugin.oidc.op.profile.logic.DefaultLogoutHintMatchingPredicate"/>
 
@@ -1113,4 +1119,55 @@
         c:sealer-ref="DefaultDPoPNonceSealer">
 
     </bean>
+
+    <bean id="shibboleth.oidc.federation.SigningConfiguration"
+        parent="shibboleth.oidc.BasicSignatureSigningConfiguration"
+        p:signingCredentials-ref="shibboleth.oidc.federation.SigningCredentialsFactory">
+        <property name="signatureAlgorithms">
+            <list>
+                <util:constant
+                    static-field="net.shibboleth.oidc.jwa.support.SignatureConstants.ALGO_ID_SIGNATURE_RS_256" />
+                <util:constant
+                    static-field="net.shibboleth.oidc.jwa.support.SignatureConstants.ALGO_ID_SIGNATURE_RS_384" />
+                <util:constant
+                    static-field="net.shibboleth.oidc.jwa.support.SignatureConstants.ALGO_ID_SIGNATURE_RS_512" />
+                <util:constant
+                    static-field="net.shibboleth.oidc.jwa.support.SignatureConstants.ALGO_ID_SIGNATURE_ES_256" />
+                <util:constant
+                    static-field="net.shibboleth.oidc.jwa.support.SignatureConstants.ALGO_ID_SIGNATURE_ES_384" />
+                <util:constant
+                    static-field="net.shibboleth.oidc.jwa.support.SignatureConstants.ALGO_ID_SIGNATURE_ES_512" />
+                <util:constant
+                    static-field="net.shibboleth.oidc.jwa.support.SignatureConstants.ALGO_ID_SIGNATURE_PS_256" />
+                <util:constant
+                    static-field="net.shibboleth.oidc.jwa.support.SignatureConstants.ALGO_ID_SIGNATURE_PS_384" />
+                <util:constant
+                    static-field="net.shibboleth.oidc.jwa.support.SignatureConstants.ALGO_ID_SIGNATURE_PS_512" />
+            </list>
+        </property>
+    </bean>
+
+    <bean id="shibboleth.oidc.federation.SigningCredentialsFactory"
+        class="net.shibboleth.oidc.profile.config.CredentialsListFactory"
+        c:_0="#{getObject('shibboleth.oidfed.SigningCredentials') ?: getObject('shibboleth.oidc.SigningCredentials')}" />
+
+    <bean id="shibboleth.oidc.federation.DefaultSecurityConfiguration"
+        class="net.shibboleth.oidc.profile.config.JSONSecurityConfiguration" c:clockSkew="%{idp.policy.clockSkew:PT1M}">
+        <constructor-arg name="idGenerator">
+                <bean 
+                    class="net.shibboleth.shared.security.IdentifierGenerationStrategy" factory-method="getInstance">
+                <constructor-arg>
+                    <util:constant
+                        static-field="net.shibboleth.shared.security.IdentifierGenerationStrategy.ProviderType.SECURE" />
+                </constructor-arg>
+                </bean>
+        </constructor-arg>
+        <property name="jwtSignatureSigningConfiguration">
+            <ref bean="#{'%{idp.oidfed.signing.config:shibboleth.oidc.federation.SigningConfiguration}'.trim()}" />
+        </property>
+        <property name="jwtSignatureValidationConfiguration">
+            <ref bean="#{'%{idp.oidfed.validation.config:shibboleth.oidc.SignatureValidationConfiguration}'.trim()}" />
+        </property>
+    </bean>
+
 </beans>
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/FederationConfigurationFlowTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/FederationConfigurationFlowTest.java
new file mode 100644
index 00000000..58be9d19
--- /dev/null
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/FederationConfigurationFlowTest.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;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
+import org.springframework.webflow.executor.FlowExecutionResult;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.nimbusds.jose.Algorithm;
+import com.nimbusds.oauth2.sdk.ParseException;
+import com.nimbusds.oauth2.sdk.Response;
+import com.nimbusds.oauth2.sdk.id.Issuer;
+import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
+import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
+
+/**
+ * Unit test for the entity configuration flow.
+ */
+public class FederationConfigurationFlowTest extends AbstractOidcFlowTest {
+
+    public static final String FLOW_ID = "oidfed/entity-configuration";
+
+    protected FederationConfigurationFlowTest() {
+        super(FLOW_ID);
+    }
+
+    @Test
+    public void testOutputAndCaching() throws ParseException, IOException, InterruptedException {
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        final Response response = parseResponse(result);
+        Assert.assertTrue(response.indicatesSuccess());
+        assertEntityStatement(response);
+        
+        final FlowExecutionResult result2 = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        final Response response2 = parseResponse(result2);
+        Assert.assertEquals(response2.toHTTPResponse().getContent(), response.toHTTPResponse().getContent());
+        
+        Thread.sleep(2000);
+        final FlowExecutionResult result3 = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        final Response response3 = parseResponse(result3);
+        assertEntityStatement(response3);
+        Assert.assertNotEquals(response3.toHTTPResponse().getContent(), response.toHTTPResponse().getContent());
+    }
+
+    protected void assertEntityStatement(final Response response) throws ParseException {
+        final EntityStatement entityStatement = EntityStatement.parse(response.toHTTPResponse().getContent());
+        final OIDCProviderMetadata metadata = entityStatement.getClaimsSet().getOPMetadata();
+        Assert.assertEquals(metadata.getIssuer(), new Issuer("https://op.example.org"));;
+        // all but RSA-OAEP-384 as it's excluded in test relying-party.xml
+        final List<String> jweAlgs = Arrays.asList("RSA1_5", "RSA-OAEP", "RSA-OAEP-256", "RSA-OAEP-512",
+                "A128KW", "A192KW", "A256KW", "A128GCMKW", "A192GCMKW", "A256GCMKW", "ECDH-ES", "ECDH-ES+A128KW",
+                "ECDH-ES+A192KW", "ECDH-ES+A256KW");
+        // all but A192CBC-HS384 as it's excluded in test relying-party.xml
+        final List<String> jweEncs = Arrays.asList("A128CBC-HS256", "A256CBC-HS512", "A128GCM", "A192GCM", "A256GCM");
+        // all but ES384 as it's excluded in test relying-party.xml
+        final List<String> jwsAlgs = Arrays.asList("RS256", "RS384", "RS512", "ES256", "ES512", "HS256", "HS384",
+                "HS512", "PS256", "PS384", "PS512");
+        Assert.assertNotNull(metadata.getIDTokenJWEAlgs());
+        Assert.assertTrue(containsAll(metadata.getIDTokenJWEAlgs(), jweAlgs));
+        Assert.assertNotNull(metadata.getIDTokenJWEAlgs());
+        Assert.assertTrue(containsAll(metadata.getIDTokenJWEEncs(), jweEncs));
+        Assert.assertNotNull(metadata.getIDTokenJWEEncs());
+        Assert.assertTrue(containsAll(metadata.getIDTokenJWSAlgs(), jwsAlgs));
+        Assert.assertNotNull(metadata.getUserInfoJWEAlgs());
+        Assert.assertTrue(containsAll(metadata.getUserInfoJWEAlgs(), jweAlgs));
+        Assert.assertNotNull(metadata.getUserInfoJWEAlgs());
+        Assert.assertTrue(containsAll(metadata.getUserInfoJWEEncs(), jweEncs));
+        Assert.assertNotNull(metadata.getUserInfoJWEEncs());
+        Assert.assertTrue(containsAll(metadata.getUserInfoJWSAlgs(), jwsAlgs));
+        Assert.assertNotNull(metadata.getCustomParameter("STATIC_TEST_ATTRIBUTE"));
+        Assert.assertEquals(metadata.getCustomParameter("STATIC_TEST_ATTRIBUTE"), "TestValue");
+
+    }
+
+    protected boolean containsAll(Collection<? extends Algorithm> algs, Collection<String> strings) {
+        final List<String> algStrings = new ArrayList<>();
+        for (final Algorithm alg : algs) {
+            algStrings.add(alg.toString());
+        }
+        return strings.size() == algStrings.size() ? strings.containsAll(strings) : false;
+    }
+
+}
diff --git a/idp-oidc-extension-impl/src/test/resources/credentials/fed-signing-es256.jwk b/idp-oidc-extension-impl/src/test/resources/credentials/fed-signing-es256.jwk
new file mode 100644
index 00000000..a70c96c8
--- /dev/null
+++ b/idp-oidc-extension-impl/src/test/resources/credentials/fed-signing-es256.jwk
@@ -0,0 +1,10 @@
+{
+  "kty": "EC",
+  "d": "CO-ctmQcB-hS042i2omOIPpaaAaKkBAU6s_v4W09oA0",
+  "use": "sig",
+  "crv": "P-256",
+  "kid": "fedtestkeyES256",
+  "x": "2uzfE1oK0cf1_c11SFc9vFdGLnJoH3e0AKTrGPAmUis",
+  "y": "14410NGKqwLM58b26ZcvGOruFixpHt_SJTw8I5wwgLQ",
+  "alg": "ES256"
+}
diff --git a/idp-oidc-extension-impl/src/test/resources/credentials/fed-signing-es384.jwk b/idp-oidc-extension-impl/src/test/resources/credentials/fed-signing-es384.jwk
new file mode 100644
index 00000000..c42f913f
--- /dev/null
+++ b/idp-oidc-extension-impl/src/test/resources/credentials/fed-signing-es384.jwk
@@ -0,0 +1,9 @@
+{
+  "kty": "EC",
+  "d": "e65hCxxbNq5gubmkgZD73A1cDf_GfGzkl4KZtbRg0GxAktztyDg4pI4bcxXaUNOb",
+  "use": "sig",
+  "crv": "P-384",
+  "kid": "fedtestkeyES384",
+  "x": "uVsAjiFw4Hv0Kcwl2532baUKPTzDht2966ar_pJ8ZdAzquFwJPdRjCfpbkqZUi46",
+  "y": "yp3W3Cmc1QQptLC3s072Iy69l1ubx_WSFRivMYqCpK4Ec89HKvYh3mTKcfjHvk2l"
+}
diff --git a/idp-oidc-extension-impl/src/test/resources/credentials/fed-signing-es521.jwk b/idp-oidc-extension-impl/src/test/resources/credentials/fed-signing-es521.jwk
new file mode 100644
index 00000000..b42d56a4
--- /dev/null
+++ b/idp-oidc-extension-impl/src/test/resources/credentials/fed-signing-es521.jwk
@@ -0,0 +1,9 @@
+{
+  "kty": "EC",
+  "d": "ADaJK1sgPtlu4xAFGmb8scq8XGujamVjP3z7Xr4xErwuurSynn8sNtZKX8SfoId9syS27VLFHe12CbeBR6nbReFv",
+  "use": "sig",
+  "crv": "P-521",
+  "kid": "fedtestkeyES512",
+  "x": "AKObj9VTXWndDB7RC9dqSEkEsCqYgOHxq9AgvlDA8XBKxPzp39XrnBD0CMFy0C1HFvoiFKh9lPXJewkkruAOLW-6",
+  "y": "AMG6cRDBekWfD8imLDkBCmm-mtI16mFbifxZ06bgI5GwdyRTIMYUaBizmOzRK038Am4h6EjF8RCFr7383iKcqGZt"
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/test/resources/credentials/fed-signing-rs.jwk b/idp-oidc-extension-impl/src/test/resources/credentials/fed-signing-rs.jwk
new file mode 100644
index 00000000..a6297ee6
--- /dev/null
+++ b/idp-oidc-extension-impl/src/test/resources/credentials/fed-signing-rs.jwk
@@ -0,0 +1,8 @@
+{
+  "kty": "RSA",
+  "d": "gv7aqFcXV86jDcCn6-JCqEEIRcv1Rh1AEv4dKziFzQal1nROliDdtkJjELpOYlFY9CgI-xAXt8ivwJ4q1eA_G9WTId7qLxPdcQW4QjfRl8VVEPUhka6Gc8y95WUO4VONEwzZnZ4V7KobE0QGADXvXUw3MtIZdGgvRCS-6avQXITjhTnlkUONxeqpy2BE6l0cI8GSM1vlLy66vjsQ06aAizMB-g3yMMpbKNd73oYgrdpEjAtddH3-sLhv_TG7pMlbB_etnPGkWKdIbpvTKr2P2oZN_8Qvq7G4ETIe9nIv7i8T7GXZfTxWspYkszbrpRACM9Ic8fSctvil2j013JeSgQ",
+  "e": "AQAB",
+  "use": "sig",
+  "kid": "fedtestkeyRS",
+  "n": "pNf03ghVzMAw5sWrwDAMAZdSYNY2q7OVlxMInljMgz8XB5mf8XKH3EtP7AKrb8IAf7rGhfuH3T1N1C7F-jwIeYjXxMm2nIAZ0hXApgbccvBpf4n2H7IZflMjt4A3tt587QQSxQ069drCP4sYevxhTcLplJy6RWA0cLj-5CHyWy94zPeeA4GRd6xgHFLz0RNiSF0pF0kE4rmRgQVZ-b4_BmD9SsWnIpwhms5Ihciw36WyAGQUeZqULGsfwAMwlNLIaTCBLAoRgv370p-XsLrgz86pTkNBJqXP5GwI-ZfgiLmJuHjQ9l85KqHM87f-QdsqiV8KoRcslgXPqb6VOTJBVw"
+}
diff --git a/idp-oidc-extension-impl/src/test/resources/net/shibboleth/idp/module/conf/oidc-credentials.xml b/idp-oidc-extension-impl/src/test/resources/net/shibboleth/idp/module/conf/oidc-credentials.xml
index 663dc553..2652e658 100644
--- a/idp-oidc-extension-impl/src/test/resources/net/shibboleth/idp/module/conf/oidc-credentials.xml
+++ b/idp-oidc-extension-impl/src/test/resources/net/shibboleth/idp/module/conf/oidc-credentials.xml
@@ -29,6 +29,18 @@
     <bean id="shibboleth.oidc.DefaultECEncryptionCredential" parent="shibboleth.JWKCredential"
         p:resource="/credentials/idp-encryption-ec.jwk" />
 
+    <bean id="shibboleth.oidfed.DefaultRSSigningCredential" parent="shibboleth.JWKCredential"
+        p:resource="/credentials/fed-signing-rs.jwk" />
+
+    <bean id="shibboleth.oidfed.DefaultES256SigningCredential" parent="shibboleth.JWKCredential"
+        p:resource="/credentials/fed-signing-es256.jwk" />
+
+    <bean id="shibboleth.oidfed.DefaultES384SigningCredential" parent="shibboleth.JWKCredential"
+        p:resource="/credentials/fed-signing-es384.jwk" />
+
+    <bean id="shibboleth.oidfed.DefaultES521SigningCredential" parent="shibboleth.JWKCredential"
+        p:resource="/credentials/fed-signing-es521.jwk" />
+
     <!--
     Lists ALL of your OP's response signing credentials for the default security configuration. 
     If you define additional signing credentials make sure to include them within this list.
@@ -49,6 +61,17 @@
         <ref bean="shibboleth.oidc.DefaultECEncryptionCredential" />
     </util:list>
 
+    <!--
+    List ALL of your OP's signing credentials for the default federation security configuration.
+    If you define additional signing credentials make sure to include them within this list.
+    -->
+    <util:list id="shibboleth.oidfed.SigningCredentials">
+        <ref bean="shibboleth.oidfed.DefaultRSSigningCredential" />
+        <ref bean="shibboleth.oidfed.DefaultES256SigningCredential" />
+        <ref bean="shibboleth.oidfed.DefaultES384SigningCredential" />
+        <ref bean="shibboleth.oidfed.DefaultES521SigningCredential" />
+    </util:list>
+
     <!--
     If you need to publish a key set different from shibboleth.oidc.EncryptionCredentials, define
     a list bean named "shibboleth.oidc.EncryptionCredentialsToPublish".
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 c75c3c7b..acbced56 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
@@ -46,6 +46,7 @@
                 <bean parent="OAUTH2.Introspection" />
                 <bean parent="OAUTH2.Revocation" />
                 <bean parent="OAUTH2.PAR" />
+                <bean parent="OIDFED.Configuration" p:cachedEntityStatementLifetime="PT2S" />
             </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