[java-idp-plugin-oidc-op-oidfed] branch dev/CACHE-REFACTOR updated: Initial implementation for consuming signed_jwks_uri

Codeberg noreply at shibboleth.net
Tue Apr 7 05:58:27 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/1087aeaf73dd55b87e32c6c13cfa78c0d9490dcc

The following commit(s) were added to refs/heads/dev/CACHE-REFACTOR by this push:
     new 1087aea  Initial implementation for consuming signed_jwks_uri
1087aea is described below

commit 1087aeaf73dd55b87e32c6c13cfa78c0d9490dcc
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Tue Apr 7 08:58:02 2026 +0300

    Initial implementation for consuming signed_jwks_uri
    
    - Contents fetched via metadata cache, defaults to shibboleth.oidfed.SignedKeysetMetadataCache
      - Configurable via idp.oidfed.SignedKeysetMetadataCache
      - Cache identifier: <subject>:<signed_jwks_uri>
    - Resolution integrated to existing credential resolvers via JWKSetDecorator (JCOMOIDC-161)
      - Requires dependency to the latest snapshots of oidc-common and oidc-config
---
 .../metadata/payload/SignedKeysetPayload.java      |   6 +-
 ...faultSignedKeysetCriteriaSetLookupFunction.java |  49 +++++
 .../DefaultSignedKeysetFetchingStrategy.java       | 145 +++++++++++++
 .../cache/keyset/SignedKeysetCacheIdentifier.java  | 105 ++++++++++
 .../cache/keyset/SignedKeysetContainer.java        |  68 ++++++
 .../cache/keyset/SubjectSignedKeysetCriterion.java |  78 +++++++
 .../keyset/SubjectSignedKeysetUriCriterion.java    |  79 +++++++
 .../oidfed/metadata/jackson/JWKDeserializer.java   |  72 +++++++
 .../payload/impl/SignedKeysetPayloadImpl.java      |  10 +-
 .../op/oidfed/profile/impl/BuildSignedKeyset.java  |   2 +-
 ...ientInformationSignedKeysetJWKSetDecorator.java | 174 +++++++++++++++
 .../META-INF/net.shibboleth.idp/postconfig.xml     | 106 ++++++++++
 .../AuthorizeFlowAutomaticRegistrationTest.java    |  61 ++++++
 .../profile/flow/oidfed/SignedKeysetFlowTest.java  |   8 +-
 .../cache/SignedKeysetMetadataCacheTest.java       | 233 +++++++++++++++++++++
 .../idp/module/conf/oidfed/oidfed.properties       |   2 +
 pom.xml                                            |   4 +-
 17 files changed, 1189 insertions(+), 13 deletions(-)

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
index e158d63..87d2415 100644
--- 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
@@ -14,7 +14,9 @@
 
 package net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.payload;
 
-import com.nimbusds.jose.jwk.JWKSet;
+import java.util.List;
+
+import com.nimbusds.jose.jwk.JWK;
 
 /**
  * Signed keyset payload claims as defined by the OpenID Federation 1.0 Section 5.2.1.
@@ -26,6 +28,6 @@ public interface SignedKeysetPayload extends BaseExpirableSubjectPayload {
      * 
      * @return JWK set
      */
-    public JWKSet getKeys();
+    public List<JWK> getKeys();
 
 }
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/keyset/DefaultSignedKeysetCriteriaSetLookupFunction.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/keyset/DefaultSignedKeysetCriteriaSetLookupFunction.java
new file mode 100644
index 0000000..0d7607c
--- /dev/null
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/keyset/DefaultSignedKeysetCriteriaSetLookupFunction.java
@@ -0,0 +1,49 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.keyset;
+
+import java.util.function.Function;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.ThreadSafe;
+
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.SignedKeyset;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.BaseExpirableStatementContainer;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.SubjectEntityStatementCriterion;
+import net.shibboleth.shared.resolver.CriteriaSet;
+
+/**
+ * Default criteria set lookup strategy for signed keyset.
+ */
+ at ThreadSafe
+public class DefaultSignedKeysetCriteriaSetLookupFunction
+    implements Function<BaseExpirableStatementContainer<?>, CriteriaSet> {
+
+    /** {@inheritDoc} */
+    @Override @Nullable
+    public CriteriaSet apply(@Nullable final BaseExpirableStatementContainer<?> metadataContainer) {
+        if (metadataContainer instanceof SignedKeysetContainer keysetContainer) {
+            final CriteriaSet result = new CriteriaSet(
+                    new SubjectEntityStatementCriterion(keysetContainer.getIdentifier().getEntityConfiguration()));
+            final SignedKeyset keyset = keysetContainer.getStatement();
+            if (keyset != null) {
+                result.add(new SubjectSignedKeysetCriterion(keyset));
+            }
+            return result;
+        }
+        return null;
+    }
+
+}
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/keyset/DefaultSignedKeysetFetchingStrategy.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/keyset/DefaultSignedKeysetFetchingStrategy.java
new file mode 100644
index 0000000..115bc3b
--- /dev/null
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/keyset/DefaultSignedKeysetFetchingStrategy.java
@@ -0,0 +1,145 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.keyset;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.time.Instant;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.apache.hc.client5.http.classic.methods.HttpGet;
+import org.apache.hc.core5.http.ClassicHttpRequest;
+import org.apache.hc.core5.http.ClassicHttpResponse;
+import org.apache.hc.core5.http.ProtocolException;
+import org.apache.hc.core5.http.io.entity.EntityUtils;
+import org.slf4j.Logger;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.nimbusds.jose.JOSEObjectType;
+import com.nimbusds.jwt.SignedJWT;
+
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.EntityConfiguration;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.SignedKeyset;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.AbstractFederationEndpointResponseFetchingStrategy;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.SubjectEntityStatementCriterion;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.impl.SignedKeysetImpl;
+import net.shibboleth.shared.annotation.constraint.ThreadSafeAfterInit;
+import net.shibboleth.shared.logic.ConstraintViolationException;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.resolver.CriteriaSet;
+
+/**
+ * Default strategy for fetching signed keyset for the request specified in the criteria set. The parsed response
+ * details are stored inside a {@link SignedKeysetContainer}.
+ */
+ at ThreadSafeAfterInit
+public class DefaultSignedKeysetFetchingStrategy
+    extends AbstractFederationEndpointResponseFetchingStrategy<SignedKeysetCacheIdentifier, SignedKeysetContainer> {
+
+    /** The JWT type header. */
+    @Nonnull
+    public static final JOSEObjectType JWT_TYPE_HEADER = new JOSEObjectType("jwk-set+jwt");
+
+    /** The HTTP response content type. */
+    @Nonnull public static final String HTTP_RESPONSE_CONTENT_TYPE = "application/" + JWT_TYPE_HEADER.toString();
+
+    /** Class logger. */
+    @Nonnull private Logger log = LoggerFactory.getLogger(DefaultSignedKeysetFetchingStrategy.class);
+
+    /**
+     * Constructor.
+     */
+    public DefaultSignedKeysetFetchingStrategy() {
+        setCriteriaToRequestDataStrategy(criteria -> {
+            final SubjectEntityStatementCriterion subjectCriterion =
+                    criteria.get(SubjectEntityStatementCriterion.class);
+            if (subjectCriterion == null) {
+                log.debug("No SubjectEntityStatementCriterion, returning null");
+                return null;
+            }
+            final SubjectSignedKeysetUriCriterion uriCriterion = criteria.get(SubjectSignedKeysetUriCriterion.class);
+            if (uriCriterion == null) {
+                log.debug("No SubjectSignedKeysetUriCriterion, returning null");
+                return null;
+            }
+            if (subjectCriterion.getValue() instanceof EntityConfiguration entityConfiguration) {
+                return new SignedKeysetCacheIdentifier(entityConfiguration, uriCriterion.getValue());
+            }
+            log.debug("SubjectEntityStatementCriterion did not contain entity configuration, returning null");
+            return null;
+        });
+    }
+
+    /** {@inheritDoc} */
+    @Nullable protected ClassicHttpRequest initializeHttpRequest(@Nonnull final CriteriaSet criteria,
+            @Nonnull final SignedKeysetCacheIdentifier identifier) {
+        try {
+            return new HttpGet(new URI(identifier.getUri()));
+        } catch (final URISyntaxException e) {
+            log.error("Could not create URI for {} with the given parameters {}",
+                    identifier.getEntityConfiguration().getSubject(), identifier.getUri(), e);
+        }
+        return null;
+    }
+
+    /** {@inheritDoc} */
+    @Nullable protected SignedKeysetContainer parseHttpResponse(@Nonnull final CriteriaSet criteria,
+            @Nonnull final SignedKeysetCacheIdentifier identifier, @Nullable final ClassicHttpResponse response,
+            @Nonnull final Instant validExpiration, @Nonnull final Instant invalidExpiration,
+            @Nonnull final Instant nullExpiration) throws ProtocolException, IOException {
+        if (response != null) {
+            if (!HTTP_RESPONSE_CONTENT_TYPE.equals(response.getEntity().getContentType())) {
+                log.warn("Unexpected content type: {}", response.getEntity().getContentType());
+                return new SignedKeysetContainer(identifier, null, validExpiration, invalidExpiration);
+            }
+
+            try {
+                final String content = EntityUtils.toString(response.getEntity());
+                log.trace("Attempting to parse signed JWT from content: {}", content);
+                final SignedJWT jwt = SignedJWT.parse(content);
+                if (!JWT_TYPE_HEADER.equals(jwt.getHeader().getType())) {
+                    log.warn("Unexpected JWT type header {}", jwt.getHeader().getType());
+                    return new SignedKeysetContainer(identifier, null, validExpiration, invalidExpiration);
+                }
+                assert objectMapper != null;
+                final SignedKeyset signedKeyset = SignedKeysetImpl.parse(jwt, objectMapper);
+                return new SignedKeysetContainer(
+                        identifier, signedKeyset, validExpiration, invalidExpiration);
+            } catch (final JsonProcessingException e) {
+                log.warn("Could not deserialize signed keyset payload from the response", e);
+            } catch (final ConstraintViolationException e) {
+                log.warn("Parsed signed keyset claims did not pass syntax validation", e);
+            } catch (java.text.ParseException e) {
+                log.warn("Could not parse JWT from the response", e);
+            }
+            return new SignedKeysetContainer(identifier, null, validExpiration, invalidExpiration);
+        } else {
+            log.debug("Unable to fetch signed keyset for: {}, from {} (null response)",
+                    identifier.getEntityConfiguration().getSubject(), identifier.getUri());
+            return new SignedKeysetContainer(identifier, null, validExpiration, nullExpiration);
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Nullable protected SignedKeysetContainer handleException(@Nonnull final CriteriaSet criteria,
+            @Nonnull final SignedKeysetCacheIdentifier identifier, @Nonnull final Throwable throwable,
+            @Nonnull final Instant expiration) {
+        return new SignedKeysetContainer(identifier, null, expiration, expiration);
+    }
+
+}
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/keyset/SignedKeysetCacheIdentifier.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/keyset/SignedKeysetCacheIdentifier.java
new file mode 100644
index 0000000..7b7b50e
--- /dev/null
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/keyset/SignedKeysetCacheIdentifier.java
@@ -0,0 +1,105 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.keyset;
+
+import java.io.Serializable;
+import java.util.Objects;
+
+import javax.annotation.Nonnull;
+
+import com.google.common.base.MoreObjects;
+
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.EntityConfiguration;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.logic.Constraint;
+
+/**
+ * Default identifier for signed keyset within {@link SignedKeysetContainer}.
+ */
+public class SignedKeysetCacheIdentifier implements Serializable {
+
+    /** Serial version UID. */
+    private static final long serialVersionUID = -4169816153785776493L;
+
+    /** Entity configuration related to the signed keyset. */
+    @Nonnull private final EntityConfiguration entityConfiguration;
+
+    /** URI of the signed keyset. */
+    @Nonnull @NotEmpty private final String uri;
+
+    /**
+     * Constructor.
+     *
+     * @param configuration entity configuration
+     * @param keysetUri keyset URI
+     */
+    public SignedKeysetCacheIdentifier(@Nonnull final EntityConfiguration configuration,
+            @Nonnull @NotEmpty final String keysetUri) {
+        entityConfiguration = Constraint.isNotNull(configuration, "Subject entity configuration cannot be empty");
+        uri = Constraint.isNotEmpty(keysetUri, "Keyset URI cannot be empty");
+    }
+
+    /**
+     * Get the URI of the signed keyset.
+     * 
+     * @return uri
+     */
+    @Nonnull @NotEmpty public String getUri() {
+        return uri;
+    }
+
+    /**
+     * Get the subject entity configuration
+     * 
+     * @return entity configuration
+     */
+    @Nonnull public EntityConfiguration getEntityConfiguration() {
+        return entityConfiguration;
+    }
+
+    /** {@inheritDoc} */
+    @Override public String toString() {
+        return MoreObjects.toStringHelper(this)
+                .add("entityConfiguration", getEntityConfiguration().getJwt().serialize())
+                .add("uri", getUri())
+                .toString();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public int hashCode() {
+        return Objects.hash(uri, entityConfiguration);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean equals(final Object obj) {
+        if (this == obj) {
+            return true;
+        }
+
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final SignedKeysetCacheIdentifier other = (SignedKeysetCacheIdentifier) obj;
+        return uri.equals(other.uri) && entityConfiguration.getJwt().serialize().equals(
+                other.entityConfiguration.getJwt().serialize());
+
+    }
+
+}
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/keyset/SignedKeysetContainer.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/keyset/SignedKeysetContainer.java
new file mode 100644
index 0000000..b766e71
--- /dev/null
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/keyset/SignedKeysetContainer.java
@@ -0,0 +1,68 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.keyset;
+
+import java.io.Serializable;
+import java.time.Instant;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.SignedKeyset;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.BaseExpirableStatementContainer;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.logic.Constraint;
+
+/**
+ * A container class for metadata caches carrying entity ID and keyset URI details related to signed keyset.
+ */
+public class SignedKeysetContainer extends BaseExpirableStatementContainer<SignedKeyset>
+    implements Serializable {
+
+    /** Serial version UID. */
+    private static final long serialVersionUID = 756269369356865370L;
+
+    /** Signed keyset identifier. */
+    @Nonnull private final SignedKeysetCacheIdentifier identifier;
+
+    /**
+     * Constructor.
+     *
+     * @param id signed keyset identifier, must not be null
+     * @param keyset signed keyset parsed from the response
+     * @param validExpirationInstant expiration instant for valid statement, must not be null
+     * @param invalidExpirationInstant expiration instant for invalid statement, must not be null
+     */
+    public SignedKeysetContainer(@Nonnull final SignedKeysetCacheIdentifier id,
+            @Nullable final SignedKeyset keyset, @Nonnull final Instant validExpirationInstant,
+            @Nonnull final Instant invalidExpirationInstant) {
+        super(validExpirationInstant, keyset, invalidExpirationInstant);
+        identifier = Constraint.isNotNull(id, "Signed keyset identifier cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    @Nonnull @NotEmpty public String getEntityId() {
+        return identifier.getEntityConfiguration().getSubject();
+    }
+
+    /**
+     * Get the signed keyset cache identifier.
+     * 
+     * @return keyset cache identifier
+     */
+    @Nonnull public SignedKeysetCacheIdentifier getIdentifier() {
+        return identifier;
+    }
+}
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/keyset/SubjectSignedKeysetCriterion.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/keyset/SubjectSignedKeysetCriterion.java
new file mode 100644
index 0000000..d32a486
--- /dev/null
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/keyset/SubjectSignedKeysetCriterion.java
@@ -0,0 +1,78 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.keyset;
+
+import java.util.Objects;
+
+import javax.annotation.Nonnull;
+
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.SignedKeyset;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.resolver.Criterion;
+
+/**
+ * A {@link Criterion} representing a subject signed keyset.
+ */
+public class SubjectSignedKeysetCriterion implements Criterion {
+
+    /** The signed keyset value. */
+    @Nonnull final SignedKeyset value;
+
+    /**
+     * Constructor.
+     *
+     * @param keyset signed keyset value, must not be null
+     */
+    public SubjectSignedKeysetCriterion(@Nonnull final SignedKeyset keyset) {
+        value = Constraint.isNotNull(keyset, "Signed keyset cannot be null");
+    }
+
+    /**
+     * Get the signed keyset value.
+     * 
+     * @return the signed keyset value
+     */
+    @Nonnull public SignedKeyset getValue() {
+        return value;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String toString() {
+        return "SubjectSignedKeysetCriterion [value=" + value.getJwt().serialize() + "]";
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public int hashCode() {
+        return Objects.hash(value);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean equals(final Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final SubjectSignedKeysetCriterion other = (SubjectSignedKeysetCriterion) obj;
+        return value.getJwt().serialize().equals(other.value.getJwt().serialize());
+    }
+}
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/keyset/SubjectSignedKeysetUriCriterion.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/keyset/SubjectSignedKeysetUriCriterion.java
new file mode 100644
index 0000000..e0ab097
--- /dev/null
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/keyset/SubjectSignedKeysetUriCriterion.java
@@ -0,0 +1,79 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.keyset;
+
+import java.util.Objects;
+
+import javax.annotation.Nonnull;
+
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.resolver.Criterion;
+
+/**
+ * A {@link Criterion} representing a signed_jwks_uri value for an entity.
+ */
+public class SubjectSignedKeysetUriCriterion implements Criterion {
+
+    /** The signed keyset URI value. */
+    @Nonnull @NotEmpty private final String value;
+
+    /**
+     * Constructor.
+     *
+     * @param uri the signed keyset URI value, must not be null
+     */
+    public SubjectSignedKeysetUriCriterion(@Nonnull @NotEmpty final String uri) {
+        value = Constraint.isNotEmpty(uri, "Signed keyset URI cannot be null");
+    }
+
+    /**
+     * Get the signed keyset URI value.
+     * 
+     * @return the signed keyset URI value
+     */
+    @Nonnull @NotEmpty public String getValue() {
+        return value;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String toString() {
+        return "SubjectSignedKeysetUriCriterion [value=" + value + "]";
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public int hashCode() {
+        return Objects.hash(value);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean equals(final Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final SubjectSignedKeysetUriCriterion other = (SubjectSignedKeysetUriCriterion) obj;
+        return value.equals(other.value);
+    }
+
+}
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/jackson/JWKDeserializer.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/jackson/JWKDeserializer.java
new file mode 100644
index 0000000..9516191
--- /dev/null
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/jackson/JWKDeserializer.java
@@ -0,0 +1,72 @@
+/*
+ * 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.jackson;
+
+import java.io.IOException;
+import java.text.ParseException;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+
+import org.slf4j.Logger;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JavaType;
+import com.fasterxml.jackson.databind.JsonDeserializer;
+import com.fasterxml.jackson.databind.type.MapType;
+import com.fasterxml.jackson.databind.type.TypeFactory;
+import com.nimbusds.jose.jwk.JWK;
+
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * Jackson deserializer for {@link JWK} that exploits {@link JWK#parse(Map)}.
+ */
+public class JWKDeserializer extends JsonDeserializer<JWK> {
+
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(JWKDeserializer.class);
+
+    /**
+     * Constructor.
+     */
+    public JWKDeserializer() {
+        // no op
+    }
+
+    /** {@inheritDoc} */
+    @Override @Nonnull
+    public JWK deserialize(final JsonParser jsonParser, final DeserializationContext deserializationContext)
+            throws IOException, JsonProcessingException {
+
+        log.debug("Starting deseralize");
+        final JavaType objectType = TypeFactory.defaultInstance().constructType(Object.class);
+        final JavaType stringType = TypeFactory.defaultInstance().constructType(String.class);
+        final MapType objectMapType =
+                TypeFactory.defaultInstance().constructMapType(Map.class, stringType, objectType);
+
+        final Map<String,Object> map = deserializationContext.readValue(jsonParser, objectMapType);
+        try {
+            final JWK result = JWK.parse(map);
+            assert result != null;
+            return result;
+        } catch (final ParseException e) {
+            throw new IOException("Could not parse JWK from the given input", e);
+        }
+
+    }
+}
\ No newline at end of file
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
index c5d8c4c..f18363d 100644
--- 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
@@ -14,11 +14,13 @@
 
 package net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.payload.impl;
 
+import java.util.List;
+
 import javax.annotation.Nonnull;
 
 import com.fasterxml.jackson.annotation.JsonProperty;
 import com.google.common.base.MoreObjects;
-import com.nimbusds.jose.jwk.JWKSet;
+import com.nimbusds.jose.jwk.JWK;
 
 import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.payload.SignedKeysetPayload;
 
@@ -28,7 +30,7 @@ import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.payload.SignedKeysetPay
 public class SignedKeysetPayloadImpl extends BaseExpirableSubjectPayloadImpl implements SignedKeysetPayload {
 
     /** A JWK set. */
-    @JsonProperty("keys") private JWKSet keys;
+    @JsonProperty("keys") private List<JWK> keys;
 
     /**
      * Constructor.
@@ -52,7 +54,7 @@ public class SignedKeysetPayloadImpl extends BaseExpirableSubjectPayloadImpl imp
      * 
      * @return JWK set
      */
-    public JWKSet getKeys() {
+    public List<JWK> getKeys() {
         return keys;
     }
 
@@ -61,7 +63,7 @@ public class SignedKeysetPayloadImpl extends BaseExpirableSubjectPayloadImpl imp
      * 
      * @param jwks JWK set
      */
-    public void setKeys(final JWKSet jwks) {
+    public void setKeys(final List<JWK> jwks) {
         keys = jwks;
     }
 
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
index 2d08aa2..ccf421a 100644
--- 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
@@ -116,7 +116,7 @@ public class BuildSignedKeyset extends AbstractBuildEntityStatementAction {
 
         log.trace("{} Resolved jwks to publish: {}", getLogPrefix(), jwks);
         
-        builder.claim("keys", jwks.toJSONObject(true));
+        builder.claim("keys", jwks.toJSONObject(true).get("keys"));
 
         final Map<String, Function<ProfileRequestContext, Object>> optionalClaimsLookupStrategies =
                 optionalClaimsLookupStrategiesLookupStrategy.apply(profileRequestContext);
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/security/credential/ClientInformationSignedKeysetJWKSetDecorator.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/security/credential/ClientInformationSignedKeysetJWKSetDecorator.java
new file mode 100644
index 0000000..a3dd70a
--- /dev/null
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/security/credential/ClientInformationSignedKeysetJWKSetDecorator.java
@@ -0,0 +1,174 @@
+/*
+ * 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.security.credential;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
+import org.slf4j.Logger;
+
+import com.nimbusds.jose.jwk.JWK;
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformation;
+
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.EntityConfiguration;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.SubjectEntityStatementCriterion;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.keyset.SignedKeysetContainer;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.keyset.SubjectSignedKeysetUriCriterion;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.context.RelyingPartyTrustChainContext;
+import net.shibboleth.oidc.metadata.cache.MetadataCache;
+import net.shibboleth.oidc.metadata.cache.MetadataCacheException;
+import net.shibboleth.oidc.metadata.context.OIDCMetadataContext;
+import net.shibboleth.oidc.security.credential.ClientInformationJWKSetDecorator;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.ThreadSafeAfterInit;
+import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.resolver.CriteriaSet;
+
+/**
+ * A {@link ClientInformationJWKSetDecorator} that fetches the contents of signed_jwks_uri via configured metadata
+ * cache.
+ */
+ at ThreadSafeAfterInit
+public class ClientInformationSignedKeysetJWKSetDecorator extends AbstractIdentifiableInitializableComponent 
+    implements ClientInformationJWKSetDecorator {
+    
+    /** Class logger. */
+    @Nonnull private Logger log = LoggerFactory.getLogger(ClientInformationSignedKeysetJWKSetDecorator.class);
+
+    /** Metadata cache used for fetching signed keysets. */
+    @NonnullAfterInit private MetadataCache<SignedKeysetContainer> signedKeysetCache;
+
+    /** Strategy used to lookup the trust chain context. */
+    @Nonnull protected Function<ProfileRequestContext, RelyingPartyTrustChainContext> trustChainContextLookupStrategy;
+
+    /**
+     * Constructor.
+     */
+    public ClientInformationSignedKeysetJWKSetDecorator() {
+        final Function<ProfileRequestContext, RelyingPartyTrustChainContext> tcls =
+                new ChildContextLookup<>(RelyingPartyTrustChainContext.class).compose(
+                        new InboundMessageContextLookup());
+        assert tcls != null;
+        trustChainContextLookupStrategy = tcls;
+    }
+
+    /**
+     * Set the metadata cache used for fetching signed keysets.
+     * 
+     * @param cache signed keyset cache
+     */
+    public void setSignedKeysetCache(@Nonnull final MetadataCache<SignedKeysetContainer> cache) {
+        checkSetterPreconditions();
+        signedKeysetCache = Constraint.isNotNull(cache, "SignedKeysetCache cannot be null");
+    }
+
+    /**
+     * Set the strategy used to lookup the trust chain context.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setTrustChainContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext, RelyingPartyTrustChainContext> strategy) {
+        checkSetterPreconditions();
+        trustChainContextLookupStrategy =
+                Constraint.isNotNull(strategy, "TrustChainContextLookupStrategy cannot be null");
+    }
+
+
+    /** {@inheritDoc} */
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        if (signedKeysetCache == null) {
+            throw new ComponentInitializationException("SignedKeysetCache cannot be null");
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void accept(@Nullable final List<JWK> keyset, @Nullable final ProfileRequestContext profileRequestContext) {
+        checkComponentActive();
+        if (keyset == null || profileRequestContext == null) {
+            log.error("Null input, unable to proceed");
+            return;
+        }
+
+        final OIDCClientInformation clientInformation = Optional.ofNullable(profileRequestContext)
+                .map(prc -> prc.getInboundMessageContext())
+                .map(msgCtx -> msgCtx.getSubcontext(OIDCMetadataContext.class))
+                .map(oidcCtx -> oidcCtx.getClientInformation())
+                .orElse(null);
+        if (clientInformation == null) {
+            log.error("Could not resolve client information, unable to proceed");
+            return;
+        }
+        final String signedJwksUri = Optional.ofNullable(clientInformation.getOIDCMetadata().getSignedJWKSetURI())
+                .map(uri -> uri.toString())
+                .orElse(null);
+        if (signedJwksUri == null) {
+            log.trace("No signed_jwks_uri in the metadata, nothing to do");
+            return;
+        }
+
+        final EntityConfiguration entityConfiguration =
+                Optional.ofNullable(trustChainContextLookupStrategy.apply(profileRequestContext))
+                .map(trustChainContext -> trustChainContext.getSelectedTrustChain())
+                .map(selectedChain -> selectedChain.getTrustChain())
+                .map(trustChain -> trustChain.get(0))
+                .filter(EntityConfiguration.class::isInstance)
+                .map(EntityConfiguration.class::cast)
+                .orElse(null);
+        if (entityConfiguration == null) {
+            log.error("Could not resolve entity configuration, unable to proceed");
+            return;
+        }
+        log.trace("Resolved entity configuration {}", entityConfiguration.getSubject());
+
+        final List<SignedKeysetContainer> containers;
+        try {
+            containers = signedKeysetCache.get(new CriteriaSet(new SubjectEntityStatementCriterion(entityConfiguration),
+                    new SubjectSignedKeysetUriCriterion(signedJwksUri)));
+        } catch (final MetadataCacheException e) {
+            log.warn("Could not resolve signed keyset contents via metadata cache from {} for {}", signedJwksUri,
+                    entityConfiguration.getSubject(), e);
+            return;
+        }
+        if (containers.isEmpty()) {
+            log.warn("No signed keyset containers were resolved via cache from {} for {}", signedJwksUri,
+                    entityConfiguration.getSubject());
+            return;
+        }
+        final List<JWK> signedKeys = Optional.ofNullable(containers.get(0).getStatement())
+                .map(statement -> statement.getParsedPayload().getKeys())
+                .orElse(null);
+        if (signedKeys == null) {
+            log.warn("No signed keys were resolved via cache from {} for {}", signedJwksUri,
+                    entityConfiguration.getSubject());
+            return;
+        }
+        signedKeys.forEach(key -> keyset.add(key));
+        log.trace("Signed keys {} successfully populated for {}", signedKeys, entityConfiguration.getSubject());
+    }
+
+}
diff --git a/idp-oidfed-op-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml b/idp-oidfed-op-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
index 01e771e..fb85aa7 100644
--- a/idp-oidfed-op-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
+++ b/idp-oidfed-op-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
@@ -83,6 +83,13 @@
         </constructor-arg>
     </bean>
 
+    <bean id="shibboleth.oidfed.SignedKeysetMetadataCache" parent="shibboleth.oidc.CacheBuilder">
+        <constructor-arg>
+            <bean p:cacheId="DefaultSignedKeysetMetadataCache" parent="shibboleth.oidfed.SignedKeysetMetadataCacheBuilderSpec"
+                p:cleanupTaskInterval="%{idp.oidfed.cache.signedKeyset.cleanupTaskInterval:PT5M}"/>
+        </constructor-arg>
+    </bean>
+
     <bean id="DefaultResponseContainerExpirationTimeStrategy"
         class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.DefaultResponseContainerExpirationTimeStrategy"/>
 
@@ -708,6 +715,90 @@
         </constructor-arg>
     </bean>
 
+    <bean id="shibboleth.oidfed.SignedKeysetMetadataCacheBuilderSpec"
+        class="net.shibboleth.oidc.metadata.cache.impl.DynamicMetadataCacheBuilderSpec"
+        p:minCacheDuration="%{idp.oidfed.cache.signedKeyset.minRefreshDelay:PT1M}"
+        p:maxCacheDuration="%{idp.oidfed.cache.signedKeyset.maxRefreshDelay:PT30M}"
+        p:metadataExpirationTimeStrategy-ref="DefaultResponseContainerExpirationTimeStrategy">
+        <property name="criteriaToIdentifierStrategy">
+            <bean parent="shibboleth.Functions.Expression" c:expression="#input?.get(T(net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.SubjectEntityStatementCriterion))?.getValue().getSubject() + ':' + #input?.get(T(net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.keyset.SubjectSignedKeysetUriCriterion))?.getValue()"/>
+        </property>
+        <property name="identifierExtractionStrategy">
+            <bean parent="shibboleth.Functions.Expression" c:expression="#input?.getEntityId() + ':' + #input?.getIdentifier().getUri()"/>
+        </property>
+        <property name="fetchStrategy">
+            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.keyset.DefaultSignedKeysetFetchingStrategy"
+                p:httpClient="#{getObject('shibboleth.oidfed.HttpClient') ?: getObject('shibboleth.InternalHttpClient')}"
+                p:httpClientSecurityParameters="#{getObject('shibboleth.oidfed.NonBrowser.HttpClientSecurityParameters')}"
+                p:objectMapper-ref="shibboleth.oidfed.JWTPayloadJSONObjectMapper">
+                <property name="criteriaToValidContainerLifetimeStrategy">
+                    <bean parent="shibboleth.Functions.Constant">
+                        <constructor-arg>
+                            <bean class="java.time.Duration" factory-method="parse" c:_0="%{idp.oidfed.cache.signedKeyset.validContainerLifetime:PT5M}" />
+                        </constructor-arg>
+                    </bean>
+                </property>
+                <property name="criteriaToInvalidContainerLifetimeStrategy">
+                    <bean parent="shibboleth.Functions.Constant">
+                        <constructor-arg>
+                            <bean class="java.time.Duration" factory-method="parse" c:_0="%{idp.oidfed.cache.signedKeyset.invalidContainerLifetime:PT1M}" />
+                        </constructor-arg>
+                    </bean>
+                </property>
+                <property name="criteriaToExceptionContainerLifetimeStrategy">
+                    <bean parent="shibboleth.Functions.Constant">
+                        <constructor-arg>
+                            <bean class="java.time.Duration" factory-method="parse" c:_0="%{idp.oidfed.cache.signedKeyset.exceptionContainerLifetime:PT0S}" />
+                        </constructor-arg>
+                    </bean>
+                </property>
+            </bean>
+        </property>
+        <property name="metadataFilterStrategy">
+            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.DefaultEntityStatementMetadataFilterStrategy"
+                p:customFilterStrategies="#{getObject('%{idp.oidfed.cache.signedKeyset.customFilterStrategies:}'.trim())}">
+                <property name="validationConditions">
+                    <util:list value-type="java.util.function.BiPredicate">
+                        <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.DefaultEntityStatementSignatureValidationCondition"
+                            p:trustEngine-ref="shibboleth.oidfed.DefaultEntityConfigurationTrustEngine">
+                            <property name="criteriaSetLookupStrategy">
+                                <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.keyset.DefaultSignedKeysetCriteriaSetLookupFunction"/>
+                            </property>
+                        </bean>
+                        <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.DefaultEntityStatementContentValidationCondition"
+                            p:claimsValidator-ref="%{idp.oidfed.signedKeyset.claimsValidator:shibboleth.oidfed.DefaultSignedKeysetClaimsValidator}"
+                            p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier">
+                            <property name="headerValidator">
+                                <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.DefaultTrustChainHeaderValidationCondition"
+                                    p:objectMapper-ref="shibboleth.oidfed.JWTPayloadJSONObjectMapper" />
+                            </property>
+                        </bean>
+                    </util:list>
+                </property>
+            </bean>
+        </property>
+    </bean>
+
+    <bean id="shibboleth.oidfed.DefaultSignedKeysetClaimsValidator"
+        class="net.shibboleth.oidc.security.jwt.claims.impl.ChainingJWTClaimsValidator">
+        <property name="claimValidators">
+            <util:list value-type="net.shibboleth.oidc.jwt.claims.ClaimsValidator">
+                <bean class="net.shibboleth.oidc.security.jwt.claims.impl.IssuedAtClaimsValidator"
+                    p:clockSkew="%{idp.policy.clockSkew:PT1M}"
+                    p:messageLifetime="%{idp.oidfed.cache.entityConfiguration.maxJwtLifetime:PT24H}"
+                    p:requiredRule="true" />
+                <bean class="net.shibboleth.oidc.security.jwt.claims.impl.RequiredClaimsValidator"
+                    p:requiredClaims="iss" />
+                <bean class="net.shibboleth.oidc.security.jwt.claims.impl.RequiredClaimsValidator"
+                    p:requiredClaims="sub" />
+                <bean class="net.shibboleth.oidc.security.jwt.claims.impl.RequiredClaimsValidator"
+                    p:requiredClaims="keys" />
+                <bean class="net.shibboleth.oidc.security.jwt.claims.impl.ExpiryClaimsValidator"
+                    p:clockSkew="%{idp.policy.clockSkew:PT1M}" />
+            </util:list>
+        </property>
+    </bean>
+
     <bean id="PreferFileSystemResourceLoader"
         class="net.shibboleth.shared.spring.resource.PreferFileSystemResourceLoader"/>
 
@@ -745,6 +836,17 @@
         </property>
     </bean>
 
+    <bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
+        <property name="targetObject" ref="shibboleth.oidfed.policy.JSONSimpleModule" />
+        <property name="targetMethod" value="addDeserializer" />
+        <property name="arguments">
+            <list>
+                <value>#{ T(com.nimbusds.jose.jwk.JWK)}</value>
+                <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.jackson.JWKDeserializer"/>
+            </list>
+        </property>
+    </bean>
+
     <bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
         <property name="targetObject" ref="shibboleth.oidfed.policy.JSONSimpleModule" />
         <property name="targetMethod" value="addDeserializer" />
@@ -950,6 +1052,10 @@
         class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.AutoRegisteredTrustChainRequestUriClaimsSetDecorator"
         p:claimsSetDecorators-ref="DefaultAutoRegisteredTokenClaimsSetDecorators"/>
 
+    <bean id="ClientInformationSignedKeysetJWKSetDecorator"
+        class="net.shibboleth.idp.plugin.oidc.op.oidfed.security.credential.ClientInformationSignedKeysetJWKSetDecorator"
+        p:signedKeysetCache-ref="#{'%{idp.oidfed.SignedKeysetMetadataCache:shibboleth.oidfed.SignedKeysetMetadataCache}'.trim()}" />
+
     <bean id="AutomaticRegistrationCondition"
         class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.logic.DefaultAutomaticRegistrationActivationCondition">
         <property name="profileIdLookupStrategy">
diff --git a/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/AuthorizeFlowAutomaticRegistrationTest.java b/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/AuthorizeFlowAutomaticRegistrationTest.java
index 9d9469b..1ecfa46 100644
--- a/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/AuthorizeFlowAutomaticRegistrationTest.java
+++ b/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/AuthorizeFlowAutomaticRegistrationTest.java
@@ -25,6 +25,7 @@ import java.time.Duration;
 import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -52,6 +53,7 @@ import com.nimbusds.openid.connect.sdk.AuthenticationResponse;
 import com.nimbusds.openid.connect.sdk.AuthenticationSuccessResponse;
 import com.nimbusds.openid.connect.sdk.rp.OIDCClientMetadata;
 
+import net.shibboleth.idp.plugin.oidc.op.oidfed.TrustChainTestUtil;
 import net.shibboleth.idp.plugin.oidc.op.profile.flow.AuthorizeFlowTest;
 import net.shibboleth.idp.plugin.oidc.op.profile.logic.DefaultPushedAuthorizationRequestUriSerializationFunction;
 import net.shibboleth.idp.plugin.oidc.op.token.support.AuthorizeCodeClaimsSet;
@@ -148,6 +150,65 @@ public class AuthorizeFlowAutomaticRegistrationTest extends AbstractFederationFl
         Assert.assertEquals(unwrapTrustChainFromAuthorizeCode(successResponse), List.of(clientId, anchorId));
     }
 
+    @Test
+    public void testWithValidTrustChain_signedRequestObject_validSignedJwksUri()
+            throws IOException, UnsupportedOperationException, URISyntaxException {
+        final String clientId = uniqueClientId();
+        final String signedJwksUri = clientId + "/jwks.jwt";
+        final OIDCClientMetadata metadata = new OIDCClientMetadata();
+        metadata.setRedirectionURI(new URI(redirectUri));
+        metadata.setSignedJWKSetURI(URI.create(signedJwksUri));
+
+        rpConfigureMockHttpClient(clientId, metadata);
+        final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(clientId).subject(clientId)
+                .issueTime(Date.from(Instant.now()))
+                .claim("keys", new JWKSet(rpKey).toJSONObject(true).get("keys"));
+        final String signedKeyset = TrustChainTestUtil.signedJwt(
+                JWSAlgorithm.RS256, leafKey, "jwk-set+jwt", builder.build()).serialize();
+        
+        mapResponse(signedJwksUri, mockResponse(200, "application/jwk-set+jwt", signedKeyset));
+        final FlowExecutionResult result =
+                launchAuthenticationRequest(clientId, "openid profile", signedRequestObject(Map.of(
+                        "iss", clientId,
+                        "client_id", clientId,
+                        "aud", issuer,
+                        "exp", Instant.now().plus(Duration.ofMinutes(5)).toEpochMilli(),
+                        "jti", UUID.randomUUID(),
+                        "response_type", "code",
+                        "scope", "openid profile",
+                        "redirect_uri", redirectUri)));
+        final AuthenticationResponse responseMessage = parseSuccessResponse(result, AuthenticationResponse.class);
+        final AuthenticationSuccessResponse successResponse = responseMessage.toSuccessResponse();
+        Assert.assertEquals(successResponse.getRedirectionURI().toString(), redirectUri);
+        Assert.assertNull(successResponse.getIDToken());
+        Assert.assertNull(successResponse.getAccessToken());
+        Assert.assertNotNull(successResponse.getAuthorizationCode());
+        Assert.assertEquals(unwrapTrustChainFromAuthorizeCode(successResponse), List.of(clientId, anchorId));
+    }
+
+    @Test
+    public void testWithValidTrustChain_signedRequestObject_nonExistingSignedJwksUri()
+            throws IOException, UnsupportedOperationException, URISyntaxException {
+        final String clientId = uniqueClientId();
+        final String signedJwksUri = clientId + "/jwks.jwt";
+        final OIDCClientMetadata metadata = new OIDCClientMetadata();
+        metadata.setRedirectionURI(new URI(redirectUri));
+        metadata.setSignedJWKSetURI(URI.create(signedJwksUri));
+
+        rpConfigureMockHttpClient(clientId, metadata);
+        final FlowExecutionResult result =
+                launchAuthenticationRequest(clientId, "openid profile", signedRequestObject(Map.of(
+                        "iss", clientId,
+                        "client_id", clientId,
+                        "aud", issuer,
+                        "exp", Instant.now().plus(Duration.ofMinutes(5)).toEpochMilli(),
+                        "jti", UUID.randomUUID(),
+                        "response_type", "code",
+                        "scope", "openid profile",
+                        "redirect_uri", redirectUri)));
+        Assert.assertEquals(result.getOutcome().getId(), "ErrorView");
+    }
+
     @Test
     public void testWithValidTrustChain_signedRequestObject_invalidJwksUri()
             throws IOException, UnsupportedOperationException, URISyntaxException {
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
index d5d4468..3a14788 100644
--- 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
@@ -15,6 +15,7 @@
 package net.shibboleth.idp.plugin.oidc.op.profile.flow.oidfed;
 
 import java.io.IOException;
+import java.util.List;
 
 import org.apache.commons.io.IOUtils;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -28,7 +29,6 @@ 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;
@@ -96,7 +96,7 @@ public class SignedKeysetFlowTest extends AbstractFederationFlowTest {
         Assert.assertEquals(payload.getSubject(), issuer);
         Assert.assertEquals(payload.getIssuer(), issuer);
         Assert.assertNull(payload.getExpiration());
-        final JWKSet keys = payload.getKeys();
+        final List<JWK> keys = payload.getKeys();
         Assert.assertNotNull(keys);
         Assert.assertEquals(keys.size(), 6);
         final JWK rsaSigJwk = JWK.parse(IOUtils.toString(rsaSigKey.getInputStream(), "UTF-8"));
@@ -113,8 +113,8 @@ public class SignedKeysetFlowTest extends AbstractFederationFlowTest {
         Assert.assertTrue(listContainsPublicJwk(keys, ecEncJwk));
     }
     
-    protected boolean listContainsPublicJwk(final JWKSet jwkSet, final JWK jwk) {
-        for (final JWK item : jwkSet.getKeys()) {
+    protected boolean listContainsPublicJwk(final List<JWK> jwkSet, final JWK jwk) {
+        for (final JWK item : jwkSet) {
             Assert.assertEquals(item.toJSONString(), item.toPublicJWK().toJSONString());
             if (jwk.getKeyType().equals(item.getKeyType())
                     && jwk.getKeyUse().equals(item.getKeyUse())
diff --git a/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/cache/SignedKeysetMetadataCacheTest.java b/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/cache/SignedKeysetMetadataCacheTest.java
new file mode 100644
index 0000000..44ab21e
--- /dev/null
+++ b/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/cache/SignedKeysetMetadataCacheTest.java
@@ -0,0 +1,233 @@
+/*
+ * 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.cache;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.time.Instant;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.jwk.JWK;
+import com.nimbusds.jose.jwk.JWKSet;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientMetadata;
+
+import net.shibboleth.idp.plugin.oidc.op.oidfed.TrustChainTestUtil;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.EntityConfiguration;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.EntityStatement;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.SignedKeyset;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.SubjectEntityStatementCriterion;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.keyset.SignedKeysetContainer;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.keyset.SubjectSignedKeysetUriCriterion;
+import net.shibboleth.idp.plugin.oidc.op.profile.flow.oidfed.AbstractFederationFlowTest;
+import net.shibboleth.idp.plugin.oidc.op.profile.flow.oidfed.EntityConfigurationFlowTest;
+import net.shibboleth.oidc.metadata.cache.MetadataCache;
+import net.shibboleth.oidc.metadata.cache.MetadataCacheException;
+import net.shibboleth.shared.resolver.CriteriaSet;
+
+/**
+ * Unit tests for the default signed keysetn metadata cache.
+ */
+ at SuppressWarnings("null")
+public class SignedKeysetMetadataCacheTest extends AbstractFederationFlowTest {
+
+    protected SignedKeysetMetadataCacheTest() {
+        super(EntityConfigurationFlowTest.FLOW_ID);
+    }
+
+    @Autowired
+    @Qualifier("shibboleth.oidfed.SignedKeysetMetadataCache")
+    MetadataCache<SignedKeysetContainer> signedKeysetCache;
+
+    @Test
+    public void testValidSignedKeyset()
+            throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+        final String entityId = uniqueClientId();
+        final String signedJwksUri = entityId + "/jwks.jwt";
+        final OIDCClientMetadata metadata = new OIDCClientMetadata();
+        metadata.setRedirectionURI(new URI(redirectUri));
+        metadata.setCustomField("signed_jwks_uri", signedJwksUri);
+        final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(entityId).subject(entityId)
+                .issueTime(Date.from(Instant.now()))
+                .claim("keys", new JWKSet(rpKey).toJSONObject(true).get("keys"));
+        final String signedKeyset = TrustChainTestUtil.signedJwt(
+                JWSAlgorithm.RS256, leafKey, "jwk-set+jwt", builder.build()).serialize();
+        
+        mapResponse(signedJwksUri, mockResponse(200, "application/jwk-set+jwt", signedKeyset));
+        try {
+            final List<SignedKeysetContainer> result =
+                    signedKeysetCache.get(new CriteriaSet(
+                            new SubjectSignedKeysetUriCriterion(signedJwksUri),
+                            new SubjectEntityStatementCriterion(buildEntityConfiguration(entityId, metadata))));
+            Assert.assertNotNull(result);
+            Assert.assertEquals(result.size(), 1);
+            final SignedKeyset statement = result.get(0).getStatement();
+            Assert.assertNotNull(statement);
+            assert statement != null;
+            final List<JWK> keys = statement.getParsedPayload().getKeys();
+            Assert.assertNotNull(keys);
+            Assert.assertEquals(keys.size(), 1);
+            Assert.assertEquals(keys.get(0), rpKey.toPublicJWK());
+        } catch (MetadataCacheException e) {
+            Assert.fail("Could not resolve entity configuration", e);
+        }
+    }
+
+    @Test
+    public void testValidSignedKeyset_optionalExpiration()
+            throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+        final String entityId = uniqueClientId();
+        final String signedJwksUri = entityId + "/jwks.jwt";
+        final OIDCClientMetadata metadata = new OIDCClientMetadata();
+        metadata.setRedirectionURI(new URI(redirectUri));
+        metadata.setCustomField("signed_jwks_uri", signedJwksUri);
+        final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(entityId).subject(entityId)
+                .issueTime(Date.from(Instant.now()))
+                .expirationTime(Date.from(Instant.now().plusSeconds(300)))
+                .claim("keys", new JWKSet(rpKey).toJSONObject(true).get("keys"));
+        final String signedKeyset = TrustChainTestUtil.signedJwt(
+                JWSAlgorithm.RS256, leafKey, "jwk-set+jwt", builder.build()).serialize();
+        
+        mapResponse(signedJwksUri, mockResponse(200, "application/jwk-set+jwt", signedKeyset));
+        try {
+            final List<SignedKeysetContainer> result =
+                    signedKeysetCache.get(new CriteriaSet(
+                            new SubjectSignedKeysetUriCriterion(signedJwksUri),
+                            new SubjectEntityStatementCriterion(buildEntityConfiguration(entityId, metadata))));
+            Assert.assertNotNull(result);
+            Assert.assertEquals(result.size(), 1);
+            final SignedKeyset statement = result.get(0).getStatement();
+            Assert.assertNotNull(statement);
+            assert statement != null;
+            final List<JWK> keys = statement.getParsedPayload().getKeys();
+            Assert.assertNotNull(keys);
+            Assert.assertEquals(keys.size(), 1);
+            Assert.assertEquals(keys.get(0), rpKey.toPublicJWK());
+        } catch (MetadataCacheException e) {
+            Assert.fail("Could not resolve entity configuration", e);
+        }
+    }
+
+    @Test
+    public void testInvalidSignedKeyset_expired()
+            throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+        final String entityId = uniqueClientId();
+        final String signedJwksUri = entityId + "/jwks.jwt";
+        final OIDCClientMetadata metadata = new OIDCClientMetadata();
+        metadata.setRedirectionURI(new URI(redirectUri));
+        metadata.setCustomField("signed_jwks_uri", signedJwksUri);
+        final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(entityId).subject(entityId)
+                .issueTime(Date.from(Instant.now()))
+                .expirationTime(Date.from(Instant.now().minusSeconds(300)))
+                .claim("keys", new JWKSet(rpKey).toJSONObject(true).get("keys"));
+        final String signedKeyset = TrustChainTestUtil.signedJwt(
+                JWSAlgorithm.RS256, leafKey, "jwk-set+jwt", builder.build()).serialize();
+        
+        mapResponse(signedJwksUri, mockResponse(200, "application/jwk-set+jwt", signedKeyset));
+        assertNoSignedKeyset(signedJwksUri, buildEntityConfiguration(entityId, metadata));
+    }
+
+    @Test
+    public void testInvalidSignedKeyset_invalidStatementType()
+            throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+        final String entityId = uniqueClientId();
+        final String signedJwksUri = entityId + "/jwks.jwt";
+        final OIDCClientMetadata metadata = new OIDCClientMetadata();
+        metadata.setRedirectionURI(new URI(redirectUri));
+        metadata.setCustomField("signed_jwks_uri", signedJwksUri);
+        final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(entityId).subject(entityId)
+                .issueTime(Date.from(Instant.now()))
+                .expirationTime(Date.from(Instant.now().plusSeconds(300)))
+                .claim("keys", new JWKSet(rpKey).toJSONObject(true).get("keys"));
+        final String signedKeyset = TrustChainTestUtil.signedJwt(
+                JWSAlgorithm.RS256, leafKey, "entity-statement+jwt", builder.build()).serialize();
+        
+        mapResponse(signedJwksUri, mockResponse(200, "application/jwk-set+jwt", signedKeyset));
+        assertNoSignedKeyset(signedJwksUri, buildEntityConfiguration(entityId, metadata));
+    }
+
+    @Test
+    public void testInvalidSignedKeyset_invalidResponseContentType()
+            throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+        final String entityId = uniqueClientId();
+        final String signedJwksUri = entityId + "/jwks.jwt";
+        final OIDCClientMetadata metadata = new OIDCClientMetadata();
+        metadata.setRedirectionURI(new URI(redirectUri));
+        metadata.setCustomField("signed_jwks_uri", signedJwksUri);
+        final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(entityId).subject(entityId)
+                .issueTime(Date.from(Instant.now()))
+                .expirationTime(Date.from(Instant.now().plusSeconds(300)))
+                .claim("keys", new JWKSet(rpKey).toJSONObject(true).get("keys"));
+        final String signedKeyset = TrustChainTestUtil.signedJwt(
+                JWSAlgorithm.RS256, leafKey, "jwk-set+jwt", builder.build()).serialize();
+        
+        mapResponse(signedJwksUri, mockResponse(200, "application/entity-statement+jwt", signedKeyset));
+        assertNoSignedKeyset(signedJwksUri, buildEntityConfiguration(entityId, metadata));
+    }
+
+    @Test
+    public void testInvalidSignedKeyset_wrongKey()
+            throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+        final String entityId = uniqueClientId();
+        final String signedJwksUri = entityId + "/jwks.jwt";
+        final OIDCClientMetadata metadata = new OIDCClientMetadata();
+        metadata.setRedirectionURI(new URI(redirectUri));
+        metadata.setCustomField("signed_jwks_uri", signedJwksUri);
+        final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(entityId).subject(entityId)
+                .issueTime(Date.from(Instant.now()))
+                .expirationTime(Date.from(Instant.now().minusSeconds(300)))
+                .claim("keys", new JWKSet(rpKey).toJSONObject(true).get("keys"));
+        final String signedKeyset = TrustChainTestUtil.signedJwt(
+                JWSAlgorithm.RS256, rpKey, "jwk-set+jwt", builder.build()).serialize();
+        
+        mapResponse(signedJwksUri, mockResponse(200, "application/jwk-set+jwt", signedKeyset));
+        assertNoSignedKeyset(signedJwksUri, buildEntityConfiguration(entityId, metadata));
+    }
+
+    protected EntityConfiguration buildEntityConfiguration(final String clientId, final OIDCClientMetadata metadata) {
+        final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(clientId).subject(clientId)
+                .issueTime(Date.from(Instant.now()))
+                .expirationTime(Date.from(Instant.now().plusSeconds(300)))
+                .claim("jwks", new JWKSet(leafKey).toJSONObject(true))
+                .claim("metadata", Map.of("openid_relying_party", metadata.toJSONObject()))
+                .claim("authority_hints", new String[] { anchorId });
+        final EntityStatement<?> rpConfiguration =
+                TrustChainTestUtil.entityStatement(JWSAlgorithm.RS256, leafKey, builder.build());
+        return (EntityConfiguration) rpConfiguration;
+    }
+
+    protected void assertNoSignedKeyset(final String signedJwksUri, final EntityConfiguration entityConfiguration) {
+        try {
+            final List<SignedKeysetContainer> result =
+                    signedKeysetCache.get(new CriteriaSet(
+                            new SubjectSignedKeysetUriCriterion(signedJwksUri),
+                            new SubjectEntityStatementCriterion(entityConfiguration)));
+            Assert.assertNotNull(result);
+            Assert.assertEquals(result.size(), 0);
+        } catch (MetadataCacheException e) {
+            Assert.fail("Could not resolve signed keyset", e);
+        }
+        
+    }
+}
diff --git a/idp-oidfed-op-impl/src/test/resources/net/shibboleth/idp/module/conf/oidfed/oidfed.properties b/idp-oidfed-op-impl/src/test/resources/net/shibboleth/idp/module/conf/oidfed/oidfed.properties
index d18dc17..6d166bf 100644
--- a/idp-oidfed-op-impl/src/test/resources/net/shibboleth/idp/module/conf/oidfed/oidfed.properties
+++ b/idp-oidfed-op-impl/src/test/resources/net/shibboleth/idp/module/conf/oidfed/oidfed.properties
@@ -2,6 +2,8 @@ idp.oidfed.entity.authorityHints = https://anchor1.example.org, https://anchor2.
 
 idp.oidfed.cache.entityConfiguration.invalidContainerLifetime = PT0S
 idp.oidfed.cache.entityConfiguration.minRefreshDelay = PT0S
+idp.oidfed.cache.signedKeyset.invalidContainerLifetime = PT0S
+idp.oidfed.cache.signedKeyset.minRefreshDelay = PT0S
 
 idp.oidfed.cache.default.critClaims = default_crit
 idp.oidfed.cache.entityConfiguration.customFilterStrategies = CustomEntityConfigurationFilters
diff --git a/pom.xml b/pom.xml
index 366a7cf..16cdfc8 100644
--- a/pom.xml
+++ b/pom.xml
@@ -26,9 +26,9 @@
         <shib-shared.testing.version>9.0.0</shib-shared.testing.version>
         <okhttp3.tls.version>4.9.3</okhttp3.tls.version>        
         <oidc-config.groupId>net.shibboleth.idp.plugin.config.oidc</oidc-config.groupId>
-        <oidc-config.version>3.0.0</oidc-config.version>
+        <oidc-config.version>3.1.0-SNAPSHOT</oidc-config.version>
         <oidc-common.groupId>net.shibboleth.oidc</oidc-common.groupId>
-        <oidc-common.version>3.3.0</oidc-common.version>
+        <oidc-common.version>3.4.0-SNAPSHOT</oidc-common.version>
         <oidc-op.groupId>net.shibboleth.idp.plugin.oidc</oidc-op.groupId>
         <oidc-op.version>4.4.0-SNAPSHOT</oidc-op.version>
         <shib-shared.groupId>net.shibboleth</shib-shared.groupId>

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


More information about the commits mailing list