[java-idp-plugin-oidc-op-oidfed] branch dev/CACHE-REFACTOR updated: Initial implementation for the signed keyset flow (to be used as OP's signed_jwks_uri)

Codeberg noreply at shibboleth.net
Thu Mar 19 16:51:50 UTC 2026


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

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

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

The following commit(s) were added to refs/heads/dev/CACHE-REFACTOR by this push:
     new 293855a  Initial implementation for the signed keyset flow (to be used as OP's signed_jwks_uri)
293855a is described below

commit 293855a3ce05a9a63fbe7d2666e08c4be4a7a629
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Thu Mar 19 18:49:54 2026 +0200

    Initial implementation for the signed keyset flow (to be used as OP's signed_jwks_uri)
    
    - OIDFED.Keyset profile needs to be enabled for shibboleth.UnverifiedRelyingParty
      - Response (a signed JWT) is cached in the same way as entity configuration
---
 .../oidc/op/oidfed/metadata/SignedKeyset.java      |  25 +++
 .../metadata/payload/SignedKeysetPayload.java      |  31 +++
 ...tityStatementProducingProfileConfiguration.java |  30 +++
 ...FederationSignedKeysetProfileConfiguration.java |  32 +++
 .../EntityStatementLifetimeLookupFunction.java     |   7 +-
 ...tionalClaimsLookupStrategiesLookupFunction.java |   6 +-
 idp-oidfed-op-impl/pom.xml                         |   5 +
 .../messaging/impl/SignedKeysetResponse.java       |  55 ++++++
 .../op/oidfed/metadata/impl/SignedKeysetImpl.java  |  63 ++++++
 .../payload/impl/SignedKeysetPayloadImpl.java      |  80 ++++++++
 ...onExplicitRegistrationProfileConfiguration.java |  78 ++++++++
 ...FederationSignedKeysetProfileConfiguration.java | 199 +++++++++++++++++++
 .../op/oidfed/profile/impl/BuildSignedKeyset.java  | 140 ++++++++++++++
 .../profile/impl/EntityStatementContext.java       |  25 +++
 .../impl/FormOutboundSignedKeysetResponse.java     | 214 +++++++++++++++++++++
 .../InitializeEntityStatementContextForKeyset.java | 189 ++++++++++++++++++
 ...sponse.java => LookupCachedNimbusResponse.java} |  14 +-
 .../impl/LookupCachedResolveEntityResponse.java    |   6 +-
 .../oidfed/profile/impl/OidFederationEventIds.java |  10 +-
 .../entity-configuration-beans.xml                 |   4 +-
 .../entity-configuration-flow.xml                  |   4 +-
 .../oidfed/resolve-entity/resolve-entity-flow.xml  |   2 +-
 .../signed-keyset-beans.xml}                       |  47 ++---
 .../signed-keyset-flow.xml}                        |  12 +-
 .../idp/service/relying-party/postconfig.xml       |   8 +
 .../profile/flow/oidfed/SignedKeysetFlowTest.java  | 146 ++++++++++++++
 .../shibboleth/idp/module/conf/relying-party.xml   |   1 +
 pom.xml                                            |   7 +
 28 files changed, 1383 insertions(+), 57 deletions(-)

diff --git a/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/SignedKeyset.java b/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/SignedKeyset.java
new file mode 100644
index 0000000..926d381
--- /dev/null
+++ b/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/SignedKeyset.java
@@ -0,0 +1,25 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.oidfed.metadata;
+
+import com.nimbusds.jwt.SignedJWT;
+
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.payload.SignedKeysetPayload;
+
+/**
+ * A wrapper inteface containing the {@link SignedJWT} and parsed claims related to signed keyset.
+ */
+public interface SignedKeyset extends BaseJWTWrapper<SignedKeysetPayload> {
+}
diff --git a/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/payload/SignedKeysetPayload.java b/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/payload/SignedKeysetPayload.java
new file mode 100644
index 0000000..e158d63
--- /dev/null
+++ b/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/payload/SignedKeysetPayload.java
@@ -0,0 +1,31 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.payload;
+
+import com.nimbusds.jose.jwk.JWKSet;
+
+/**
+ * Signed keyset payload claims as defined by the OpenID Federation 1.0 Section 5.2.1.
+ */
+public interface SignedKeysetPayload extends BaseExpirableSubjectPayload {
+
+    /**
+     * Get the JWK set.
+     * 
+     * @return JWK set
+     */
+    public JWKSet getKeys();
+
+}
diff --git a/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/config/OIDFederationEntityStatementProducingProfileConfiguration.java b/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/config/OIDFederationEntityStatementProducingProfileConfiguration.java
index 986291a..30b379a 100644
--- a/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/config/OIDFederationEntityStatementProducingProfileConfiguration.java
+++ b/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/config/OIDFederationEntityStatementProducingProfileConfiguration.java
@@ -14,14 +14,20 @@
 
 package net.shibboleth.idp.plugin.oidc.op.oidfed.profile.config;
 
+import java.time.Duration;
 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.shared.annotation.ConfigurationSetting;
+import net.shibboleth.shared.annotation.constraint.NonnullElements;
+import net.shibboleth.shared.annotation.constraint.NotLive;
+import net.shibboleth.shared.annotation.constraint.Unmodifiable;
 
 /** 
  * Configuration common to OpenID Federation Entity Statement producing profiles.
@@ -40,4 +46,28 @@ public interface OIDFederationEntityStatementProducingProfileConfiguration exten
         getEntityStatementClaimsSetManipulationStrategy(
                 @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")
+    @Nonnull Duration getEntityStatementLifetime(@Nullable final ProfileRequestContext profileRequestContext);
+
+    /**
+     * Get the lookup strategies for optional claims to be included to the entity statement.
+     * 
+     * @param profileRequestContext profile request context
+     * 
+     * @return optional claims to be included to the entity statement
+     */
+    @ConfigurationSetting(name="optionalClaimsLookupStrategies")
+    @Nonnull @NonnullElements @NotLive @Unmodifiable
+    Map<String,Function<ProfileRequestContext,Object>> getOptionalClaimsLookupStrategies(
+            @Nullable final ProfileRequestContext profileRequestContext);
+
 }
diff --git a/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/config/OIDFederationSignedKeysetProfileConfiguration.java b/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/config/OIDFederationSignedKeysetProfileConfiguration.java
new file mode 100644
index 0000000..eb93b46
--- /dev/null
+++ b/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/config/OIDFederationSignedKeysetProfileConfiguration.java
@@ -0,0 +1,32 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.oidfed.profile.config;
+
+import net.shibboleth.profile.config.OverriddenIssuerProfileConfiguration;
+
+/** 
+ * Profile configuration for an OpenID Federation Entity Configuration.
+ */
+public interface OIDFederationSignedKeysetProfileConfiguration extends OverriddenIssuerProfileConfiguration, 
+    OIDFederationProfileConfiguration, OIDFederationEntityStatementProducingProfileConfiguration,
+    OIDFederationResponseCachingProfileConfiguration{
+    
+    /** 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/keyset";
+
+}
diff --git a/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/config/navigate/EntityStatementLifetimeLookupFunction.java b/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/config/navigate/EntityStatementLifetimeLookupFunction.java
index 8e36ff2..c2504ab 100644
--- a/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/config/navigate/EntityStatementLifetimeLookupFunction.java
+++ b/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/config/navigate/EntityStatementLifetimeLookupFunction.java
@@ -20,13 +20,14 @@ import javax.annotation.Nullable;
 
 import org.opensaml.profile.context.ProfileRequestContext;
 
-import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.config.OIDFederationEntityConfigurationProfileConfiguration;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.config.OIDFederationEntityStatementProducingProfileConfiguration;
 import net.shibboleth.profile.config.ProfileConfiguration;
 import net.shibboleth.profile.context.RelyingPartyContext;
 import net.shibboleth.profile.context.navigate.AbstractRelyingPartyLookupFunction;
 
 /**
- * A function that returns {@link OIDFederationEntityConfigurationProfileConfiguration#getEntityStatementLifetime(ProfileRequestContext)}
+ * A function that returns
+ * {@link OIDFederationEntityStatementProducingProfileConfiguration#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}.
  * 
@@ -40,7 +41,7 @@ public class EntityStatementLifetimeLookupFunction extends AbstractRelyingPartyL
         final RelyingPartyContext rpc = getRelyingPartyContextLookupStrategy().apply(input);
         if (rpc != null) {
             final ProfileConfiguration pc = rpc.getProfileConfig();
-            if (pc instanceof OIDFederationEntityConfigurationProfileConfiguration ofec) {
+            if (pc instanceof OIDFederationEntityStatementProducingProfileConfiguration ofec) {
                 return ofec.getEntityStatementLifetime(input);
             }
         }
diff --git a/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/config/navigate/OptionalClaimsLookupStrategiesLookupFunction.java b/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/config/navigate/OptionalClaimsLookupStrategiesLookupFunction.java
index a973cd0..f28141c 100644
--- a/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/config/navigate/OptionalClaimsLookupStrategiesLookupFunction.java
+++ b/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/config/navigate/OptionalClaimsLookupStrategiesLookupFunction.java
@@ -21,14 +21,14 @@ import javax.annotation.Nullable;
 
 import org.opensaml.profile.context.ProfileRequestContext;
 
-import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.config.OIDFederationEntityConfigurationProfileConfiguration;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.config.OIDFederationEntityStatementProducingProfileConfiguration;
 import net.shibboleth.profile.config.ProfileConfiguration;
 import net.shibboleth.profile.context.RelyingPartyContext;
 import net.shibboleth.profile.context.navigate.AbstractRelyingPartyLookupFunction;
 
 /**
  * A function that returns optional entity configuration claims lookup strategies from
- * {@link OIDFederationEntityConfigurationProfileConfiguration} if such a profile is available from a
+ * {@link OIDFederationEntityStatementProducingProfileConfiguration} 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>
@@ -43,7 +43,7 @@ public class OptionalClaimsLookupStrategiesLookupFunction  extends
         final RelyingPartyContext rpc = getRelyingPartyContextLookupStrategy().apply(input);
         if (rpc != null) {
             final ProfileConfiguration pc = rpc.getProfileConfig();
-            if (pc instanceof OIDFederationEntityConfigurationProfileConfiguration ofecpc) {
+            if (pc instanceof OIDFederationEntityStatementProducingProfileConfiguration ofecpc) {
                 return ofecpc.getOptionalClaimsLookupStrategies(input);
             }
         }
diff --git a/idp-oidfed-op-impl/pom.xml b/idp-oidfed-op-impl/pom.xml
index 0e8b2ed..a9d1a94 100644
--- a/idp-oidfed-op-impl/pom.xml
+++ b/idp-oidfed-op-impl/pom.xml
@@ -486,6 +486,11 @@
             <artifactId>nashorn-core</artifactId>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>commons-io</groupId>
+            <artifactId>commons-io</artifactId>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 
     <build>
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/messaging/impl/SignedKeysetResponse.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/messaging/impl/SignedKeysetResponse.java
new file mode 100644
index 0000000..8fb4788
--- /dev/null
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/messaging/impl/SignedKeysetResponse.java
@@ -0,0 +1,55 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.oidfed.messaging.impl;
+
+import javax.annotation.Nonnull;
+
+import com.nimbusds.common.contenttype.ContentType;
+import com.nimbusds.jose.JOSEObjectType;
+import com.nimbusds.jwt.SignedJWT;
+
+/**
+ * Response message to the OpenID federation signed keyset endpoint.
+ */
+public class SignedKeysetResponse extends AbstractSignedJWTResponse {
+
+    /** The JWT type header. */
+    @Nonnull
+    public static final JOSEObjectType JWT_TYPE_HEADER = new JOSEObjectType("jwk-set+jwt");
+
+    /** The content type. */
+    @Nonnull public static final ContentType HTTP_RESPONSE_CONTENT_TYPE =
+            new ContentType("application", JWT_TYPE_HEADER.toString());
+
+    /**
+     * 
+     * Constructor.
+     *
+     * @param statement entity statement
+     */
+    public SignedKeysetResponse(@Nonnull final SignedJWT statement) {
+        super(statement);
+    }
+
+    /** {@inheritDoc} */
+    protected ContentType getHttpResponseContentType() {
+        return HTTP_RESPONSE_CONTENT_TYPE;
+    }
+
+    /** {@inheritDoc} */
+    protected JOSEObjectType getJWTTypeHeader() {
+        return JWT_TYPE_HEADER;
+    }
+}
\ No newline at end of file
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/impl/SignedKeysetImpl.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/impl/SignedKeysetImpl.java
new file mode 100644
index 0000000..6113649
--- /dev/null
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/impl/SignedKeysetImpl.java
@@ -0,0 +1,63 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.impl;
+
+import javax.annotation.Nonnull;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JavaType;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.nimbusds.jwt.SignedJWT;
+
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.SignedKeyset;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.payload.SignedKeysetPayload;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.payload.impl.SignedKeysetPayloadImpl;
+import net.shibboleth.shared.logic.ConstraintViolationException;
+
+/**
+ * A wrapper class extending {@link EntityStatementImpl} with parsed claims related to signed keyset.
+ */
+public class SignedKeysetImpl extends AbstractJWTWrapperImpl<SignedKeysetPayload> implements SignedKeyset {
+
+    /**
+     * Constructor.
+     *
+     * @param signedJwt entity configuration
+     * @param payload entity configuration payload
+     * @throws ConstraintViolationException if the payload syntax/content is not expected
+     */
+    public SignedKeysetImpl(@Nonnull final SignedJWT signedJwt,
+            @Nonnull final SignedKeysetPayload payload) 
+        throws ConstraintViolationException {
+        super(signedJwt, payload);
+    }
+
+    /**
+     * Parse an {@link SignedKeysetImpl} from the given JWT by using the given object mapper.
+     * 
+     * @param jwt entity statement JWT
+     * @param objectMapper object mapper capable of parsing JWT payload
+     * @return entity configuration
+     * @throws JsonProcessingException if the payload could not be parsed
+     */
+    @Nonnull public static SignedKeysetImpl parse(@Nonnull final SignedJWT jwt,
+            @Nonnull final ObjectMapper objectMapper) throws JsonProcessingException {
+        final JavaType objectType = objectMapper.constructType(SignedKeysetPayloadImpl.class);
+        final SignedKeysetPayloadImpl result = objectMapper.readValue(jwt.getPayload().toString(), objectType);
+        assert result != null;
+        return new SignedKeysetImpl(jwt, result);
+    }
+    
+}
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/payload/impl/SignedKeysetPayloadImpl.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/payload/impl/SignedKeysetPayloadImpl.java
new file mode 100644
index 0000000..c5d8c4c
--- /dev/null
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/payload/impl/SignedKeysetPayloadImpl.java
@@ -0,0 +1,80 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.payload.impl;
+
+import javax.annotation.Nonnull;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.base.MoreObjects;
+import com.nimbusds.jose.jwk.JWKSet;
+
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.payload.SignedKeysetPayload;
+
+/**
+ * Signed keyset payload claims as defined by the OpenID Federation 1.0 Section 5.2.1.
+ */
+public class SignedKeysetPayloadImpl extends BaseExpirableSubjectPayloadImpl implements SignedKeysetPayload {
+
+    /** A JWK set. */
+    @JsonProperty("keys") private JWKSet keys;
+
+    /**
+     * Constructor.
+     */
+    public SignedKeysetPayloadImpl() {
+        // no op
+    }
+
+    /**
+     * Constructor.
+     *
+     * @param payload content
+     */
+    public SignedKeysetPayloadImpl(@Nonnull final SignedKeysetPayload payload) {
+        super(payload);
+        setKeys(payload.getKeys());
+    }
+
+    /**
+     * Get the JWK set.
+     * 
+     * @return JWK set
+     */
+    public JWKSet getKeys() {
+        return keys;
+    }
+
+    /**
+     * Set the JWK set.
+     * 
+     * @param jwks JWK set
+     */
+    public void setKeys(final JWKSet jwks) {
+        keys = jwks;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override public String toString() {
+        return MoreObjects.toStringHelper(this)
+                .add("iss", getIssuer())
+                .add("sub", getSubject())
+                .add("iat", getIssuedAt())
+                .add("exp", getExpiration())
+                .add("keys", keys)
+                .add("customClaims", getCustomClaims()).toString();
+    }
+}
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/config/impl/DefaultOIDFederationExplicitRegistrationProfileConfiguration.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/config/impl/DefaultOIDFederationExplicitRegistrationProfileConfiguration.java
index 5679621..33f6b6b 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/config/impl/DefaultOIDFederationExplicitRegistrationProfileConfiguration.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/config/impl/DefaultOIDFederationExplicitRegistrationProfileConfiguration.java
@@ -14,6 +14,7 @@
 
 package net.shibboleth.idp.plugin.oidc.op.oidfed.profile.config.impl;
 
+import java.time.Duration;
 import java.util.Collection;
 import java.util.Map;
 import java.util.Set;
@@ -32,6 +33,7 @@ import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.config.OIDFederationExpl
 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;
@@ -65,11 +67,19 @@ public class DefaultOIDFederationExplicitRegistrationProfileConfiguration
     /** Enabled token endpoint authentication methods. */
     @Nonnull private Function<ProfileRequestContext,Set<String>> tokenEndpointAuthMethodsLookupStrategy;
 
+    /** Lookup function to supply entity statement lifetime. */
+    @Nonnull private Function<ProfileRequestContext,Duration> entityStatementLifetimeLookupStrategy;
+
     /** 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;    
 
+    /** Lookup function to supply map of strategies for optional claims to be included in the entity statement. */
+    @Nonnull
+    private Function<ProfileRequestContext,Map<String,Function<ProfileRequestContext,Object>>>
+        optionalClaimsLookupStrategiesLookupStrategy;
+
     /**
      * Constructor.
      */
@@ -95,6 +105,8 @@ public class DefaultOIDFederationExplicitRegistrationProfileConfiguration
                         ClientAuthenticationMethod.CLIENT_SECRET_JWT.toString(),
                         ClientAuthenticationMethod.PRIVATE_KEY_JWT.toString()));
         entityStatementClaimsSetManipulationStrategyLookupStrategy = FunctionSupport.constant(null);
+        entityStatementLifetimeLookupStrategy = FunctionSupport.constant(Duration.ofHours(24));
+        optionalClaimsLookupStrategiesLookupStrategy = FunctionSupport.constant(null);
     }
 
     /** {@inheritDoc} */
@@ -260,4 +272,70 @@ public class DefaultOIDFederationExplicitRegistrationProfileConfiguration
         entityStatementClaimsSetManipulationStrategyLookupStrategy = Constraint.isNotNull(strategy,
                 "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
+    @Nonnull
+    public Map<String,Function<ProfileRequestContext,Object>> getOptionalClaimsLookupStrategies(
+            @Nullable final ProfileRequestContext profileRequestContext) {
+        final Map<String,Function<ProfileRequestContext,Object>> strategies =
+                optionalClaimsLookupStrategiesLookupStrategy.apply(profileRequestContext);
+        return strategies != null ? strategies : CollectionSupport.emptyMap();
+    }
+
+    /**
+     * Set the lookup strategies for optional claims to be included to the entity statement.
+     * 
+     * @param strategies lookup strategies for optional claims to be included to the entity statement
+     */
+    public void setOptionalClaimsLookupStrategies(
+            @Nullable final Map<String,Function<ProfileRequestContext,Object>> strategies) {
+        optionalClaimsLookupStrategiesLookupStrategy = FunctionSupport.constant(strategies);
+    }
+
+    /**
+     * Set a lookup strategy for the lookup strategies for optional claims to be included to the entity statement.
+     *
+     * @param strategy lookup strategy
+     */
+    public void setOptionalClaimsLookupStrategiesLookupStrategy(@Nonnull final 
+            Function<ProfileRequestContext,Map<String,Function<ProfileRequestContext,Object>>> strategy) {
+        optionalClaimsLookupStrategiesLookupStrategy = Constraint.isNotNull(strategy,
+                "Lookup strategy cannot be null");
+    }
 }
\ No newline at end of file
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/config/impl/DefaultOIDFederationSignedKeysetProfileConfiguration.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/config/impl/DefaultOIDFederationSignedKeysetProfileConfiguration.java
new file mode 100644
index 0000000..3df320c
--- /dev/null
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/config/impl/DefaultOIDFederationSignedKeysetProfileConfiguration.java
@@ -0,0 +1,199 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.oidfed.profile.config.impl;
+
+import java.time.Duration;
+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.idp.plugin.oidc.op.oidfed.profile.config.OIDFederationSignedKeysetProfileConfiguration;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+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 Signed keyset.
+ */
+public class DefaultOIDFederationSignedKeysetProfileConfiguration
+    extends AbstractOIDFederationResponseCachingProfileConfiguration
+    implements OIDFederationSignedKeysetProfileConfiguration {
+
+    /** OIDC provider information profile counter name. */
+    @Nonnull @NotEmpty public static final String PROFILE_COUNTER = "net.shibboleth.idp.profiles.oidfed.keyset";
+
+    /** Lookup function to override issuer value. */
+    @Nonnull private Function<ProfileRequestContext,String> issuerLookupStrategy;
+
+    /** Lookup function to supply entity statement lifetime. */
+    @Nonnull private Function<ProfileRequestContext,Duration> entityStatementLifetimeLookupStrategy;
+
+    /** 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;    
+
+    /** Lookup function to supply map of strategies for optional claims to be included in the entity statement. */
+    @Nonnull
+    private Function<ProfileRequestContext,Map<String,Function<ProfileRequestContext,Object>>>
+        optionalClaimsLookupStrategiesLookupStrategy;
+
+    /**
+     * Constructor.
+     */
+    public DefaultOIDFederationSignedKeysetProfileConfiguration() {
+        this(PROFILE_ID);
+    }
+    
+    /**
+     * Creates a new configuration instance.
+     *
+     * @param profileId Unique profile identifier.
+     */
+    public DefaultOIDFederationSignedKeysetProfileConfiguration(@Nonnull @NotEmpty final String profileId) {
+        super(profileId);
+        issuerLookupStrategy = FunctionSupport.constant(null);
+        entityStatementClaimsSetManipulationStrategyLookupStrategy = FunctionSupport.constant(null);
+        entityStatementLifetimeLookupStrategy = FunctionSupport.constant(Duration.ofHours(24));
+        optionalClaimsLookupStrategiesLookupStrategy = 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
+    public Duration getEntityStatementLifetime(@Nullable final ProfileRequestContext profileRequestContext) {
+        final Duration lifetime = entityStatementLifetimeLookupStrategy.apply(profileRequestContext);
+
+        Constraint.isTrue(lifetime != null && !lifetime.isNegative(),
+                "Entity statement lifetime must be equal to or 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(@Nonnull final Duration lifetime) {
+        final Duration statementLifetime = Constraint.isNotNull(lifetime, "Entity statement lifetime cannot be null");
+        Constraint.isTrue(!statementLifetime.isNegative(),
+                "Entity statement lifetime must be equal or 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
+    @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");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    @Nonnull
+    public Map<String,Function<ProfileRequestContext,Object>> getOptionalClaimsLookupStrategies(
+            @Nullable final ProfileRequestContext profileRequestContext) {
+        final Map<String,Function<ProfileRequestContext,Object>> strategies =
+                optionalClaimsLookupStrategiesLookupStrategy.apply(profileRequestContext);
+        return strategies != null ? strategies : CollectionSupport.emptyMap();
+    }
+
+    /**
+     * Set the lookup strategies for optional claims to be included to the entity statement.
+     * 
+     * @param strategies lookup strategies for optional claims to be included to the entity statement
+     */
+    public void setOptionalClaimsLookupStrategies(
+            @Nullable final Map<String,Function<ProfileRequestContext,Object>> strategies) {
+        optionalClaimsLookupStrategiesLookupStrategy = FunctionSupport.constant(strategies);
+    }
+
+    /**
+     * Set a lookup strategy for the lookup strategies for optional claims to be included to the entity statement.
+     *
+     * @param strategy lookup strategy
+     */
+    public void setOptionalClaimsLookupStrategiesLookupStrategy(@Nonnull final 
+            Function<ProfileRequestContext,Map<String,Function<ProfileRequestContext,Object>>> strategy) {
+        optionalClaimsLookupStrategiesLookupStrategy = Constraint.isNotNull(strategy,
+                "Lookup strategy cannot be null");
+    }
+
+}
\ No newline at end of file
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/BuildSignedKeyset.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/BuildSignedKeyset.java
new file mode 100644
index 0000000..2d08aa2
--- /dev/null
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/BuildSignedKeyset.java
@@ -0,0 +1,140 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Date;
+import java.util.Map;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import com.nimbusds.jose.jwk.JWKSet;
+import com.nimbusds.jwt.JWTClaimsSet;
+
+import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.config.navigate.EntityStatementLifetimeLookupFunction;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.config.navigate.OptionalClaimsLookupStrategiesLookupFunction;
+import net.shibboleth.idp.profile.IdPEventIds;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+import org.opensaml.profile.action.ActionSupport;
+
+/**
+ * Action that creates a keyset JWT, 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}
+ */
+public class BuildSignedKeyset extends AbstractBuildEntityStatementAction {
+
+    /** Class logger. */
+    @Nonnull private Logger log = LoggerFactory.getLogger(BuildSignedKeyset.class);
+
+    /** Strategy used to obtain the entity statement lifetime. */
+    @Nonnull private Function<ProfileRequestContext,Duration> entityStatementLifetimeLookupStrategy;
+
+    /** Strategy used to locate strategies for optional claims. */
+    @Nonnull private Function<ProfileRequestContext,Map<String, Function<ProfileRequestContext,Object>>>
+        optionalClaimsLookupStrategiesLookupStrategy;
+
+    /** Constructor. */
+    public BuildSignedKeyset() {
+        entityStatementLifetimeLookupStrategy = new EntityStatementLifetimeLookupFunction();
+        optionalClaimsLookupStrategiesLookupStrategy = new OptionalClaimsLookupStrategiesLookupFunction();
+    }
+
+    /**
+     * Set the strategy used to obtain the entity statement lifetime.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setEntityStatementLifetimeLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,Duration> strategy) {
+        checkSetterPreconditions();
+        
+        entityStatementLifetimeLookupStrategy =
+                Constraint.isNotNull(strategy, "Entity statement lifetime lookup strategy cannot be null");
+    }
+
+    /**
+     * Set the strategy used to locate strategies for optional claims.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setOptionalClaimsLookupStrategiesLookupStrategy(@Nonnull final
+            Function<ProfileRequestContext, Map<String,Function<ProfileRequestContext,Object>>> strategy) {
+        checkSetterPreconditions();
+
+        optionalClaimsLookupStrategiesLookupStrategy =
+                Constraint.isNotNull(strategy, "Optional claims lookup strategies lookup strategy cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected boolean populateClaimsSetBuilder(@Nonnull final JWTClaimsSet.Builder builder,
+            @Nonnull final ProfileRequestContext profileRequestContext) {
+
+        final Duration lifetime = entityStatementLifetimeLookupStrategy.apply(profileRequestContext);
+        if (lifetime == null || Duration.ZERO.equals(lifetime)) {
+            log.debug("{} No lifetime supplied for entity statement", getLogPrefix());
+        } else {
+            final Instant now = Instant.now();
+            final Instant dateExp = now.plus(lifetime);
+            assert dateExp != null;
+
+            log.debug("{} Set expiration time of entity statement into {}", getLogPrefix(), dateExp);
+            builder.expirationTime(Date.from(dateExp));
+        }
+        
+        final JWKSet jwks = entityStatementCtx.getKeys();
+        if (jwks == null || jwks.isEmpty()) {
+            log.error("{} No credentials to publish resolved for signed keyset entity statement", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_PROFILE_CONFIG);
+            return false;
+            
+        }
+
+        log.trace("{} Resolved jwks to publish: {}", getLogPrefix(), jwks);
+        
+        builder.claim("keys", jwks.toJSONObject(true));
+
+        final Map<String, Function<ProfileRequestContext, Object>> optionalClaimsLookupStrategies =
+                optionalClaimsLookupStrategiesLookupStrategy.apply(profileRequestContext);
+        if (optionalClaimsLookupStrategies != null) {
+            for (final String claim : optionalClaimsLookupStrategies.keySet()) {
+                log.trace("{} Looking up the value for clain {}", getLogPrefix(), claim);
+                final Function<ProfileRequestContext,Object> lookup = optionalClaimsLookupStrategies.get(claim);
+                final Object value = lookup.apply(profileRequestContext);
+                if (value != null) {
+                    log.debug("{} Resolved value {} for clain {}", getLogPrefix(), value, claim);
+                    builder.claim(claim, value);
+                } else {
+                    log.debug("{} No value resolved for clain {}", getLogPrefix(), claim);
+                }
+            }
+        }
+
+       return true;
+   }
+
+}
\ No newline at end of file
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/EntityStatementContext.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/EntityStatementContext.java
index 2b155bf..7a1386f 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/EntityStatementContext.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/EntityStatementContext.java
@@ -21,6 +21,7 @@ import javax.annotation.Nullable;
 
 import org.opensaml.messaging.context.BaseContext;
 
+import com.nimbusds.jose.jwk.JWKSet;
 import com.nimbusds.jwt.JWT;
 
 import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.payload.claim.Metadata;
@@ -39,6 +40,9 @@ public final class EntityStatementContext extends BaseContext {
     /** The entity statement. */
     @Nullable private JWT jwt;
 
+    /** The keys claim for the entity statement. */
+    @Nullable private JWKSet keys;
+
     /**
      * Get the metadata.
      * 
@@ -105,4 +109,25 @@ public final class EntityStatementContext extends BaseContext {
         lifetime = lt;
         return this;
     }
+
+    /**
+     * Get the keys claim for the entity statement.
+     * 
+     * @return keys
+     */
+    @Nullable public JWKSet getKeys() {
+        return keys;
+    }
+
+    /**
+     * Set the keys claim for the entity statement.
+     * 
+     * @param jwks keys
+     * 
+     * @return this context
+     */
+    @Nonnull public EntityStatementContext setKeys(@Nullable final JWKSet jwks) {
+        keys = jwks;
+        return this;
+    }
 }
\ No newline at end of file
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/FormOutboundSignedKeysetResponse.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/FormOutboundSignedKeysetResponse.java
new file mode 100644
index 0000000..48b3ae9
--- /dev/null
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/FormOutboundSignedKeysetResponse.java
@@ -0,0 +1,214 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.util.List;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.OutboundMessageContextLookup;
+import org.slf4j.Logger;
+
+import com.nimbusds.jwt.SignedJWT;
+import com.nimbusds.oauth2.sdk.Response;
+
+import net.shibboleth.idp.plugin.oidc.op.oidfed.messaging.impl.SignedKeysetResponse;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.ResponseContainerExpirationCriterion;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.local.NimbusResponseContainer;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.local.NimbusResponseCriterion;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.config.navigate.CachedSuccessResponseLifetimeLookupFunction;
+import net.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.oidc.metadata.cache.MetadataCache;
+import net.shibboleth.oidc.metadata.cache.MetadataCacheException;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.resolver.CriteriaSet;
+
+/**
+ * This action builds a response for the OpenID federation configuration request. The response contains an
+ * {@link SignedJWT} obtained from {@link EntityStatementContext#getJWT()}.
+ */
+public class FormOutboundSignedKeysetResponse extends AbstractProfileAction {
+
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(FormOutboundSignedKeysetResponse.class);
+
+    /** Metadata cache for cached response containers. */
+    @NonnullAfterInit private MetadataCache<NimbusResponseContainer> responseCache;
+
+    /** Strategy used to locate the cached message context. */
+    @Nonnull
+    private Function<ProfileRequestContext, RelyingPartyCachedMessageContext> cachedMessageContextLookupStrategy;
+
+    /** Strategy used to locate the subcontext to hold the statement. */
+    @Nonnull private Function<ProfileRequestContext,EntityStatementContext> entityStatementContextLookupStrategy;
+
+    /** Strategy used to locate the lifetime for the cached response record. */
+    @Nonnull private Function<ProfileRequestContext,Duration> cachedResponseLifetimeLookupStrategy;
+
+    /** JWT used to build entity statement. */
+    @Nullable private SignedJWT jwt;
+
+    /** The resolve entity context to operate on. */
+    @NonnullBeforeExec private RelyingPartyCachedMessageContext cachedMessageContext;
+
+    /**
+     * Constructor.
+     */
+    public FormOutboundSignedKeysetResponse() {
+        final Function<ProfileRequestContext,EntityStatementContext> escls =
+                new ChildContextLookup<>(EntityStatementContext.class).compose(
+                        new OutboundMessageContextLookup());
+        assert escls != null;
+        entityStatementContextLookupStrategy = escls;
+        cachedMessageContextLookupStrategy = new ChildContextLookup<>(RelyingPartyCachedMessageContext.class);
+        cachedResponseLifetimeLookupStrategy = new CachedSuccessResponseLifetimeLookupFunction();
+    }
+
+    /**
+     * Set the strategy used to locate the subcontext to hold the statement
+     * 
+     * @param strategy What to set.
+     */
+    public void setEntityStatementContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,EntityStatementContext> strategy) {
+        checkSetterPreconditions();
+        entityStatementContextLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null!");
+    }
+
+    /**
+     * Set the strategy used to locate the cached message context
+     * 
+     * @param strategy What to set.
+     */
+    public void setCachedMessageContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,RelyingPartyCachedMessageContext> strategy) {
+        checkSetterPreconditions();
+        cachedMessageContextLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null!");
+    }
+
+    /**
+     * Set the metadata cache for cached response containers.
+     * 
+     * @param cache What to set.
+     */
+    public void setResponseCache(@Nonnull final MetadataCache<NimbusResponseContainer> cache) {
+        checkSetterPreconditions();
+        responseCache = Constraint.isNotNull(cache, "Response metadata cache cannot be null");
+    }
+
+    /**
+     * Set the strategy used to locate the lifetime for the cached response record.
+     * 
+     * @param strategy What to set.
+     */
+    public void setCachedResponseLifetimeLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext, Duration> strategy) {
+        checkSetterPreconditions();
+        cachedResponseLifetimeLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+
+        if (responseCache == null) {
+            throw new ComponentInitializationException("Response metadata cache cannot be null");
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+        if (!super.doPreExecute(profileRequestContext)) {
+            return false;
+        }
+        cachedMessageContext = cachedMessageContextLookupStrategy.apply(profileRequestContext);
+        if (cachedMessageContext == null) {
+            log.error("{} Could not resolve cached message context", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return false;
+        }
+
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        final Response cachedResponse = cachedMessageContext.getCachedResponse();
+        if (cachedResponse != null) {
+            log.debug("{} Cached response found, storing in to the outbound message context", getLogPrefix());
+            profileRequestContext.ensureOutboundMessageContext().setMessage(cachedResponse);
+            return;
+        }
+        log.debug("{} No cached response found, resolving the response JWT from the context", getLogPrefix());
+        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;
+        }
+        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;
+        }
+
+        assert jwt != null;
+        final SignedKeysetResponse response = new SignedKeysetResponse(jwt);
+        final NimbusResponseCriterion responseCriterion = new NimbusResponseCriterion(response);
+        final Duration lifetime = cachedResponseLifetimeLookupStrategy.apply(profileRequestContext);
+        if (lifetime == null) {
+            log.error("{} Could not resolve lifetime for the cached response record", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return;
+        }
+        final Instant expiration = Instant.now().plus(lifetime);
+        assert expiration != null;
+        final ResponseContainerExpirationCriterion expirationCriterion =
+                new ResponseContainerExpirationCriterion(expiration);
+        final CriteriaSet criteria = new CriteriaSet(responseCriterion, expirationCriterion);
+        try {
+            final List<NimbusResponseContainer> result = responseCache.get(criteria);
+            if (result.size() != 1) {
+                log.error("{} Unexpected result (size={}) when storing response record into the metadata cache",
+                        getLogPrefix(), result.size());
+            } else {
+                log.debug("{} Response stored into the cache", getLogPrefix());
+            }
+        } catch (final MetadataCacheException e) {
+            log.error("{} Could not store the response record into tht metadata cache", getLogPrefix(), e);
+        }
+
+        profileRequestContext.ensureOutboundMessageContext().setMessage(response);
+    }
+}
\ No newline at end of file
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/InitializeEntityStatementContextForKeyset.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/InitializeEntityStatementContextForKeyset.java
new file mode 100644
index 0000000..61010a7
--- /dev/null
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/InitializeEntityStatementContextForKeyset.java
@@ -0,0 +1,189 @@
+/*
+ * 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.Collections;
+import java.util.List;
+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.config.SecurityConfiguration;
+import org.opensaml.security.credential.Credential;
+import org.slf4j.Logger;
+
+import com.nimbusds.jose.jwk.JWKSet;
+
+import net.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.idp.profile.IdPEventIds;
+import net.shibboleth.oidc.profile.config.JSONSecurityConfiguration;
+import net.shibboleth.oidc.security.CredentialConversionUtil;
+import net.shibboleth.profile.config.ProfileConfiguration;
+import net.shibboleth.profile.context.RelyingPartyContext;
+import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+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}
+ */
+public class InitializeEntityStatementContextForKeyset extends AbstractProfileAction {
+
+    /** Class logger. */
+    @Nonnull private Logger log = LoggerFactory.getLogger(InitializeEntityStatementContextForKeyset.class);
+
+    /** Strategy used to create the subcontext to hold the statement. */
+    @Nonnull private Function<ProfileRequestContext,EntityStatementContext> entityStatementContextCreationStrategy;
+
+    /**
+     * Strategy used to locate the {@link RelyingPartyContext} associated with a given {@link ProfileRequestContext}.
+     */
+    @Nonnull private Function<ProfileRequestContext, RelyingPartyContext> relyingPartyContextLookupStrategy;
+    
+    /**
+     * Strategy used to locate the list of credentials to publish.
+     */
+    @Nonnull private Function<JSONSecurityConfiguration, List<Credential>> credentialsToPublishLookupStrategy;
+
+    /** Security configuration we look for keys to publish. */
+    @Nullable private JSONSecurityConfiguration secConfiguration;
+
+    /** Entity statement context. */
+    @NonnullBeforeExec private EntityStatementContext entityStatementCtx;
+
+    /** Constructor. */
+    public InitializeEntityStatementContextForKeyset() {
+        final Function<ProfileRequestContext,EntityStatementContext> esccs =
+                new ChildContextLookup<>(EntityStatementContext.class, true).compose(
+                        new OutboundMessageContextLookup());
+        assert esccs != null;
+        entityStatementContextCreationStrategy = esccs; 
+        relyingPartyContextLookupStrategy = new ChildContextLookup<>(RelyingPartyContext.class);
+        credentialsToPublishLookupStrategy = secConfig -> Collections.emptyList();
+    }
+
+    /**
+     * Set the strategy used to create the {@link EntityStatementContext} to use.
+     * 
+     * @param strategy creation strategy
+     */
+    public void setEntityStatementContextCreationStrategy(
+            @Nonnull final Function<ProfileRequestContext,EntityStatementContext> strategy) {
+        checkSetterPreconditions();
+        
+        entityStatementContextCreationStrategy =
+                Constraint.isNotNull(strategy, "EntityStatementContext creation strategy cannot be null");
+    }
+
+    /**
+     * Set the strategy used to locate the {@link RelyingPartyContext} associated with a given
+     * {@link ProfileRequestContext}.
+     * 
+     * @param strategy strategy used to locate the {@link RelyingPartyContext} associated with a given
+     *            {@link ProfileRequestContext}
+     */
+    public void setRelyingPartyContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext, RelyingPartyContext> strategy) {
+        checkSetterPreconditions();
+
+        relyingPartyContextLookupStrategy =
+                Constraint.isNotNull(strategy, "RelyingPartyContext lookup strategy cannot be null");
+    }
+    
+    /**
+     * Set the strategy used to locate the credentials to publish at the KeySet endpoint.
+     * 
+     * @param strategy the strategy.
+     */
+    public void setCredentialsToPublishLookupStrategy(
+            @Nonnull final Function<JSONSecurityConfiguration, List<Credential>> strategy) {
+        checkSetterPreconditions();
+
+        credentialsToPublishLookupStrategy = Constraint.isNotNull(strategy,
+                "credentialsToPublishLookupStrategy can not 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;
+        }
+
+        final RelyingPartyContext rpCtx = relyingPartyContextLookupStrategy.apply(profileRequestContext);
+        if (rpCtx == null) {
+            log.debug("{} No relying party context associated with this profile request", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_RELYING_PARTY_CTX);
+            return false;
+        }
+
+        final ProfileConfiguration profileConfig = rpCtx.getProfileConfig();
+        if (profileConfig == null) {
+            log.debug("{} No profile configuration associated with this profile request", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_RELYING_PARTY_CTX);
+            return false;
+        }
+
+        final SecurityConfiguration securityConfig =
+                profileConfig.getSecurityConfiguration(profileRequestContext);
+        
+        if (!(securityConfig instanceof JSONSecurityConfiguration)) {
+            log.debug("{} No security configuration associated with the profile configuration of the profile request",
+                    getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_SEC_CFG);
+            return false;
+        }
+        
+        secConfiguration = (JSONSecurityConfiguration) securityConfig;
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+        final List<Credential> credentialsToPublish = credentialsToPublishLookupStrategy.apply(secConfiguration);
+        if (credentialsToPublish == null || credentialsToPublish.isEmpty()) {
+            log.error("{} No credentials to publish resolved for signed keyset entity statement", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_PROFILE_CONFIG);
+            return;
+        }
+
+        final JWKSet jwks = new JWKSet(credentialsToPublish.stream()
+                .map(credential -> CredentialConversionUtil.credentialToKey(credential))
+                .toList());
+        log.trace("{} Resolved jwks to set in the contet: {}", getLogPrefix(), jwks);
+        entityStatementCtx.setKeys(jwks);
+    }
+}
\ No newline at end of file
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/LookupCachedEntityConfigurationResponse.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/LookupCachedNimbusResponse.java
similarity index 91%
rename from idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/LookupCachedEntityConfigurationResponse.java
rename to idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/LookupCachedNimbusResponse.java
index 9b1de1d..fd36f36 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/LookupCachedEntityConfigurationResponse.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/LookupCachedNimbusResponse.java
@@ -37,17 +37,17 @@ import net.shibboleth.shared.primitive.LoggerFactory;
 import net.shibboleth.shared.resolver.CriteriaSet;
 
 /**
- * Lookup if a cached response already exists for entity configuration. If yes, the response is
+ * Lookup if a cached response already exists for entity statement. If yes, the response is
  * stored into {@link RelyingPartyCachedMessageContext} and a corresponding event ID is published.
  * 
  * @event {@link EventIds#PROCEED_EVENT_ID}
  * @event {@link EventIds#INVALID_PROFILE_CTX}
- * @event {@link OidFederationEventIds#CACHED_ENTITY_CONFIGURATION_RESPONSE}
+ * @event {@link OidFederationEventIds#CACHED_RESPONSE_FOUND}
  */
-public class LookupCachedEntityConfigurationResponse extends AbstractProfileAction {
+public class LookupCachedNimbusResponse extends AbstractProfileAction {
 
     /** Class logger. */
-    @Nonnull private Logger log = LoggerFactory.getLogger(LookupCachedEntityConfigurationResponse.class);
+    @Nonnull private Logger log = LoggerFactory.getLogger(LookupCachedNimbusResponse.class);
 
     /** Strategy used to create the cached message context. */
     @Nonnull
@@ -62,7 +62,7 @@ public class LookupCachedEntityConfigurationResponse extends AbstractProfileActi
     /**
      * Constructor.
      */
-    public LookupCachedEntityConfigurationResponse() {
+    public LookupCachedNimbusResponse() {
         final Function<ProfileRequestContext, RelyingPartyCachedMessageContext> recls =
                 new ChildContextLookup<>(RelyingPartyCachedMessageContext.class, true);
         assert recls != null;
@@ -128,9 +128,9 @@ public class LookupCachedEntityConfigurationResponse extends AbstractProfileActi
                 final NimbusResponseContainer cachedResponse = result.get(0);
                 cachedMessageContext.setCachedResponse(cachedResponse.getResponse());
                 log.debug("{} Response found from the cache, publishing event {}", getLogPrefix(),
-                        OidFederationEventIds.CACHED_ENTITY_CONFIGURATION_RESPONSE);
+                        OidFederationEventIds.CACHED_RESPONSE_FOUND);
                 ActionSupport.buildEvent(profileRequestContext,
-                        OidFederationEventIds.CACHED_ENTITY_CONFIGURATION_RESPONSE);
+                        OidFederationEventIds.CACHED_RESPONSE_FOUND);
                 return;
             }
         } catch (final MetadataCacheException e) {
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/LookupCachedResolveEntityResponse.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/LookupCachedResolveEntityResponse.java
index f4b8e01..76d1f8e 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/LookupCachedResolveEntityResponse.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/LookupCachedResolveEntityResponse.java
@@ -45,7 +45,7 @@ import net.shibboleth.shared.resolver.CriteriaSet;
  * 
  * @event {@link EventIds#PROCEED_EVENT_ID}
  * @event {@link EventIds#INVALID_PROFILE_CTX}
- * @event {@link OidFederationEventIds#CACHED_RESOLVE_ENTITY_RESPONSE}
+ * @event {@link OidFederationEventIds#CACHED_RESPONSE_FOUND}
  */
 public class LookupCachedResolveEntityResponse extends AbstractProfileAction {
 
@@ -145,8 +145,8 @@ public class LookupCachedResolveEntityResponse extends AbstractProfileAction {
                 final ResolveEntityResponseContainer cachedResponse = result.get(0);
                 cachedMessageContext.setCachedResponse(cachedResponse.getResponse());
                 log.debug("{} Response found from the cache, publishing event {}", getLogPrefix(),
-                        OidFederationEventIds.CACHED_RESOLVE_ENTITY_RESPONSE);
-                ActionSupport.buildEvent(profileRequestContext, OidFederationEventIds.CACHED_RESOLVE_ENTITY_RESPONSE);
+                        OidFederationEventIds.CACHED_RESPONSE_FOUND);
+                ActionSupport.buildEvent(profileRequestContext, OidFederationEventIds.CACHED_RESPONSE_FOUND);
                 return;
             }
         } catch (final MetadataCacheException e) {
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/OidFederationEventIds.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/OidFederationEventIds.java
index 82837a4..9c5752c 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/OidFederationEventIds.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/OidFederationEventIds.java
@@ -30,15 +30,9 @@ public class OidFederationEventIds {
     @Nonnull @NotEmpty public static final String RESELECT_TRUST_CHAIN = "ReselectTrustChain";
 
     /**
-     * ID of event returned if cached resolve entity response was found and set to the context.
+     * ID of event returned if cached response was found and set to the context.
      */
-    @Nonnull @NotEmpty public static final String CACHED_RESOLVE_ENTITY_RESPONSE = "CachedResolveEntityResponseFound";
-
-    /**
-     * ID of event returned if cached entity configuration response was found and set to the context.
-     */
-    @Nonnull @NotEmpty public static final String CACHED_ENTITY_CONFIGURATION_RESPONSE =
-            "CachedEntityConfigurationResponseFound";
+    @Nonnull @NotEmpty public static final String CACHED_RESPONSE_FOUND = "CachedResponseFound";
 
     /**
      * ID of event returned if no trust chains were resolved for the client.
diff --git a/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/entity-configuration/entity-configuration-beans.xml b/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/entity-configuration/entity-configuration-beans.xml
index 5b21a3e..5434b68 100644
--- a/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/entity-configuration/entity-configuration-beans.xml
+++ b/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/entity-configuration/entity-configuration-beans.xml
@@ -43,8 +43,8 @@
         </property>
     </bean>
 
-    <bean id="LookupCachedEntityConfigurationResponse"
-        class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.LookupCachedEntityConfigurationResponse"
+    <bean id="LookupCachedResponse"
+        class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.LookupCachedNimbusResponse"
         scope="prototype"
         p:responseCache-ref="shibboleth.oidfed.EntityConfigurationResponseMetadataCache" />
 
diff --git a/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/entity-configuration/entity-configuration-flow.xml b/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/entity-configuration/entity-configuration-flow.xml
index 670f03d..f937952 100644
--- a/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/entity-configuration/entity-configuration-flow.xml
+++ b/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/entity-configuration/entity-configuration-flow.xml
@@ -10,9 +10,9 @@
     </action-state>
 
     <action-state id="LookupCachedResponse">
-        <evaluate expression="LookupCachedEntityConfigurationResponse" />
+        <evaluate expression="LookupCachedResponse" />
         <evaluate expression="'proceed'" />
-        <transition on="CachedEntityConfigurationResponseFound" to="BuildResponseMessage" />
+        <transition on="CachedResponseFound" to="BuildResponseMessage" />
         <transition on="proceed" to="InitializeEntityStatementContext" />
     </action-state>
 
diff --git a/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/resolve-entity/resolve-entity-flow.xml b/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/resolve-entity/resolve-entity-flow.xml
index 81639af..e1a17f0 100644
--- a/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/resolve-entity/resolve-entity-flow.xml
+++ b/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/resolve-entity/resolve-entity-flow.xml
@@ -43,7 +43,7 @@
         <evaluate expression="ValidateRequest" />
         <evaluate expression="LookupCachedResolveEntityResponse" />
         <evaluate expression="'proceed'" />
-        <transition on="CachedResolveEntityResponseFound" to="BuildResponseMessage" />
+        <transition on="CachedResponseFound" to="BuildResponseMessage" />
         <transition on="proceed" to="ResolveTrustChains" />
     </action-state>
 
diff --git a/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/entity-configuration/entity-configuration-beans.xml b/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/signed-keyset/signed-keyset-beans.xml
similarity index 83%
copy from idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/entity-configuration/entity-configuration-beans.xml
copy to idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/signed-keyset/signed-keyset-beans.xml
index 5b21a3e..65791cc 100644
--- a/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/entity-configuration/entity-configuration-beans.xml
+++ b/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/signed-keyset/signed-keyset-beans.xml
@@ -8,24 +8,24 @@
     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.profile.config.OIDFederationEntityConfigurationProfileConfiguration).PROFILE_ID}" />
+        c:_0="#{T(net.shibboleth.idp.plugin.oidc.op.oidfed.profile.config.OIDFederationSignedKeysetProfileConfiguration).PROFILE_ID}" />
 
-    <bean id="shibboleth.oidc.loggingId" class="java.lang.String" c:_0="%{idp.service.logging.oidfedconfig:OIDFED.Configuration}" />
+    <bean id="shibboleth.oidc.loggingId" class="java.lang.String" c:_0="%{idp.service.logging.oidfedconfig:OIDFED.SignedKeyset}" />
 
     <util:constant id="shibboleth.metrics.ProfileCounter"
-        static-field="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.config.impl.DefaultOIDFederationEntityConfigurationProfileConfiguration.PROFILE_COUNTER" />
+        static-field="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.config.impl.DefaultOIDFederationSignedKeysetProfileConfiguration.PROFILE_COUNTER" />
 
-    <bean id="shibboleth.oidfed.EntityConfigurationResponseMetadataCache" parent="shibboleth.oidc.CacheBuilder">
+    <bean id="shibboleth.oidfed.SignedKeysetResponseMetadataCache" parent="shibboleth.oidc.CacheBuilder">
         <constructor-arg>
-            <bean p:cacheId="DefaultEntityConfigurationResponseMetadataCache" parent="shibboleth.oidfed.EntityConfigurationResponseMetadataCacheBuilderSpec"
+            <bean p:cacheId="DefaultSignedKeysetResponseMetadataCache" parent="shibboleth.oidfed.SignedKeysetResponseMetadataCacheBuilderSpec"
                 p:cleanupTaskInterval="PT30S"/>
         </constructor-arg>
     </bean>
 
-    <bean id="shibboleth.oidfed.EntityConfigurationResponseMetadataCacheBuilderSpec"
+    <bean id="shibboleth.oidfed.SignedKeysetResponseMetadataCacheBuilderSpec"
         class="net.shibboleth.oidc.metadata.cache.impl.DynamicMetadataCacheBuilderSpec"
-        p:minCacheDuration="%{idp.oidfed.entity-configuration.maxRefreshDelay:PT1S}"
-        p:maxCacheDuration="%{idp.oidfed.entity-configuration.maxRefreshDelay:PT30S}">
+        p:minCacheDuration="%{idp.oidfed.signed-keyset.maxRefreshDelay:PT1S}"
+        p:maxCacheDuration="%{idp.oidfed.signed-keyset.maxRefreshDelay:PT30S}">
         <property name="criteriaToIdentifierStrategy">
             <bean parent="shibboleth.Functions.Constant" c:target-ref="shibboleth.oidc.issuer" />
         </property>
@@ -43,15 +43,15 @@
         </property>
     </bean>
 
-    <bean id="LookupCachedEntityConfigurationResponse"
-        class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.LookupCachedEntityConfigurationResponse"
+    <bean id="LookupCachedResponse"
+        class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.LookupCachedNimbusResponse"
         scope="prototype"
-        p:responseCache-ref="shibboleth.oidfed.EntityConfigurationResponseMetadataCache" />
+        p:responseCache-ref="shibboleth.oidfed.SignedKeysetResponseMetadataCache" />
 
     <bean id="InitializeEntityStatementContext"
-        class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.InitializeEntityStatementContext"
-        p:metadataResolver-ref="#{'%{idp.oidfed.configuration.resolver:shibboleth.oidfed.DefaultOpenIdConfigurationResolver}'.trim()}"
-        p:metadataSkeletonLookupStrategy-ref="#{'%{idp.oidfed.configuration.EntityConfigurationMetadataSkeletonLookupStrategy:DefaultEntityConfigurationMetadataSkeletonLookupStrategy}'.trim()}"/>
+        class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.InitializeEntityStatementContextForKeyset"
+        p:credentialsToPublishLookupStrategy="#{getObject('shibboleth.oidc.keyset.KeySetCredentialsToPublishLookupStrategy') ?: 
+                                                getObject('shibboleth.oidc.keyset.DefaultKeySetCredentialsToPublishLookupStrategy')}"/>
 
     <bean id="DefaultEntityConfigurationMetadataSkeletonLookupStrategy"
         class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.local.DefaultEntityConfigurationMetadataSkeletonLookupStrategy"
@@ -86,26 +86,29 @@
         </constructor-arg>
     </bean>
 
-    <bean id="SelectOidcConfigurationProfileConfiguration"
+    <bean id="SelectOidcKeysetProfileConfiguration"
         class="net.shibboleth.idp.profile.impl.SelectProfileConfiguration" scope="prototype"
-        p:profileId="#{T(net.shibboleth.oidc.profile.config.OIDCProviderInformationConfiguration).PROFILE_ID}" />
+        p:profileId="#{T(net.shibboleth.oidc.profile.config.OIDCPublishKeySetConfiguration).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.BuildEntityConfiguration" scope="prototype"
+        class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.BuildSignedKeyset" scope="prototype"
         p:identifierGeneratorLookupStrategy-ref="shibboleth.oidc.DefaultIdentifierGenerationStrategy"
-        p:objectMapper-ref="#{'%{idp.oidc.logging.objectMapper:shibboleth.oidc.JSONObjectMapper}'.trim()}"/>
+        p:objectMapper-ref="#{'%{idp.oidc.logging.objectMapper:shibboleth.oidc.JSONObjectMapper}'.trim()}" />
+
+    <bean id="shibboleth.oidc.keyset.DefaultKeySetCredentialsToPublishLookupStrategy" 
+          class="net.shibboleth.oidc.profile.config.navigate.JWKCredentialsToPublishLookupStrategy"/> 
 
     <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"
+                class="net.shibboleth.oidc.security.impl.SignJWTHandler" scope="prototype" p:logName="Signed Keyset"
                 p:securityParametersLookupStrategy-ref="EntityStatementSecurityParametersCreationViaMessageContextStrategy"
-                p:typeHeader="entity-statement+jwt">
+                p:typeHeader="jwk-set+jwt">
                 <property name="claimsToSignLookupStrategy">
                      <bean
                         class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.JWTClaimsSetFromEntityStatementLookupFunction" />
@@ -118,8 +121,8 @@
         </constructor-arg>
     </bean>
 
-    <bean id="FormOutboundMessage" class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.FormOutboundFederationConfigurationResponse"
-        scope="prototype" p:responseCache-ref="shibboleth.oidfed.EntityConfigurationResponseMetadataCache">
+    <bean id="FormOutboundMessage" class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.FormOutboundSignedKeysetResponse"
+        scope="prototype" p:responseCache-ref="shibboleth.oidfed.SignedKeysetResponseMetadataCache">
     </bean>
 
     <bean id="shibboleth.oidfed.EntityConfigurationMetadataSkeletonMetadataCache" parent="shibboleth.oidc.CacheBuilder">
diff --git a/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/entity-configuration/entity-configuration-flow.xml b/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/signed-keyset/signed-keyset-flow.xml
similarity index 75%
copy from idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/entity-configuration/entity-configuration-flow.xml
copy to idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/signed-keyset/signed-keyset-flow.xml
index 670f03d..d1b9ea8 100644
--- a/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/entity-configuration/entity-configuration-flow.xml
+++ b/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/signed-keyset/signed-keyset-flow.xml
@@ -10,22 +10,22 @@
     </action-state>
 
     <action-state id="LookupCachedResponse">
-        <evaluate expression="LookupCachedEntityConfigurationResponse" />
+        <evaluate expression="LookupCachedResponse" />
         <evaluate expression="'proceed'" />
-        <transition on="CachedEntityConfigurationResponseFound" to="BuildResponseMessage" />
+        <transition on="CachedResponseFound" to="BuildResponseMessage" />
         <transition on="proceed" to="InitializeEntityStatementContext" />
     </action-state>
 
     <action-state id="InitializeEntityStatementContext">
-        <evaluate expression="SelectOidcConfigurationProfileConfiguration" />
+        <evaluate expression="SelectOidcKeysetProfileConfiguration" />
         <evaluate expression="InitializeEntityStatementContext"/>
         <evaluate expression="SelectProfileConfiguration" />
         <evaluate expression="'proceed'" />
         
-        <transition on="proceed" to="BuildEntityConfiguration" />
+        <transition on="proceed" to="BuildNewResponse" />
     </action-state>
 
-    <action-state id="BuildEntityConfiguration">
+    <action-state id="BuildNewResponse">
         <evaluate expression="PopulateEntityStatementSignatureSigningParameters" />
         <evaluate expression="BuildEntityStatement" />
         <evaluate expression="SignEntityStatement" />
@@ -34,6 +34,6 @@
         <transition on="proceed" to="BuildResponseMessage"/>
     </action-state>
 
-    <bean-import resource="entity-configuration-beans.xml" />
+    <bean-import resource="signed-keyset-beans.xml" />
 
 </flow>
\ No newline at end of file
diff --git a/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml b/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml
index abc34ff..2e2b53b 100644
--- a/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml
+++ b/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml
@@ -18,6 +18,14 @@
         p:authorityHints="%{idp.oidfed.entity.authorityHints:https://example.org}"
         p:optionalClaimsLookupStrategies-ref="shibboleth.oidfed.EntityConfigurationClaimsLookupStrategies" />
 
+    <bean id="OIDFED.Keyset" parent="AbstractOIDFederationProfile" lazy-init="true"
+        class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.config.impl.DefaultOIDFederationSignedKeysetProfileConfiguration"
+        p:issuer-ref="shibboleth.oidc.issuer">
+        <property name="entityStatementLifetime">
+            <bean class="java.time.Duration" factory-method="parse" c:_0="PT0S" />
+        </property>
+    </bean>
+
     <bean id="OIDFED.AutomaticRegistration" parent="AbstractOIDFederationProfile" lazy-init="true"
         class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.config.impl.DefaultOIDFederationAutomaticRegistrationProfileConfiguration"
         p:mandatoryTrustMarks="%{idp.oidfed.automaticRegistration.mandatoryTrustMarks:}"
diff --git a/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/SignedKeysetFlowTest.java b/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/SignedKeysetFlowTest.java
new file mode 100644
index 0000000..d5d4468
--- /dev/null
+++ b/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/SignedKeysetFlowTest.java
@@ -0,0 +1,146 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.profile.flow.oidfed;
+
+import java.io.IOException;
+
+import org.apache.commons.io.IOUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.core.io.Resource;
+import org.springframework.webflow.executor.FlowExecutionResult;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.nimbusds.jose.jwk.JWK;
+import com.nimbusds.jose.jwk.JWKSet;
+import com.nimbusds.jose.jwk.KeyType;
+import com.nimbusds.jwt.SignedJWT;
+import com.nimbusds.oauth2.sdk.ParseException;
+import com.nimbusds.oauth2.sdk.Response;
+
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.SignedKeyset;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.impl.SignedKeysetImpl;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.payload.SignedKeysetPayload;
+
+/**
+ * Unit test for the signed keyset flow.
+ */
+ at SuppressWarnings("null")
+public class SignedKeysetFlowTest extends AbstractFederationFlowTest {
+
+    public static final String FLOW_ID = "oidfed/signed-keyset";
+
+    Resource rsaSigKey = new ClassPathResource("/credentials/idp-signing-rs.jwk");
+    Resource rsaEncKey = new ClassPathResource("/credentials/idp-encryption-rsa.jwk");
+    Resource ecSigKey = new ClassPathResource("/credentials/idp-signing-es.jwk");
+    Resource ec384SigKey = new ClassPathResource("/credentials/idp-signing-es384.jwk");
+    Resource ec512SigKey = new ClassPathResource("/credentials/idp-signing-es521.jwk");
+    Resource ecEncKey = new ClassPathResource("/credentials/idp-encryption-ec.jwk");
+
+    @Autowired
+    @Qualifier("shibboleth.oidfed.JWTPayloadJSONObjectMapper")
+    ObjectMapper payloadObjectMapper;
+
+    protected SignedKeysetFlowTest() {
+        super(FLOW_ID);
+    }
+
+    @Test
+    public void testOutputAndCaching()
+            throws ParseException, IOException, InterruptedException, java.text.ParseException {
+        request.setRequestURI("/idp/profile/oidfed/signed-keyset");
+        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(2100);
+        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, IOException, java.text.ParseException {
+        final SignedKeyset entityStatement;
+        try {
+            entityStatement = SignedKeysetImpl.parse(
+                    SignedJWT.parse(response.toHTTPResponse().getContent()), payloadObjectMapper);
+        } catch (JsonProcessingException | java.text.ParseException e) {
+            Assert.fail();
+            return;
+        }
+        final SignedKeysetPayload payload = entityStatement.getParsedPayload();
+        Assert.assertEquals(payload.getSubject(), issuer);
+        Assert.assertEquals(payload.getIssuer(), issuer);
+        Assert.assertNull(payload.getExpiration());
+        final JWKSet keys = payload.getKeys();
+        Assert.assertNotNull(keys);
+        Assert.assertEquals(keys.size(), 6);
+        final JWK rsaSigJwk = JWK.parse(IOUtils.toString(rsaSigKey.getInputStream(), "UTF-8"));
+        Assert.assertTrue(listContainsPublicJwk(keys, rsaSigJwk));
+        final JWK rsaEncJwk = JWK.parse(IOUtils.toString(rsaEncKey.getInputStream(), "UTF-8"));
+        Assert.assertTrue(listContainsPublicJwk(keys, rsaEncJwk));
+        final JWK ecSigJwk = JWK.parse(IOUtils.toString(ecSigKey.getInputStream(), "UTF-8"));
+        Assert.assertTrue(listContainsPublicJwk(keys, ecSigJwk));
+        final JWK ec384SigJwk = JWK.parse(IOUtils.toString(ec384SigKey.getInputStream(), "UTF-8"));
+        Assert.assertTrue(listContainsPublicJwk(keys, ec384SigJwk));
+        final JWK ec512SigJwk = JWK.parse(IOUtils.toString(ec512SigKey.getInputStream(), "UTF-8"));
+        Assert.assertTrue(listContainsPublicJwk(keys, ec512SigJwk));
+        final JWK ecEncJwk = JWK.parse(IOUtils.toString(ecEncKey.getInputStream(), "UTF-8"));
+        Assert.assertTrue(listContainsPublicJwk(keys, ecEncJwk));
+    }
+    
+    protected boolean listContainsPublicJwk(final JWKSet jwkSet, final JWK jwk) {
+        for (final JWK item : jwkSet.getKeys()) {
+            Assert.assertEquals(item.toJSONString(), item.toPublicJWK().toJSONString());
+            if (jwk.getKeyType().equals(item.getKeyType())
+                    && jwk.getKeyUse().equals(item.getKeyUse())
+                    && jwk.getKeyID().equals(item.getKeyID())) {
+                if (jwk.getKeyType().equals(KeyType.EC)) {
+                    Assert.assertNull(item.toJSONObject().get("d"));
+                    if (jsonValueEquals(jwk, item, "crv")
+                            && jsonValueEquals(jwk, item, "x")
+                            && jsonValueEquals(jwk, item, "y")) {
+                        return true;
+                    }
+                } else if (jwk.getKeyType().equals(KeyType.RSA)) {
+                    Assert.assertNull(item.toJSONObject().get("d"));
+                    if (jsonValueEquals(jwk, item, "e")
+                            && jsonValueEquals(jwk, item, "n")) {
+                        return true;
+                    }
+                }
+            }
+        }
+        return false;
+    }
+    
+    protected boolean jsonValueEquals(final JWK first, final JWK another, final String key) {
+        return first.toJSONObject().get(key).equals((another.toJSONObject().get(key)));
+    }
+
+
+}
diff --git a/idp-oidfed-op-impl/src/test/resources/net/shibboleth/idp/module/conf/relying-party.xml b/idp-oidfed-op-impl/src/test/resources/net/shibboleth/idp/module/conf/relying-party.xml
index 8466c66..60ce13d 100644
--- a/idp-oidfed-op-impl/src/test/resources/net/shibboleth/idp/module/conf/relying-party.xml
+++ b/idp-oidfed-op-impl/src/test/resources/net/shibboleth/idp/module/conf/relying-party.xml
@@ -40,6 +40,7 @@
                 <ref bean="OIDC.Registration" />
                 <ref bean="OIDC.Configuration" />
                 <bean parent="OIDFED.Configuration" p:cachedSuccessResponseLifetime="PT2S" />
+                <bean parent="OIDFED.Keyset" p:cachedSuccessResponseLifetime="PT2S" />
                 <bean parent="OIDFED.ResolveEntity" />
             </list>
         </property>
diff --git a/pom.xml b/pom.xml
index b84cf69..366a7cf 100644
--- a/pom.xml
+++ b/pom.xml
@@ -39,6 +39,7 @@
         <shib-attribute.version>5.0.0</shib-attribute.version>
         <shib-metadata.groupId>net.shibboleth</shib-metadata.groupId>
         <shib-metadata.version>5.0.0</shib-metadata.version>
+        <commons.io.version>2.6</commons.io.version>
         <checkstyle.configLocation>${project.basedir}/resources/checkstyle/checkstyle.xml</checkstyle.configLocation>
     </properties>
 
@@ -179,6 +180,12 @@
                 <version>${nashorn.jdk.version}</version>
                 <scope>test</scope>
             </dependency>
+            <dependency>
+                <groupId>commons-io</groupId>
+                <artifactId>commons-io</artifactId>
+                <version>${commons.io.version}</version>
+                <scope>test</scope>
+            </dependency>
         </dependencies>
     </dependencyManagement>
     

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


More information about the commits mailing list