[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