[java-oidc-common] branch dev/JCOMOIDC-23 updated: Improve dynamic oidc metadata resolver
Phil Smart
philip.smart at jisc.ac.uk
Fri Oct 1 13:48:47 UTC 2021
This is an automated email from the git hooks/post-receive script.
philsmart pushed a commit to branch dev/JCOMOIDC-23
in repository java-oidc-common.
View the commit online:
http://git.shibboleth.net/view/?p=java-oidc-common.git;a=commit;h=2d4a34df5e23dcaf68df620c4ad54cf0b074c8ab
The following commit(s) were added to refs/heads/dev/JCOMOIDC-23 by this push:
new 2d4a34d Improve dynamic oidc metadata resolver
2d4a34d is described below
commit 2d4a34df5e23dcaf68df620c4ad54cf0b074c8ab
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Fri Oct 1 14:48:44 2021 +0100
Improve dynamic oidc metadata resolver
- Metadata filters can be added as a strategy to filter immediatly
fetched metadata.
- Metadata can be filtered based on predicates in the criteriaset using
a type safe evaluablecriterion.
- Javadoc and cleanup
---
.../src/main/assembly/plugin-assembly-tgz.xml | 2 +-
.../src/main/assembly/plugin-assembly-zip.xml | 2 +-
.../AbstractEvaluableMetadataCriterion.java | 60 ++
.../net/shibboleth/oidc/metadata/BackingStore.java | 58 +-
.../oidc/metadata/DynamicBackingStore.java | 21 -
.../oidc/metadata/DynamicOIDCMetadataResolver.java | 32 +
.../DynamicOIDCProviderMetadataResolver.java | 12 -
.../oidc/metadata/EvaluableMetadataCriterion.java | 34 +
.../oidc/metadata/MetadataManagementData.java | 55 +-
.../oidc/metadata/OIDCMetadataResolver.java | 8 +-
.../oidc/metadata/cache/MetadataCache.java | 39 ++
.../metadata/cache/MetadataCacheException.java | 48 ++
.../oidc/metadata/cache/package-info.java | 22 +
.../oidc/metadata/filter/MetadataFilter.java | 5 +-
.../oidc/metadata/filter/package-info.java | 22 +
.../metadata/cache/impl/DefaultMetadataCache.java | 736 +++++++++++++++++++++
...oviderMetadataCriteriaToIdentifierStrategy.java | 42 ++
...OIDCProviderMetadataExpirationTimeStrategy.java | 45 ++
...oviderMetadataIdentifierExtractionStrategy.java | 35 +
.../metadata/cache/impl/MetadataCacheBuilder.java | 341 ++++++++++
.../impl/OIDCProviderMetadataCacheFactoryBean.java | 43 ++
.../oidc/metadata/cache/impl/package-info.java | 22 +
...va => AbstractDynamicHTTPFetchingStrategy.java} | 219 +++---
.../impl/AbstractDynamicOIDCMetadataResolver.java | 132 ++++
...bstractDynamicOIDCProviderMetadataResolver.java | 592 -----------------
.../impl/AbstractFileOIDCEntityResolver.java | 17 +
.../impl/AbstractOIDCMetadataResolver.java | 248 ++++---
.../oidc/metadata/impl/DefaultBackingStore.java | 78 ++-
.../metadata/impl/DefaultDynamicBackingStore.java | 48 --
.../impl/DynamicOIDCProviderMetadataResolver.java | 43 ++
...HTTPProviderConfigurationFetchingStrategy.java} | 134 +---
.../impl/ReloadingProviderMetadataProvider.java | 4 +-
.../cache/impl/DefaultMetadataCacheTest.java | 386 +++++++++++
.../ManuallyTriggeredScheduledExecutorService.java | 336 ++++++++++
.../oidc/metadata/cache/impl/ScheduledTask.java | 123 ++++
...> DynamicOIDCProviderMetadataResolverTest.java} | 138 +++-
.../src/test/resources/logback-test.xml | 2 +-
37 files changed, 3086 insertions(+), 1098 deletions(-)
diff --git a/oidc-common-dist/src/main/assembly/plugin-assembly-tgz.xml b/oidc-common-dist/src/main/assembly/plugin-assembly-tgz.xml
index fcef674..47cbe9a 100644
--- a/oidc-common-dist/src/main/assembly/plugin-assembly-tgz.xml
+++ b/oidc-common-dist/src/main/assembly/plugin-assembly-tgz.xml
@@ -1,5 +1,5 @@
<assembly>
- <id>assembly</id>
+ <id>assembly-tar</id>
<formats>
<format>tar.gz</format>
</formats>
diff --git a/oidc-common-dist/src/main/assembly/plugin-assembly-zip.xml b/oidc-common-dist/src/main/assembly/plugin-assembly-zip.xml
index dc27f6e..9853d48 100644
--- a/oidc-common-dist/src/main/assembly/plugin-assembly-zip.xml
+++ b/oidc-common-dist/src/main/assembly/plugin-assembly-zip.xml
@@ -1,5 +1,5 @@
<assembly>
- <id>assembly</id>
+ <id>assembly-zip</id>
<formats>
<format>zip</format>
</formats>
diff --git a/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/AbstractEvaluableMetadataCriterion.java b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/AbstractEvaluableMetadataCriterion.java
new file mode 100644
index 0000000..15c06d2
--- /dev/null
+++ b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/AbstractEvaluableMetadataCriterion.java
@@ -0,0 +1,60 @@
+package net.shibboleth.oidc.metadata;
+
+import javax.annotation.Nonnull;
+
+import net.shibboleth.utilities.java.support.logic.Constraint;
+
+/**
+ * Base class for all metadata criterion classes. If the correct object type this criterion accepts is not provided,
+ * {@link #test(Object)} returns ({@code defaultResultOnWrongType}).
+ *
+ * @param <T> The metadata type this criterion accepts.
+ */
+public abstract class AbstractEvaluableMetadataCriterion<T> implements EvaluableMetadataCriterion<T> {
+
+ /** Object type. */
+ @Nonnull private final Class<T> objectType;
+
+ /** What should the default return type be if the wrong type <T> is supplied.*/
+ @Nonnull private final boolean defaultResultOnWrongType;
+
+ /**
+ *
+ * Constructor.
+ *
+ * @param claz the class this criterion accepts.
+ */
+ protected AbstractEvaluableMetadataCriterion(@Nonnull final Class<T> claz,
+ @Nonnull final boolean defaultResult) {
+ objectType = Constraint.isNotNull(claz, "Object type cannot be null");
+ defaultResultOnWrongType = Constraint.isNotNull(defaultResult, "Default result on wrong type can not be null");
+ }
+
+ /**
+ * Get the object type this criterion accepts.
+ *
+ * @return the object type.
+ */
+ @Nonnull public Class<T> getType() {
+ return objectType;
+ }
+
+
+ @Override
+ public boolean test(T t) {
+ if (objectType.isInstance(t)) {
+ return doTest(t);
+ }
+ return defaultResultOnWrongType;
+ }
+
+ /**
+ * Evaluates the predicate on the given argument. Concrete implementations should override this method.
+ *
+ * @param metadata the metadata.
+ *
+ * @return true if the predicate is true, false otherwise.
+ */
+ public abstract boolean doTest(final T metadata);
+
+}
\ No newline at end of file
diff --git a/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/BackingStore.java b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/BackingStore.java
index b68b68c..bd21547 100644
--- a/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/BackingStore.java
+++ b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/BackingStore.java
@@ -1,11 +1,35 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements. See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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.oidc.metadata;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import javax.annotation.Nonnull;
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
+import net.shibboleth.utilities.java.support.annotation.constraint.NotLive;
+import net.shibboleth.utilities.java.support.annotation.constraint.Unmodifiable;
+
/**
+ * A backing store that holds cached objects.
*
* @param <I> the identifier type
* @param <T> the type of object stored, referenced by the key.
@@ -17,14 +41,44 @@ public interface BackingStore<I, T> {
*
* @return the index.
*/
- @Nonnull public Map<I, List<T>> getIndexedDescriptors();
+ @Nonnull Map<I, List<T>> getIndexedValues();
/**
* Get the list of ordered values.
*
* @return the list of ordered values.
*/
- @Nonnull public List<T> getOrderedDescriptors();
+ @Nonnull List<T> getOrderedValues();
+
+ /**
+ * Get the management data for the specified identifier. If the management data does not exist
+ * it should be created.
+ *
+ * <p>Management data facilitates per-entity metadata locking and cache primitives e.g. next refresh time. </p>
+ *
+ * <p>Should do so in a thread-safe way e.g. if two threads enter this method, only one should be allowed
+ * to create management data for the same identifier.</p>
+ *
+ * @param identifier the identifier of the entity to find management data aboout
+ *
+ * @return the corresponding management data
+ */
+ @Nonnull public MetadataManagementData<I> computeManagementDataIfAbsent(@Nonnull final I identifier);
+
+ /**
+ * Remove the management data for the specified entityID.
+ *
+ * @param identifier the input identifier
+ */
+ void removeManagementData(@Nonnull final I identifier);
+
+ /**
+ * Get the set of entityIDs which currently have management data.
+ *
+ * @return set of entityIDs, may be empty
+ */
+ @Nonnull @NonnullElements @Unmodifiable @NotLive
+ Set<I> getManagementDataIdentifiers();
diff --git a/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/DynamicBackingStore.java b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/DynamicBackingStore.java
deleted file mode 100644
index c4d4403..0000000
--- a/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/DynamicBackingStore.java
+++ /dev/null
@@ -1,21 +0,0 @@
-package net.shibboleth.oidc.metadata;
-
-import javax.annotation.Nonnull;
-
-/**
- * Specialized metadata backing store implementation for dynamic metadata resolvers.
- */
-public interface DynamicBackingStore<Identifier, Type> extends BackingStore<Identifier, Type> {
-
- /**
- * Get the management data for the specified identifier. If the management data does not exist
- * it should be created.
- *
- * <p> Should do so in a thread-safe way.</p>
- *
- * @param identifier the identifier of the entity to find management data aboout
- * @return the corresponding management data
- */
- @Nonnull public MetadataManagementData<Identifier> computeManagementDataIfAbsent(@Nonnull final Identifier identifier);
-
-}
diff --git a/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/DynamicOIDCMetadataResolver.java b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/DynamicOIDCMetadataResolver.java
new file mode 100644
index 0000000..26ca82c
--- /dev/null
+++ b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/DynamicOIDCMetadataResolver.java
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements. See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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.oidc.metadata;
+
+/**
+ * Marker interface for {@link OIDCMetadataResolver} implementations which resolve
+ * metadata by dynamically querying for the requested data individually at the time of the
+ * resolution operation, for example by invoking a request to the well-known provider configuration
+ * endpoint. Implementations may cache the results of previous resolutions so that
+ * subsequent queries may be answered locally.
+ *
+ * @param <T> the metadata type
+ */
+public interface DynamicOIDCMetadataResolver<T> extends OIDCMetadataResolver<T> {
+
+}
diff --git a/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/DynamicOIDCProviderMetadataResolver.java b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/DynamicOIDCProviderMetadataResolver.java
deleted file mode 100644
index 9277068..0000000
--- a/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/DynamicOIDCProviderMetadataResolver.java
+++ /dev/null
@@ -1,12 +0,0 @@
-package net.shibboleth.oidc.metadata;
-
-/**
- * Marker interface for {@link ProviderMetadataResolver} implementations which resolve
- * metadata by dynamically querying for the requested data individually at the time of the
- * resolution operation, for example by invoking a request to the well-known provider configuration
- * endpoint. Implementations may cache the results of previous resolutions so that
- * subsequent queries may be answered locally.
- */
-public interface DynamicOIDCProviderMetadataResolver<ProductType, CriteriaType> extends OIDCMetadataResolver<ProductType, CriteriaType> {
-
-}
diff --git a/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/EvaluableMetadataCriterion.java b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/EvaluableMetadataCriterion.java
new file mode 100644
index 0000000..2a0b963
--- /dev/null
+++ b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/EvaluableMetadataCriterion.java
@@ -0,0 +1,34 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements. See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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.oidc.metadata;
+
+import java.util.function.Predicate;
+
+import net.shibboleth.utilities.java.support.resolver.Criterion;
+
+/**
+ * Marker interface for evaluable metadata criteria.
+ *
+ * @param <T> The metadata type this criterion applies to.
+ */
+public interface EvaluableMetadataCriterion<T> extends Predicate<T>, Criterion {
+
+}
+
+
diff --git a/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/MetadataManagementData.java b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/MetadataManagementData.java
index 0a1fe72..6a94131 100644
--- a/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/MetadataManagementData.java
+++ b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/MetadataManagementData.java
@@ -1,24 +1,45 @@
package net.shibboleth.oidc.metadata;
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements. See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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.
+ */
import java.time.Duration;
import java.time.Instant;
-import java.util.concurrent.locks.ReadWriteLock;
-import java.util.concurrent.locks.ReentrantReadWriteLock;
+import java.util.concurrent.locks.StampedLock;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import net.shibboleth.utilities.java.support.logic.Constraint;
-/** Class that holds management data about an entities metadata.*/
+/**
+ * Class that holds management data about an entities metadata.
+ *
+ * @param <MetadataIdentifier> the key to the stored object.
+ *
+ */
//TODO removed negative lookup cache.
+//TODO change to a generic ObjectManagmentData type - if we want to broaden beyond metadata.
public class MetadataManagementData<MetadataIdentifier> {
/** The identifier of the entity managed by this instance. */
- private final MetadataIdentifier id;
+ @Nonnull private final MetadataIdentifier id;
/** Last update time of the associated metadata. */
- private Instant lastUpdateTime;
+ @Nullable private Instant lastUpdateTime;
/** Expiration time of the associated metadata. */
private Instant expirationTime;
@@ -29,21 +50,19 @@ public class MetadataManagementData<MetadataIdentifier> {
/** The last time at which the entity's backing store data was accessed. */
private Instant lastAccessedTime;
- /** Read-write lock instance which governs access to the entity's backing store data. */
- private ReadWriteLock readWriteLock;
+ /** Read-write stamped lock which governs access to the metadata's backing store data. */
+ private final StampedLock stmpLock;
/** Constructor.
*
* @param identifier the entity ID managed by this instance
* @param maxCacheDuration the maximum cache duration for metadata
*/
- public MetadataManagementData(@Nonnull final MetadataIdentifier identifier, @Nonnull final Duration maxCacheDuration) {
+ public MetadataManagementData(@Nonnull final MetadataIdentifier identifier) {
id = Constraint.isNotNull(identifier, "ID was null");
- final Instant now = Instant.now();
- expirationTime = now.plus(maxCacheDuration);
- refreshTriggerTime = now.plus(maxCacheDuration);
+ final Instant now = Instant.now();
lastAccessedTime = now;
- readWriteLock = new ReentrantReadWriteLock(true);
+ stmpLock = new StampedLock();
}
/**
@@ -78,7 +97,7 @@ public class MetadataManagementData<MetadataIdentifier> {
*
* @return the expiration time
*/
- @Nonnull public Instant getExpirationTime() {
+ @Nullable public Instant getExpirationTime() {
return expirationTime;
}
@@ -96,7 +115,7 @@ public class MetadataManagementData<MetadataIdentifier> {
*
* @return the refresh trigger time
*/
- @Nonnull public Instant getRefreshTriggerTime() {
+ @Nullable public Instant getRefreshTriggerTime() {
return refreshTriggerTime;
}
@@ -114,7 +133,7 @@ public class MetadataManagementData<MetadataIdentifier> {
*
* @return last access time
*/
- @Nonnull public Instant getLastAccessedTime() {
+ @Nullable public Instant getLastAccessedTime() {
return lastAccessedTime;
}
@@ -126,12 +145,12 @@ public class MetadataManagementData<MetadataIdentifier> {
}
/**
- * Get the read-write lock instance which governs access to the entity's backing store data.
+ * Get the read-write lock instance which governs access to the metadata's backing store data.
*
* @return the lock instance
*/
- @Nonnull public ReadWriteLock getReadWriteLock() {
- return readWriteLock;
+ @Nonnull public StampedLock getStampLock() {
+ return stmpLock;
}
}
diff --git a/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/OIDCMetadataResolver.java b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/OIDCMetadataResolver.java
index b0e40e4..48ac035 100644
--- a/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/OIDCMetadataResolver.java
+++ b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/OIDCMetadataResolver.java
@@ -1,6 +1,7 @@
package net.shibboleth.oidc.metadata;
import net.shibboleth.utilities.java.support.component.IdentifiedComponent;
+import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
import net.shibboleth.utilities.java.support.resolver.Resolver;
@@ -8,11 +9,8 @@ import net.shibboleth.utilities.java.support.resolver.Resolver;
* Generic interface for OIDC Metadata resolvers which process specified criteria and produce some implementation-specific
* result information.
*
- * @param <MetadataType> the type of objects produced by this metadata resolver
- * @param <CriteriaType> the type of criteria to process during resolution
+ * @param <T> the type of objects produced by this metadata resolver
*/
-// TODO do we need a specific resolver interface with only generic types? why not straight to Resolver.
-public interface OIDCMetadataResolver<MetadataType, CriteriaType>
- extends Resolver<MetadataType, CriteriaType>, IdentifiedComponent {
+public interface OIDCMetadataResolver<T> extends Resolver<T, CriteriaSet>, IdentifiedComponent {
}
diff --git a/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/cache/MetadataCache.java b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/cache/MetadataCache.java
new file mode 100644
index 0000000..3918eb0
--- /dev/null
+++ b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/cache/MetadataCache.java
@@ -0,0 +1,39 @@
+package net.shibboleth.oidc.metadata.cache;
+
+import java.util.List;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
+import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
+import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
+
+/**
+ * A metadata cache.
+ *
+ * TODO: finish
+ *
+ * @param <T> The metadata identifier type.
+ * @param <U> The metadata type.
+ */
+public interface MetadataCache<U> {
+
+ /**
+ * Get the list of metadata matching the given identifier. If not found, obtain it from the given fetching
+ * function.
+ *
+ * <p>The implementing method is required to be thread safe. The fetching function must be called within an
+ * appropriate lock.</p>
+ *
+ * @param criteria the criteria to use when getting the metadata from the cache or the source.
+ * @param fetchFunction a function to call to obtain the metadata if it does not exist in the cache.
+ *
+ * @return a list of metadata matching the criteria.
+ *
+ * @throws MetadataCacheException on error fetching metadata.
+ */
+ @Nonnull @NonnullElements public List<U> getOrFetchIfAbsent(@Nonnull @NotEmpty final CriteriaSet criteria,
+ @Nonnull final Function<CriteriaSet, U> fetchFunction) throws MetadataCacheException;
+
+}
diff --git a/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/cache/MetadataCacheException.java b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/cache/MetadataCacheException.java
new file mode 100644
index 0000000..83a2958
--- /dev/null
+++ b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/cache/MetadataCacheException.java
@@ -0,0 +1,48 @@
+package net.shibboleth.oidc.metadata.cache;
+
+import javax.annotation.Nullable;
+
+/**
+ * Base exception for metadata cache related errors.
+ */
+public class MetadataCacheException extends Exception {
+
+ /** SerialUID */
+ private static final long serialVersionUID = 4749191928630000314L;
+
+ /**
+ * Constructor.
+ */
+ public MetadataCacheException() {
+ super();
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param message exception message
+ */
+ public MetadataCacheException(@Nullable final String message) {
+ super(message);
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param wrappedException exception to be wrapped by this one
+ */
+ public MetadataCacheException(@Nullable final Exception wrappedException) {
+ super(wrappedException);
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param message exception message
+ * @param wrappedException exception to be wrapped by this one
+ */
+ public MetadataCacheException(@Nullable final String message, @Nullable final Exception wrappedException) {
+ super(message, wrappedException);
+ }
+
+}
diff --git a/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/cache/package-info.java b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/cache/package-info.java
new file mode 100644
index 0000000..278486d
--- /dev/null
+++ b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/cache/package-info.java
@@ -0,0 +1,22 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements. See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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.
+ */
+
+/**
+ *
+ * Metadata cache API.
+ */
+package net.shibboleth.oidc.metadata.cache;
\ No newline at end of file
diff --git a/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/filter/MetadataFilter.java b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/filter/MetadataFilter.java
index 74706d3..accb3dc 100644
--- a/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/filter/MetadataFilter.java
+++ b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/filter/MetadataFilter.java
@@ -15,13 +15,14 @@ import org.opensaml.core.xml.XMLObject;
* digital signature.
* </p>
* <p>
- * Some example OIDC filters might throw a filter exception or return null if the metadata no longer supports the ES-algorithms etc.
+ * Some example OIDC filters might throw a filter exception or return null if the metadata no longer supports, for example,
+ * the ES-algorithms.
* </p>
*
* <p>
* If a filter wishes to completely remove the metadata, or otherwise indicate that it
* has successfully produced an empty data set from the input metadata, <code>null</code> may be returned
- * by the filter's {@link #filter(XMLObject, MetadataFilterContext)} method.
+ * by the filter's {@link #filter(Object, MetadataFilterContext)} method.
* </p>
*
* @param <T> The metadata type.
diff --git a/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/filter/package-info.java b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/filter/package-info.java
new file mode 100644
index 0000000..e16d054
--- /dev/null
+++ b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/filter/package-info.java
@@ -0,0 +1,22 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements. See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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.
+ */
+
+/**
+ *
+ * Metadata cache filter API.
+ */
+package net.shibboleth.oidc.metadata.filter;
\ No newline at end of file
diff --git a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/DefaultMetadataCache.java b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/DefaultMetadataCache.java
new file mode 100644
index 0000000..37e3d22
--- /dev/null
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/DefaultMetadataCache.java
@@ -0,0 +1,736 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements. See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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.oidc.metadata.cache.impl;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.StampedLock;
+import java.util.function.BiConsumer;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.ThreadSafe;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
+
+import net.shibboleth.oidc.metadata.BackingStore;
+import net.shibboleth.oidc.metadata.MetadataManagementData;
+import net.shibboleth.oidc.metadata.cache.MetadataCache;
+import net.shibboleth.oidc.metadata.cache.MetadataCacheException;
+import net.shibboleth.oidc.metadata.filter.MetadataFilterContext;
+import net.shibboleth.oidc.metadata.filter.MetadataSource;
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
+import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
+import net.shibboleth.utilities.java.support.annotation.constraint.Positive;
+import net.shibboleth.utilities.java.support.component.AbstractIdentifiableInitializableComponent;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+import net.shibboleth.utilities.java.support.component.ComponentSupport;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+import net.shibboleth.utilities.java.support.primitive.TimerSupport;
+import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
+
+
+/**
+ *
+ * The executor service is shutdown when the {@link #doDestroy()} method is called.
+ *
+ * @param <IdentifierType> the metadata identifier type
+ * @param <MetadataType> the metadata type
+ */
+//TODO add getId() to log messages?
+ at ThreadSafe
+public final class DefaultMetadataCache<IdentifierType, MetadataType>
+ extends AbstractIdentifiableInitializableComponent implements MetadataCache<MetadataType> {
+
+ /** Class logger. */
+ private final Logger log = LoggerFactory.getLogger(DefaultMetadataCache.class);
+
+ /** Maximum cache duration. */
+ @NonnullAfterInit private Duration maxCacheDuration;
+
+ /** Minimum cache duration. */
+ @NonnullAfterInit private Duration minCacheDuration;
+
+ /** Factor used to compute when the next refresh interval will occur. Default value: 0.75 */
+ @NonnullAfterInit @Positive private Float refreshDelayFactor;
+
+ /** The maximum idle time for which the resolver will keep data for a given entityID,
+ * before it is removed. */
+ @NonnullAfterInit private Duration maxIdleEntityData;
+
+ /** Flag indicating whether idle entity data should be removed. */
+ private boolean removeIdleEntityData;
+
+ /** The interval at which the cleanup task should run. */
+ @NonnullAfterInit private Duration cleanupTaskInterval;
+
+ /** The initial cleanup task delay.*/
+ @NonnullAfterInit private Duration initialCleanupTaskDelay;
+
+ /** Backing store for runtime metadata.*/
+ @Nonnull private final BackingStore<IdentifierType, MetadataType> backingStore;
+
+ /**
+ * A hook that is executed just before a cache entry will been removed/invalidated/evicted.
+ * The metadata list could be null, the identifier is never null.
+ */
+ @Nullable private BiConsumer<List<MetadataType>, IdentifierType> metadataBeforeRemovalHook;
+
+ //TODO move this into the method for get or fetch? Otherwise why not add the fetch strategy to here also?
+ /** Strategy used to extract an identifier from the given metadata.*/
+ @NonnullAfterInit private Function<MetadataType, IdentifierType> identifierExtractionStrategy;
+
+ /** Strategy used to compute an expiration time. */
+ @NonnullAfterInit private BiFunction<MetadataType, Instant, Instant> metadataExpirationTimeStrategy;
+
+ /** Map criteria to identifiers to use as keys to the backing store.*/
+ @NonnullAfterInit private Function<CriteriaSet, IdentifierType> criteriaToIdentifierStrategy;
+
+ /** A strategy to filter metadata. */
+ @NonnullAfterInit private BiFunction<MetadataType, MetadataFilterContext , MetadataType> metadataFilterStrategy;
+
+ /** A single threaded executor service for running background cache tasks.*/
+ @NonnullAfterInit private ScheduledExecutorService executorService;
+
+ /** Whether we created our own schedular during object construction. */
+ private boolean createOwnSchedular;
+
+ /** Package private constructor. For safe-construction through the factory only.*/
+ DefaultMetadataCache(@Nonnull final BackingStore<IdentifierType, MetadataType> store) {
+ this(store, null);
+ }
+
+
+ /**
+ *
+ * Package private constructor. For safe-construction through the factory only.
+ *
+ * <p>Accepts an executor. Mostly used for testing.</p>
+ *
+ * @param executor
+ */
+ DefaultMetadataCache(@Nonnull final BackingStore<IdentifierType, MetadataType> store,
+ @Nullable final ScheduledExecutorService executor) {
+ backingStore = Constraint.isNotNull(store, "A backingstore must be set");
+ if (executor != null) {
+ executorService = executor;
+ createOwnSchedular = false;
+ } else {
+ createOwnSchedular = true;
+ }
+
+ }
+
+
+ @Override
+ protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+
+
+ if (identifierExtractionStrategy == null) {
+ throw new ComponentInitializationException("Identifier extraction strategy can not be null");
+ }
+ if (metadataExpirationTimeStrategy == null) {
+ throw new ComponentInitializationException("Metadata expiration strategy can not be null");
+ }
+ if (criteriaToIdentifierStrategy == null) {
+ throw new ComponentInitializationException("Criteria to identifier strategy can not be null");
+ }
+ if (metadataFilterStrategy == null) {
+ throw new ComponentInitializationException("Metadata filter strategy can not be null");
+ }
+ if (maxCacheDuration == null || minCacheDuration == null || refreshDelayFactor == null ||
+ maxIdleEntityData == null || cleanupTaskInterval == null || initialCleanupTaskDelay == null ||
+ backingStore == null) {
+ throw new ComponentInitializationException("Metadata cache not property initialized");
+ }
+
+ if (createOwnSchedular) {
+ // Use thread builder to allow setting threads as deamon and a name.
+ // Set as deamon background threads. Do not prevent JVM exit.
+ executorService = Executors.newSingleThreadScheduledExecutor(
+ new ThreadFactoryBuilder().setDaemon(true).setNameFormat(TimerSupport.getTimerName(this)+"-%d").build());
+ }
+
+ executorService.scheduleAtFixedRate(
+ errorHandlingWrapper(new ExpiredAndIdleMetadataCleanupTask()), initialCleanupTaskDelay.toMillis(),
+ cleanupTaskInterval.toMillis(), TimeUnit.MILLISECONDS);
+
+ }
+
+ @Override protected void doDestroy() {
+ executorService.shutdown();
+ super.doDestroy();
+ }
+
+ /**
+ * Set the initial cleanup task delay.
+ *
+ * @param delay The initialCleanupTaskDelay to set.
+ */
+ public void setInitialCleanupTaskDelay(@Nonnull final Duration delay) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+ ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+
+ Constraint.isNotNull(delay, "Cleanup task delay can not be null");
+ Constraint.isFalse(delay.isNegative() || delay.isZero(), "Cleanup task delay must be positive");
+ initialCleanupTaskDelay = delay;
+
+ }
+
+ /**
+ * Set the interval at which the cleanup task should run.
+ *
+ * <p>Defaults to: 30 minutes.</p>
+ *
+ * @param interval the interval to set
+ */
+ public void setCleanupTaskInterval(@Nonnull final Duration interval) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+ ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+
+ Constraint.isNotNull(interval, "Cleanup task interval may not be null");
+ Constraint.isFalse(interval.isNegative() || interval.isZero(), "Cleanup task interval must be positive");
+
+ cleanupTaskInterval = interval;
+ }
+
+ /**
+ * Set the {@link CriteriaSet} to IdentifierType strategy.
+ *
+ * @param strategy the strategy.
+ */
+ public void setCriteriaToIdentifierStrategy(@Nonnull final Function<CriteriaSet, IdentifierType> strategy) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+ ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+
+ criteriaToIdentifierStrategy = Constraint.isNotNull(strategy,"Criteria to identifier strategy can not be null");
+ }
+
+ /**
+ * Set the MetadataType to IdentifierType extraction time strategy.
+ *
+ * @param strategy the strategy.
+ */
+ public void setIdentifierExtractionStrategy(@Nonnull final Function<MetadataType, IdentifierType> strategy) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+ ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+
+ identifierExtractionStrategy = Constraint.isNotNull(strategy, "Identifier extraction strategy can not be null");
+ }
+
+ /**
+ * Set the metadata expiration time strategy.
+ *
+ * @param strategy the strategy.
+ */
+ public void setMetadataExpirationTimeStrategy(
+ @Nonnull final BiFunction<MetadataType, Instant, Instant> strategy) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+ ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+
+ metadataExpirationTimeStrategy = Constraint.isNotNull(strategy, "Metadata expiration strategy can not be null");
+ }
+
+ /**
+ * Set the metadata filtering strategy.
+ *
+ * @param strategy the metadata filtering strategy.
+ */
+ public void setMetadataFilterStrategy(
+ @Nonnull final BiFunction<MetadataType, MetadataFilterContext, MetadataType> strategy) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+ ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+
+ metadataFilterStrategy = Constraint.isNotNull(strategy, "Metadata filter strategy can not be null");;
+ }
+
+ /**
+ * Set the flag indicating whether idle entity data should be removed.
+ *
+ * @param flag true if idle entity data should be removed, false otherwise
+ */
+ public void setRemoveIdleEntityData(final boolean flag) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+ ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+ removeIdleEntityData = flag;
+ }
+
+ /**
+ * Set the maximum idle time for which the resolver will keep data for a given entityID,
+ * before it is removed.
+ *
+ * <p>Defaults to: 8 hours.</p>
+ *
+ * @param max the maximum entity data idle time
+ */
+ protected void setMaxIdleEntityData(@Nonnull final Duration max) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+ ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+
+ Constraint.isNotNull(max, "Max idle time cannot be null");
+ Constraint.isFalse(max.isNegative(), "Max idle time cannot be negative");
+
+ maxIdleEntityData = max;
+ }
+
+
+ /**
+ * Set the maximum cache duration for metadata.
+ *
+ * <p>Defaults to: 8 hours.</p>
+ *
+ * @param duration the maximum cache duration
+ */
+ public void setMaxCacheDuration(@Nonnull final Duration duration) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+ ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+
+ Constraint.isNotNull(duration, "Duration cannot be null");
+ Constraint.isFalse(duration.isNegative(), "Duration cannot be negative");
+
+ maxCacheDuration = duration;
+ }
+
+
+ /**
+ * Set the minimum cache duration for metadata.
+ *
+ * <p>Defaults to: 10 minutes.</p>
+ *
+ * @param duration the minimum cache duration
+ */
+ public void setMinCacheDuration(@Nonnull final Duration duration) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+ ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+
+ Constraint.isNotNull(duration, "Duration cannot be null");
+ Constraint.isFalse(duration.isNegative(), "Duration cannot be negative");
+
+ minCacheDuration = duration;
+ }
+
+ /**
+ * Set a hook to run before a metadata cache entry is removed from the cache.
+ * <p>The hook is required to gracefully handle null metadata lists.</p>
+ *
+ * @param hook the hook to run.
+ */
+ public void setMetadataBeforeRemovalHook(
+ @Nullable final BiConsumer<List<MetadataType>, IdentifierType> hook) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+ ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+
+ metadataBeforeRemovalHook = hook;
+ }
+
+
+ /**
+ * Sets the delay factor used to compute the next refresh time. The delay must be between 0.0 and 1.0, exclusive.
+ *
+ * <p>Defaults to: 0.75.</p>
+ *
+ * @param factor delay factor used to compute the next refresh time
+ */
+ public void setRefreshDelayFactor(@Nonnull final Float factor) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+ ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+
+ if (factor <= 0 || factor >= 1) {
+ throw new IllegalArgumentException("Refresh delay factor must be a number between 0.0 and 1.0, exclusive");
+ }
+
+ refreshDelayFactor = factor;
+ }
+
+ /**
+ * Lookup the specified identifier from the index. The returned list will be a copy of what is stored in the backing
+ * index, and so the list is safe to be manipulated by callers.
+ *
+ * @param identifier the entityID to lookup
+ *
+ * @return list copy of indexed metadata, may be empty, will never be null
+ */
+ @Nonnull @NonnullElements private List<MetadataType> lookupIndexedIdentifier(
+ @Nonnull @NotEmpty final IdentifierType identifier) {
+ final List<MetadataType> metadata = backingStore.getIndexedValues().get(identifier);
+ if (metadata != null) {
+ return new ArrayList<>(metadata);
+ }
+ return Collections.emptyList();
+ }
+
+ /**
+ * Get the backing store. Mainly used for accessing the store for tests.
+ *
+ * @return the backing store.
+ */
+ @Nonnull public BackingStore<IdentifierType, MetadataType> getBackingStore() {
+ return backingStore;
+ }
+
+ @Override
+ @Nonnull @NonnullElements public List<MetadataType> getOrFetchIfAbsent(
+ @Nonnull @NotEmpty final CriteriaSet criteria,
+ @Nonnull final Function<CriteriaSet, MetadataType> fetchFunction) throws MetadataCacheException {
+
+ if (!isInitialized()) {
+ throw new MetadataCacheException("Metadata cache has not been initialized");
+ }
+
+ final IdentifierType identifier = criteriaToIdentifierStrategy.apply(criteria);
+ log.debug("Resolved criteria to identifier: {}", identifier);
+
+ if (identifier != null) {
+ final MetadataManagementData<IdentifierType> mgmtData = backingStore
+ .computeManagementDataIfAbsent(identifier);
+
+ // check metadata refresh is not needed before reading.
+ List<MetadataType> allMetadata = Collections.emptyList();
+ if (!shouldAttemptRefresh(mgmtData)) {
+ allMetadata = read(mgmtData, identifier);
+ }
+ if (allMetadata.isEmpty()) {
+ log.debug("Metadata for '{}' does not exist or is stale, attempting to fetch it", identifier);
+ fetch(mgmtData, identifier, fetchFunction, criteria);
+ return read(mgmtData, identifier);
+ } else {
+ log.debug("Metadata for '{}' found in cache", identifier);
+ return allMetadata;
+ }
+ } else {
+ //TODO: see SAML version, could resolve from criteria even if no identifier.
+ log.debug("Identifier not resolvable from criteria, can not fetch metadata");
+ return Collections.emptyList();
+ }
+
+ }
+
+
+ /**
+ * Fetch metadata using the supplied fetch function, filter the metadata, then save it to the
+ * backing store - updating the management data at the same time.
+ *
+ * @param mgmtData the metadata management data.
+ * @param identifier the identifier of the metadata to fetch.
+ * @param fetchFunction the function used to fetch the metadata.
+ * @param criteria the criteria used in determining how to fetch the metadata.
+ */
+ private void fetch(@Nonnull final MetadataManagementData<IdentifierType> mgmtData,
+ @Nonnull final IdentifierType identifier,
+ @Nonnull final Function<CriteriaSet, MetadataType> fetchFunction,
+ @Nonnull @NotEmpty final CriteriaSet criteria){
+
+ final StampedLock sl = mgmtData.getStampLock();
+ long stamp = sl.writeLock();
+
+ try {
+ if (!shouldAttemptRefresh(mgmtData)){
+ // re-check another thread has not acquired this lock before hand - and therefore obtained
+ // the metadata.
+ List<MetadataType> allMetadata = lookupIndexedIdentifier(identifier);
+ if (!allMetadata.isEmpty()) {
+ log.debug("Metadata for '{}' was acquired while waiting for the write lock", identifier);
+ return;
+ }
+ } else {
+ log.debug("Metadata for '{}' is stale and requires refreshing", identifier);
+ }
+
+ final MetadataType resolvedMetadata = fetchFunction.apply(criteria);
+
+ if (resolvedMetadata != null) {
+
+ final MetadataType filteredMetadata = metadataFilterStrategy.apply(resolvedMetadata, newFilterContext());
+
+ final IdentifierType extractedIdentifier = identifierExtractionStrategy.apply(filteredMetadata);
+ // equality method of the identifier is required to be implemented correctly.
+ if (!Objects.equals(identifier, extractedIdentifier)) {
+ log.warn("New metadata's identifer '{}' does not match expected identifier '{}', will not process",
+ extractedIdentifier, identifier);
+ return;
+ }
+
+ log.debug("Resolved metadata dynamically with identifier '{}'",extractedIdentifier);
+ invalidate(extractedIdentifier);
+
+ backingStore.getOrderedValues().add(filteredMetadata);
+ // add new metadata to index
+ List<MetadataType> existingMetadata = backingStore.getIndexedValues().get(identifier);
+ if (existingMetadata == null) {
+ existingMetadata = new ArrayList<>();
+ backingStore.getIndexedValues().put(identifier, existingMetadata);
+ } else if (!existingMetadata.isEmpty()) {
+ log.warn("Detected duplicate metadata for identifier: {}", identifier);
+ }
+ existingMetadata.add(filteredMetadata);
+
+ final Instant now = Instant.now();
+ log.debug("For metadata '{}' expiration and refresh computation, 'now' is : {}", identifier, now);
+
+ mgmtData.setLastUpdateTime(now);
+
+ mgmtData.setExpirationTime(metadataExpirationTimeStrategy.apply(filteredMetadata, now));
+ log.debug("Computed metadata '{}' expiration time: {}", identifier, mgmtData.getExpirationTime());
+
+ mgmtData.setRefreshTriggerTime(computeRefreshTriggerTime(mgmtData.getExpirationTime(), now));
+ log.debug("Computed metadata '{}' refresh trigger time: {}", identifier, mgmtData.getRefreshTriggerTime());
+
+ log.info("Successfully loaded new Metadata with identifer '{}'", identifier);
+
+ } else {
+ log.warn("Metadata for '{}' could not be resolved from source", identifier);
+ }
+
+ } finally {
+ sl.unlock(stamp);
+ }
+ }
+
+ /**
+ * Read metadata from the cache under the lock relating to the Identifier of the metadata to find.
+ *
+ * <p>The first read attempt is optimistic and occurs without acquiring a read lock. The optimistic
+ * read is validated to ensure another thread has not acquired a write lock in the meantime. If it has,
+ * a write lock is obtained and a further read is attempted - to ensure a consistent state. This
+ * should improve efficiency given that metadata reads will vastly out number metadata fetch/writes.</p>
+ *
+ * @param mgmtData the metadata managment data.
+ * @param identifier the metadata identifier to use as a key to fetch.
+ *
+ * @return a list of metadata that matches the given key, an empty list of none are found.
+ */
+ @Nonnull private List<MetadataType> read(@Nonnull final MetadataManagementData<IdentifierType> mgmtData,
+ @Nonnull final IdentifierType identifier){
+
+ // record access attempt.
+ mgmtData.recordEntityAccess();
+ // get optimistic lock
+ final StampedLock sl = mgmtData.getStampLock();
+ long stamp = sl.tryOptimisticRead();
+
+ // A single optimistic read. A write lock will break this, but we check for
+ // that using the validate method e.g. we keep track of locks, but we do not block a write lock
+ // here. Most metadata operations will be read-only. Reads are not expensive, but writes (lookups) are.
+ final List<MetadataType> allMetadata = lookupIndexedIdentifier(identifier);
+
+ if (sl.validate(stamp)) {
+ return allMetadata;
+ }
+ else {
+ // OK, was tampered with, lets do it with a hard read lock.
+ stamp = sl.readLock();
+ try {
+ return lookupIndexedIdentifier(identifier);
+ } finally {
+ sl.unlock(stamp);
+ }
+ }
+
+ }
+
+
+ /**
+ * Get a new instance of {@link MetadataFilterContext} to be used when filtering metadata.
+ *
+ * <p>
+ * This default implementation just returns an empty context with as metadatasource for passing to
+ * the filter strategy. The strategy can then add to the context as appropriate.
+ * </p>
+ *
+ * @return the new filter context instance
+ */
+ @Nonnull protected MetadataFilterContext newFilterContext() {
+
+ final MetadataSource source = new MetadataSource();
+ source.setSourceId(getId());
+
+ final MetadataFilterContext context = new MetadataFilterContext();
+ context.add(source);
+
+ return context;
+ }
+
+
+ /**
+ * Determine whether should attempt to refresh the metadata, based on stored refresh trigger time.
+ *
+ * @param mgmtData the entity'd management data
+ * @return true if should attempt refresh, false otherwise
+ */
+ private boolean shouldAttemptRefresh(@Nonnull final MetadataManagementData<IdentifierType> mgmtData) {
+ return Instant.now().isAfter(mgmtData.getRefreshTriggerTime());
+
+ }
+
+ /**
+ * Remove/discard from the backing store all metadata for the entity with the given identifier.
+ *
+ * @param identifier the ID of the metadata to remove
+ * @param backingStore the backing store instance to update
+ */
+ private void invalidate(@Nonnull final IdentifierType identifier) {
+ final Map<IdentifierType, List<MetadataType>> indexedDescriptors = backingStore.getIndexedValues();
+ final List<MetadataType> descriptors = indexedDescriptors.get(identifier);
+ if (metadataBeforeRemovalHook != null) {
+ // descriptors is nullable.
+ metadataBeforeRemovalHook.accept(descriptors, identifier);
+ }
+ if (descriptors != null) {
+ backingStore.getOrderedValues().removeAll(descriptors);
+ }
+ indexedDescriptors.remove(identifier);
+ }
+
+ /**
+ * Compute the refresh trigger time.
+ *
+ * @param expirationTime the time at which the metadata effectively expires
+ * @param nowDateTime the current date time instant
+ *
+ * @return the time after which refresh attempt(s) should be made
+ */
+ @Nonnull private Instant computeRefreshTriggerTime(@Nullable final Instant expirationTime,
+ @Nonnull final Instant nowDateTime) {
+
+ final long now = nowDateTime.toEpochMilli();
+
+ long expireInstant = 0;
+ if (expirationTime != null) {
+ expireInstant = expirationTime.toEpochMilli();
+ }
+ long refreshDelay = (long) ((expireInstant - now) * refreshDelayFactor);
+
+ // if the expiration time was null or the calculated refresh delay was less than the floor
+ // use the floor
+ if (refreshDelay < minCacheDuration.toMillis()) {
+ refreshDelay = minCacheDuration.toMillis();
+ }
+
+ return nowDateTime.plusMillis(refreshDelay);
+ }
+
+ /**
+ * Create a wrapper for runnables that catches any throwable and logs it. Useful
+ * to prevent thread death from an uncaught exception.
+ *
+ * @param action the runnable to wrap
+ *
+ * @return the wrapped runnable.
+ */
+ @Nonnull private Runnable errorHandlingWrapper(@Nonnull final Runnable action) {
+ return () -> {
+ try {
+ action.run();
+ } catch (final Throwable e) {
+ log.error("{} Error executing thread task", getId(), e);
+ }
+ };
+ }
+
+ /**
+ * Cleanup task that removes expired and idle metadata from the backing store.
+ */
+ private class ExpiredAndIdleMetadataCleanupTask implements Runnable {
+
+ /** Logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(ExpiredAndIdleMetadataCleanupTask.class);
+
+ @Override
+ public void run() {
+ if (isDestroyed() || !isInitialized()) {
+ // just in case the metadata resolver was destroyed before this task runs,
+ // or if it somehow is being called on a non-successfully-inited cache instance.
+ log.debug("BackingStoreCleanupSweeper will not run because: inited: {}, destroyed: {}",
+ isInitialized(), isDestroyed());
+ return;
+ }
+ log.info("Running metadata cleanup background timer task {}",this);
+ removeExpiredAndIdleMetadata();
+ }
+
+ /**
+ * Purge metadata which is either 1) expired or 2) (if {@link #isRemoveIdleEntityData()} is true)
+ * which hasn't been accessed within the last {@link #getMaxIdleEntityData()} duration.
+ */
+ private void removeExpiredAndIdleMetadata() {
+ final Instant now = Instant.now();
+ final Instant earliestValidLastAccessed = now.minus(maxIdleEntityData);
+
+ final BackingStore<IdentifierType, MetadataType> store = getBackingStore();
+ final Set<IdentifierType> ids = new HashSet<>();
+ ids.addAll(store.getIndexedValues().keySet());
+ ids.addAll(store.getManagementDataIdentifiers());
+
+ for (final IdentifierType identifier : ids) {
+ final MetadataManagementData<IdentifierType> mgmtData = store.computeManagementDataIfAbsent(identifier);
+ final long stamp = mgmtData.getStampLock().writeLock();
+ try {
+ if (isRemoveData(mgmtData, now, earliestValidLastAccessed)) {
+ invalidate(identifier);
+ store.removeManagementData(identifier);
+ }
+ } finally {
+ mgmtData.getStampLock().unlock(stamp);
+ }
+ }
+
+ }
+
+ /**
+ * Determine whether metadata should be removed based on expiration and idle time data.
+ *
+ * @param mgmtData the management data instance for the entity
+ * @param now the current time
+ * @param earliestValidLastAccessed the earliest last accessed time which would be valid
+ *
+ * @return true if the entity is expired or exceeds the max idle time, false otherwise
+ */
+ private boolean isRemoveData(@Nonnull final MetadataManagementData<IdentifierType> mgmtData,
+ @Nonnull final Instant now, @Nonnull final Instant earliestValidLastAccessed) {
+ if (removeIdleEntityData && mgmtData.getLastAccessedTime().isBefore(earliestValidLastAccessed)) {
+ log.debug("Metadata exceeds maximum idle time, removing: {}", mgmtData.getID());
+ return true;
+ } else if (now.isAfter(mgmtData.getExpirationTime())) {
+ log.debug("Entity metadata is expired, removing: {}", mgmtData.getID());
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ }
+
+}
+
diff --git a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/DefaultOIDCProviderMetadataCriteriaToIdentifierStrategy.java b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/DefaultOIDCProviderMetadataCriteriaToIdentifierStrategy.java
new file mode 100644
index 0000000..40291b0
--- /dev/null
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/DefaultOIDCProviderMetadataCriteriaToIdentifierStrategy.java
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements. See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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.oidc.metadata.cache.impl;
+
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import com.nimbusds.oauth2.sdk.id.Issuer;
+
+import net.shibboleth.oidc.metadata.criterion.IssuerIDCriterion;
+import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
+
+/** Strategy for extracting the {@link IssuerIDCriterion} from a {@link CriteriaSet}.*/
+public class DefaultOIDCProviderMetadataCriteriaToIdentifierStrategy implements Function<CriteriaSet, Issuer> {
+
+ @Override
+ @Nullable public Issuer apply(@Nonnull final CriteriaSet criteria) {
+ final IssuerIDCriterion issuerId = criteria.get(IssuerIDCriterion.class);
+ if (issuerId != null) {
+ return issuerId.getIssuerID();
+ }
+ return null;
+ }
+
+}
diff --git a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/DefaultOIDCProviderMetadataExpirationTimeStrategy.java b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/DefaultOIDCProviderMetadataExpirationTimeStrategy.java
new file mode 100644
index 0000000..fa1fb78
--- /dev/null
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/DefaultOIDCProviderMetadataExpirationTimeStrategy.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements. See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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.oidc.metadata.cache.impl;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.util.function.BiFunction;
+
+import javax.annotation.Nonnull;
+
+import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
+
+import net.shibboleth.utilities.java.support.logic.Constraint;
+
+/** Strategy for computing an expiry time for {@link OIDCProviderMetadata}. Defaults to now plus the provided expiry time.*/
+public class DefaultOIDCProviderMetadataExpirationTimeStrategy implements BiFunction<OIDCProviderMetadata, Instant, Instant> {
+
+ /** How long after now should the metadata expire.*/
+ @Nonnull private final Duration expiryDuration;
+
+ public DefaultOIDCProviderMetadataExpirationTimeStrategy(@Nonnull final Duration duration) {
+ expiryDuration = Constraint.isNotNull(duration, "Expiry duration can not be null");
+ }
+
+ @Override
+ public Instant apply(@Nonnull final OIDCProviderMetadata metadata, @Nonnull final Instant now) {
+ return now.plus(expiryDuration);
+ }
+
+}
diff --git a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/DefaultOIDCProviderMetadataIdentifierExtractionStrategy.java b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/DefaultOIDCProviderMetadataIdentifierExtractionStrategy.java
new file mode 100644
index 0000000..6e86e76
--- /dev/null
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/DefaultOIDCProviderMetadataIdentifierExtractionStrategy.java
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements. See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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.oidc.metadata.cache.impl;
+
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+
+import com.nimbusds.oauth2.sdk.id.Issuer;
+import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
+
+/** Strategy for finding the Issuer of the given OIDCProviderMetadata.*/
+public class DefaultOIDCProviderMetadataIdentifierExtractionStrategy implements Function<OIDCProviderMetadata, Issuer>{
+
+ @Override
+ public Issuer apply(@Nonnull final OIDCProviderMetadata metadata) {
+ return metadata.getIssuer();
+ }
+
+}
diff --git a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/MetadataCacheBuilder.java b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/MetadataCacheBuilder.java
new file mode 100644
index 0000000..4536833
--- /dev/null
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/MetadataCacheBuilder.java
@@ -0,0 +1,341 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements. See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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.oidc.metadata.cache.impl;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.util.List;
+import java.util.function.BiConsumer;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.springframework.beans.factory.config.AbstractFactoryBean;
+
+import net.shibboleth.oidc.metadata.filter.MetadataFilterContext;
+import net.shibboleth.utilities.java.support.annotation.constraint.Positive;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
+
+/**
+ * Base metadata cache builder instance.
+ *
+ * @param <T> the metadata identifier/key
+ * @param <U> the metadata type.
+ */
+public abstract class MetadataCacheBuilder<T,U> extends AbstractFactoryBean<DefaultMetadataCache<T, U>>{
+
+ /** Maximum cache duration. */
+ @Nonnull private Duration maxCacheDuration;
+
+ /** Minimum cache duration. */
+ @Nonnull private Duration minCacheDuration;
+
+ /** Factor used to compute when the next refresh interval will occur. Default value: 0.75 */
+ @Positive private Float refreshDelayFactor;
+
+ /** The maximum idle time for which the resolver will keep data for a given entityID,
+ * before it is removed. */
+ @Nullable private Duration maxIdleEntityData;
+
+ /** The interval at which the cleanup task should run. */
+ @Nonnull private Duration cleanupTaskInterval;
+
+ /** The initial cleanup task delay.*/
+ @Nonnull private Duration initialCleanupTaskDelay;
+
+ /** Strategy used to extract an identifier from the given metadata.*/
+ @Nullable private Function<U, T> identifierExtractionStrategy;
+
+ /** Strategy used to compute an expiration time. */
+ @Nullable private BiFunction<U, Instant, Instant> metadataExpirationTimeStrategy;
+
+ /** Map criteria to identifiers to use as keys to the backing store.*/
+ @Nullable private Function<CriteriaSet, T> criteriaToIdentifierStrategy;
+
+ /** A strategy to filter metadata. */
+ @Nonnull private BiFunction<U, MetadataFilterContext , U> metadataFilterStrategy;
+
+ /**
+ * A hook that is executed just before a cache entry will been removed/invalidated/evicted.
+ * The metadata list could be null, the identifier is never null.
+ */
+ @Nullable private BiConsumer<List<U>, T> metadataBeforeRemovalHook;
+
+ /** Flag indicating whether idle entity data should be removed. */
+ private boolean removeIdleEntityData;
+
+
+ protected MetadataCacheBuilder(){
+ //defaults
+ maxCacheDuration = Duration.ofHours(8);
+ minCacheDuration = Duration.ofMinutes(10);
+ maxIdleEntityData = Duration.ofHours(8);
+ refreshDelayFactor = 0.75f;
+ cleanupTaskInterval = Duration.ofMinutes(30);
+ initialCleanupTaskDelay = Duration.ofMinutes(1);
+ removeIdleEntityData = true;
+ // create a default direct in/out filter
+ metadataFilterStrategy = (metadata, context) -> metadata;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * Call destroy on the default cache implementation.
+ */
+ @Override protected void destroyInstance(
+ @Nullable DefaultMetadataCache<T, U> instance) throws Exception {
+ if (instance != null) {
+ instance.destroy();
+ }
+ }
+
+ /**
+ * Set a hook to run before a metadata cache entry is removed from the cache.
+ * <p>The hook is required to gracefully handle null metadata lists.</p>
+ *
+ * @param hook the hook to run.
+ */
+ public void setMetadataBeforeRemovalHook(
+ @Nullable final BiConsumer<List<U>, T> hook) {
+ metadataBeforeRemovalHook = hook;
+ }
+
+ /**
+ * Get the metadata before removal hook. Could be {@literal null}.
+ *
+ * @return the hook.
+ */
+ @Nullable public BiConsumer<List<U>, T> getMetadataBeforeRemovalHook() {
+ return metadataBeforeRemovalHook;
+ }
+
+ /**
+ * Set the metadata filtering strategy.
+ *
+ * @param strategy the metadata filtering strategy.
+ */
+ public void setMetadataFilterStrategy(@Nonnull final BiFunction<U, MetadataFilterContext, U> strategy) {
+ this.metadataFilterStrategy = Constraint.isNotNull(strategy, "Metadata filter strategy can not be null");;
+ }
+
+ /**
+ * Get the metadata filter strategy.
+ *
+ * @return the metadata filtering strategy
+ */
+ public BiFunction<U, MetadataFilterContext, U> getMetadataFilterStrategy() {
+ return metadataFilterStrategy;
+ }
+
+ /**
+ * Get the initial cleanup task delay.
+ *
+ * @return Returns the initialCleanupTaskDelay.
+ */
+ protected Duration getInitialCleanupTaskDelay() {
+ return initialCleanupTaskDelay;
+ }
+
+ /**
+ * Set the initial cleanup task delay.
+ *
+ * @param delay The initialCleanupTaskDelay to set.
+ */
+ public void setInitialCleanupTaskDelay(@Nonnull final Duration delay) {
+ Constraint.isNotNull(delay, "Cleanup task delay can not be null");
+ Constraint.isFalse(delay.isNegative() || delay.isZero(), "Cleanup task delay must be positive");
+ initialCleanupTaskDelay = delay;
+
+ }
+
+ /**
+ * Set the flag indicating whether idle entity data should be removed.
+ *
+ * @param flag true if idle entity data should be removed, false otherwise
+ */
+ public void setRemoveIdleEntityData(final boolean flag) {
+ removeIdleEntityData = flag;
+ }
+
+ protected boolean isRemoveIdleEntityData() {
+ return removeIdleEntityData;
+ }
+
+ public void setIdentifierExtractionStrategy(@Nonnull final Function<U, T> strategy) {
+ identifierExtractionStrategy = Constraint.isNotNull(strategy, "Strategy can not be null");
+ }
+
+ @Nullable protected Function<U, T> getIdentifierExtractionStrategy() {
+ return identifierExtractionStrategy;
+ }
+
+ public void setMetadataExpirationTimeStrategy(
+ @Nonnull final BiFunction<U, Instant, Instant> strategy) {
+ metadataExpirationTimeStrategy = Constraint.isNotNull(strategy, "Strategy can not be null");
+ }
+
+ @Nullable protected BiFunction<U, Instant, Instant> getMetadataExpirationTimeStrategy() {
+ return metadataExpirationTimeStrategy;
+ }
+
+ public void setCriteriaToIdentifierStrategy(@Nonnull final Function<CriteriaSet, T> strategy) {
+ criteriaToIdentifierStrategy = Constraint.isNotNull(strategy,"Criteria to identifier strategy can not be null");
+ }
+
+ @Nullable protected Function<CriteriaSet, T> getCriteriaToIdentifierStrategy() {
+ return criteriaToIdentifierStrategy;
+ }
+
+ /**
+ * Get the interval at which the cleanup task should run.
+ *
+ * <p>Defaults to: 30 minutes.</p>
+ *
+ * @return return the interval
+ */
+ @Nonnull protected Duration getCleanupTaskInterval() {
+ return cleanupTaskInterval;
+ }
+
+ /**
+ * Set the interval at which the cleanup task should run.
+ *
+ * <p>Defaults to: 30 minutes.</p>
+ *
+ * @param interval the interval to set
+ */
+ public void setCleanupTaskInterval(@Nonnull final Duration interval) {
+ Constraint.isNotNull(interval, "Cleanup task interval may not be null");
+ Constraint.isFalse(interval.isNegative() || interval.isZero(), "Cleanup task interval must be positive");
+
+ cleanupTaskInterval = interval;
+ }
+
+ /**
+ * Get the maximum idle time for which the resolver will keep data for a given entityID,
+ * before it is removed.
+ *
+ * <p>Defaults to: 8 hours.</p>
+ *
+ * @return return the maximum idle time
+ */
+ @Nonnull protected Duration getMaxIdleEntityData() {
+ return maxIdleEntityData;
+ }
+
+ /**
+ * Set the maximum idle time for which the resolver will keep data for a given entityID,
+ * before it is removed.
+ *
+ * <p>Defaults to: 8 hours.</p>
+ *
+ * @param max the maximum entity data idle time
+ */
+ public void setMaxIdleEntityData(@Nonnull final Duration max) {
+
+ Constraint.isNotNull(max, "Max idle time cannot be null");
+ Constraint.isFalse(max.isNegative(), "Max idle time cannot be negative");
+
+ maxIdleEntityData = max;
+ }
+
+ /**
+ * Get the maximum cache duration for metadata.
+ *
+ * <p>Defaults to: 8 hours.</p>
+ *
+ * @return the maximum cache duration
+ */
+ @Nonnull protected Duration getMaxCacheDuration() {
+ return maxCacheDuration;
+ }
+
+ /**
+ * Set the maximum cache duration for metadata.
+ *
+ * <p>Defaults to: 8 hours.</p>
+ *
+ * @param duration the maximum cache duration
+ */
+ public void setMaxCacheDuration(@Nonnull final Duration duration) {
+
+ Constraint.isNotNull(duration, "Duration cannot be null");
+ Constraint.isFalse(duration.isNegative(), "Duration cannot be negative");
+
+ maxCacheDuration = duration;
+ }
+
+ /**
+ * Get the minimum cache duration for metadata.
+ *
+ * <p>Defaults to: 10 minutes.</p>
+ *
+ * @return the minimum cache duration
+ */
+ @Nonnull protected Duration getMinCacheDuration() {
+ return minCacheDuration;
+ }
+
+
+ /**
+ * Set the minimum cache duration for metadata.
+ *
+ * <p>Defaults to: 10 minutes.</p>
+ *
+ * @param duration the minimum cache duration
+ */
+ public void setMinCacheDuration(@Nonnull final Duration duration) {
+ Constraint.isNotNull(duration, "Duration cannot be null");
+ Constraint.isFalse(duration.isNegative(), "Duration cannot be negative");
+
+ minCacheDuration = duration;
+ }
+
+ /**
+ * Gets the delay factor used to compute the next refresh time.
+ *
+ * <p>Defaults to: 0.75.</p>
+ *
+ * @return delay factor used to compute the next refresh time
+ */
+ @Nonnull protected Float getRefreshDelayFactor() {
+ return refreshDelayFactor;
+ }
+
+
+ /**
+ * Sets the delay factor used to compute the next refresh time. The delay must be between 0.0 and 1.0, exclusive.
+ *
+ * <p>Defaults to: 0.75.</p>
+ *
+ * @param factor delay factor used to compute the next refresh time
+ */
+ public void setRefreshDelayFactor(@Nonnull final Float factor) {
+
+ if (factor <= 0 || factor >= 1) {
+ throw new IllegalArgumentException("Refresh delay factor must be a number between 0.0 and 1.0, exclusive");
+ }
+
+ refreshDelayFactor = factor;
+ }
+
+}
diff --git a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/OIDCProviderMetadataCacheFactoryBean.java b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/OIDCProviderMetadataCacheFactoryBean.java
new file mode 100644
index 0000000..8d14d94
--- /dev/null
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/OIDCProviderMetadataCacheFactoryBean.java
@@ -0,0 +1,43 @@
+package net.shibboleth.oidc.metadata.cache.impl;
+
+import com.nimbusds.oauth2.sdk.id.Issuer;
+import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
+
+import net.shibboleth.oidc.metadata.impl.DefaultBackingStore;
+
+/**
+ * Factory bean to create an OIDC specific metadata cache.
+ */
+public class OIDCProviderMetadataCacheFactoryBean extends MetadataCacheBuilder<Issuer, OIDCProviderMetadata> {
+
+ //TODO raw type?
+ @SuppressWarnings("rawtypes")
+ @Override
+ public Class<DefaultMetadataCache> getObjectType() {
+ return DefaultMetadataCache.class;
+ }
+
+ @Override
+ protected DefaultMetadataCache<Issuer, OIDCProviderMetadata> createInstance() throws Exception {
+
+ final DefaultMetadataCache<Issuer, OIDCProviderMetadata> cache = new DefaultMetadataCache<>(
+ new DefaultBackingStore<>(getMaxCacheDuration()));
+ cache.setMinCacheDuration(getMinCacheDuration());
+ cache.setRefreshDelayFactor(getRefreshDelayFactor());
+ cache.setMaxIdleEntityData(getMaxIdleEntityData());
+ cache.setMetadataExpirationTimeStrategy(getMetadataExpirationTimeStrategy());
+ cache.setIdentifierExtractionStrategy(getIdentifierExtractionStrategy());
+ cache.setCriteriaToIdentifierStrategy(getCriteriaToIdentifierStrategy());
+ cache.setCleanupTaskInterval(getCleanupTaskInterval());
+ cache.setRemoveIdleEntityData(isRemoveIdleEntityData());
+ cache.setInitialCleanupTaskDelay(getInitialCleanupTaskDelay());
+ cache.setMetadataFilterStrategy(getMetadataFilterStrategy());
+ cache.setMetadataBeforeRemovalHook(getMetadataBeforeRemovalHook());
+ cache.setId("OIDCProviderMetadataCache");
+ cache.initialize();
+ return cache;
+ }
+
+
+
+}
diff --git a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/package-info.java b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/package-info.java
new file mode 100644
index 0000000..a9d747a
--- /dev/null
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/package-info.java
@@ -0,0 +1,22 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements. See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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.
+ */
+
+/**
+ *
+ * Metadata cache implementation.
+ */
+package net.shibboleth.oidc.metadata.cache.impl;
\ No newline at end of file
diff --git a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/AbstractDynamicOIDCHTTPProviderMetadataResolver.java b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/AbstractDynamicHTTPFetchingStrategy.java
similarity index 60%
rename from oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/AbstractDynamicOIDCHTTPProviderMetadataResolver.java
rename to oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/AbstractDynamicHTTPFetchingStrategy.java
index adf9687..405b968 100644
--- a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/AbstractDynamicOIDCHTTPProviderMetadataResolver.java
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/AbstractDynamicHTTPFetchingStrategy.java
@@ -1,25 +1,20 @@
package net.shibboleth.oidc.metadata.impl;
import java.io.IOException;
-import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Set;
+import java.util.function.Function;
import java.util.stream.Collectors;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
-import org.apache.http.Header;
-import org.apache.http.HttpResponse;
-import org.apache.http.HttpStatus;
import org.apache.http.client.HttpClient;
import org.apache.http.client.ResponseHandler;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.protocol.HttpClientContext;
-import org.apache.http.util.EntityUtils;
-import org.opensaml.saml.metadata.resolver.impl.AbstractDynamicHTTPMetadataResolver;
import org.opensaml.security.httpclient.HttpClientSecurityParameters;
import org.opensaml.security.httpclient.HttpClientSecuritySupport;
import org.slf4j.Logger;
@@ -28,28 +23,18 @@ import org.slf4j.MDC;
import com.google.common.base.Strings;
import com.google.common.net.MediaType;
-import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
import net.shibboleth.utilities.java.support.annotation.constraint.NotLive;
import net.shibboleth.utilities.java.support.annotation.constraint.Unmodifiable;
-import net.shibboleth.utilities.java.support.collection.LazySet;
-import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+import net.shibboleth.utilities.java.support.component.AbstractIdentifiableInitializableComponent;
import net.shibboleth.utilities.java.support.component.ComponentSupport;
import net.shibboleth.utilities.java.support.logic.Constraint;
-import net.shibboleth.utilities.java.support.net.MediaTypeSupport;
import net.shibboleth.utilities.java.support.primitive.StringSupport;
-import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
import net.shibboleth.utilities.java.support.resolver.ResolverException;
-/**
- * Abstract subclass for dynamic metadata resolvers that implement OIDC provider
- * metadata resolution based on HTTP requests.
- *
- */
-//TODO this is a singleton, and needs guarding?
-public abstract class AbstractDynamicOIDCHTTPProviderMetadataResolver<MetadataIdentifier, MetadataType, CriteriaType>
- extends AbstractDynamicOIDCProviderMetadataResolver<MetadataIdentifier, MetadataType, CriteriaType> {
+public abstract class AbstractDynamicHTTPFetchingStrategy<CriteriaType, MetadataType>
+ extends AbstractIdentifiableInitializableComponent implements Function<CriteriaType, MetadataType> {
/** Default list of supported content MIME types. */
private static final String[] DEFAULT_CONTENT_TYPES = new String[] {"application/json",
@@ -58,10 +43,10 @@ public abstract class AbstractDynamicOIDCHTTPProviderMetadataResolver<MetadataId
/** MDC attribute representing the current request URI. Will be available during the execution of the
* configured {@link ResponseHandler}. */
public static final String MDC_ATTRIB_CURRENT_REQUEST_URI =
- AbstractDynamicOIDCHTTPProviderMetadataResolver.class.getName() + ".currentRequestURI";
+ AbstractDynamicHTTPFetchingStrategy.class.getName() + ".currentRequestURI";
/** Class logger. */
- @Nonnull private final Logger log = LoggerFactory.getLogger(AbstractDynamicOIDCHTTPProviderMetadataResolver.class);
+ @Nonnull private final Logger log = LoggerFactory.getLogger(AbstractDynamicHTTPFetchingStrategy.class);
/** HTTP Client used to pull the configuration information. */
@Nonnull private final HttpClient httpClient;
@@ -88,80 +73,12 @@ public abstract class AbstractDynamicOIDCHTTPProviderMetadataResolver<MetadataId
*
* @param client the instance of {@link HttpClient} used to fetch remote OIDC metadata
*/
- protected AbstractDynamicOIDCHTTPProviderMetadataResolver(@Nonnull final HttpClient client,
+ protected AbstractDynamicHTTPFetchingStrategy(@Nonnull final HttpClient client,
@Nonnull final ResponseHandler<MetadataType> handler) {
responseHandler = Constraint.isNotNull(handler, "Response handler can not be null");
httpClient = Constraint.isNotNull(client, "HTTP Client can not be null");
}
- /**
- * Get the list of supported MIME types for use in Accept request header and validation of
- * response Content-Type header.
- *
- * @return the supported content types
- */
- @NonnullAfterInit @NotLive @Unmodifiable
- public List<String> getSupportedContentTypes() {
- return supportedContentTypes;
- }
-
- /**
- * Set the list of supported MIME types for use in Accept request header and validation of
- * response Content-Type header. Values will be effectively lower-cased at runtime.
- *
- * @param types the new supported content types to set
- */
- public void setSupportedContentTypes(@Nullable final List<String> types) {
- ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
- ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
- if (types == null) {
- supportedContentTypes = Collections.emptyList();
- } else {
- supportedContentTypes = StringSupport.normalizeStringCollection(types)
- .stream()
- .filter(s -> s != null)
- .map(String::toLowerCase)
- .collect(Collectors.toUnmodifiableList());
- }
- }
-
- /**
- * Get the list of supported MIME {@link MediaType} instances used in validation of
- * the response Content-Type header.
- *
- * <p>
- * Is generated at init time from {@link #getSupportedContentTypes()}.
- * </p>
- *
- * @return the supported content types
- */
- @NonnullAfterInit @NotLive @Unmodifiable
- protected Set<MediaType> getSupportedMediaTypes() {
- return supportedMediaTypes;
- }
-
- @Override
- protected void initMetadataResolver() throws ComponentInitializationException {
- super.initMetadataResolver();
-
- if (getSupportedContentTypes() == null) {
- setSupportedContentTypes(Arrays.asList(DEFAULT_CONTENT_TYPES));
- }
-
- if (! getSupportedContentTypes().isEmpty()) {
- supportedContentTypesValue = StringSupport.listToStringValue(getSupportedContentTypes(), ", ");
- supportedMediaTypes = new LazySet<>();
- for (final String contentType : getSupportedContentTypes()) {
- supportedMediaTypes.add(MediaType.parse(contentType));
- }
- } else {
- supportedMediaTypes = Collections.emptySet();
- }
-
- log.debug("{} Supported content types are: {}", getLogPrefix(), getSupportedContentTypes());
- }
-
-
/**
* Set an instance of {@link HttpClientSecurityParameters} which provides various parameters to influence
* the security behavior of the HttpClient instance.
@@ -203,24 +120,43 @@ public abstract class AbstractDynamicOIDCHTTPProviderMetadataResolver<MetadataId
httpClientSecurityParameters = params;
}
- @Override
- protected void doDestroy() {
- httpClientSecurityParameters = null;
-
- supportedContentTypes = null;
- supportedContentTypesValue = null;
- supportedMediaTypes = null;
-
- super.doDestroy();
+ /**
+ * Get the list of supported MIME types for use in Accept request header and validation of
+ * response Content-Type header.
+ *
+ * @return the supported content types
+ */
+ @NonnullAfterInit @NotLive @Unmodifiable
+ public List<String> getSupportedContentTypes() {
+ return supportedContentTypes;
}
+ /**
+ * Set the list of supported MIME types for use in Accept request header and validation of
+ * response Content-Type header. Values will be effectively lower-cased at runtime.
+ *
+ * @param types the new supported content types to set
+ */
+ public void setSupportedContentTypes(@Nullable final List<String> types) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+ ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+ if (types == null) {
+ supportedContentTypes = Collections.emptyList();
+ } else {
+ supportedContentTypes = StringSupport.normalizeStringCollection(types)
+ .stream()
+ .filter(s -> s != null)
+ .map(String::toLowerCase)
+ .collect(Collectors.toUnmodifiableList());
+ }
+ }
+
@Override
- @Nullable protected MetadataType fetchFromOriginSource(@Nonnull final CriteriaType criteria)
- throws IOException {
-
+ @Nullable public MetadataType apply(@Nonnull final CriteriaType criteria) {
+ log.info("{} fetching metadata based on criteria: {}", getId(), criteria);
final HttpUriRequest request = buildHttpRequest(criteria);
if (request == null) {
- log.debug("{} Could not build request based on input criteria, unable to query", getLogPrefix());
+ log.debug("Could not build request based on input criteria, unable to query");
return null;
}
@@ -231,50 +167,53 @@ public abstract class AbstractDynamicOIDCHTTPProviderMetadataResolver<MetadataId
final MetadataType result = httpClient.execute(request, responseHandler, context);
HttpClientSecuritySupport.checkTLSCredentialEvaluated(context, request.getURI().getScheme());
return result;
+ } catch (IOException e) {
+ log.warn("Unable to fetch metadata from remote HTTP source",e);
+ return null;
} finally {
MDC.remove(MDC_ATTRIB_CURRENT_REQUEST_URI);
}
}
/**
- * Build the {@link HttpClientContext} instance which will be used to invoke the {@link HttpClient} request.
- *
- * @param request the current HTTP request
- *
- * @return a new instance of {@link HttpClientContext}
- */
- protected HttpClientContext buildHttpClientContext(@Nonnull final HttpUriRequest request) {
- final HttpClientContext context = HttpClientContext.create();
-
- HttpClientSecuritySupport.marshalSecurityParameters(context, httpClientSecurityParameters, true);
- HttpClientSecuritySupport.addDefaultTLSTrustEngineCriteria(context, request);
-
- return context;
- }
+ * Build the {@link HttpClientContext} instance which will be used to invoke the {@link HttpClient} request.
+ *
+ * @param request the current HTTP request
+ *
+ * @return a new instance of {@link HttpClientContext}
+ */
+ private HttpClientContext buildHttpClientContext(@Nonnull final HttpUriRequest request) {
+ final HttpClientContext context = HttpClientContext.create();
+
+ HttpClientSecuritySupport.marshalSecurityParameters(context, httpClientSecurityParameters, true);
+ HttpClientSecuritySupport.addDefaultTLSTrustEngineCriteria(context, request);
+
+ return context;
+ }
/**
- * Build an appropriate instance of {@link HttpUriRequest} based on the input criteria set.
- *
- * @param criteria the input criteria set
- * @return the newly constructed request, or null if it can not be built from the supplied criteria
- */
- @Nullable protected HttpUriRequest buildHttpRequest(@Nonnull final CriteriaType criteria) {
- final String url = buildRequestURL(criteria);
- log.debug("{} Built request URL of: {}", getLogPrefix(), url);
-
- if (url == null) {
- log.debug("{} Could not construct request URL from input criteria, unable to query", getLogPrefix());
- return null;
- }
-
- final HttpGet getMethod = new HttpGet(url);
-
- if (!Strings.isNullOrEmpty(supportedContentTypesValue)) {
- getMethod.addHeader("Accept", supportedContentTypesValue);
- }
-
- return getMethod;
- }
+ * Build an appropriate instance of {@link HttpUriRequest} based on the input criteria set.
+ *
+ * @param criteria the input criteria set
+ * @return the newly constructed request, or null if it can not be built from the supplied criteria
+ */
+ @Nullable private HttpUriRequest buildHttpRequest(@Nonnull final CriteriaType criteria) {
+ final String url = buildRequestURL(criteria);
+ log.debug("Built request URL of: {}", url);
+
+ if (url == null) {
+ log.debug("Could not construct request URL from input criteria, unable to query");
+ return null;
+ }
+
+ final HttpGet getMethod = new HttpGet(url);
+
+ if (!Strings.isNullOrEmpty(supportedContentTypesValue)) {
+ getMethod.addHeader("Accept", supportedContentTypesValue);
+ }
+
+ return getMethod;
+ }
/**
* Build the request URL based on the input criteria set.
@@ -283,7 +222,5 @@ public abstract class AbstractDynamicOIDCHTTPProviderMetadataResolver<MetadataId
* @return the request URL, or null if it can not be built based on the supplied criteria
*/
@Nullable protected abstract String buildRequestURL(@Nonnull final CriteriaType criteria);
-
-
}
diff --git a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/AbstractDynamicOIDCMetadataResolver.java b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/AbstractDynamicOIDCMetadataResolver.java
new file mode 100644
index 0000000..291595b
--- /dev/null
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/AbstractDynamicOIDCMetadataResolver.java
@@ -0,0 +1,132 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements. See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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.oidc.metadata.impl;
+
+import java.util.List;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import net.shibboleth.oidc.metadata.DynamicOIDCMetadataResolver;
+import net.shibboleth.oidc.metadata.cache.MetadataCache;
+import net.shibboleth.oidc.metadata.cache.MetadataCacheException;
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+import net.shibboleth.utilities.java.support.component.ComponentSupport;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
+import net.shibboleth.utilities.java.support.resolver.ResolverException;
+
+/**
+ * Abstract subclass for metadata resolvers that resolve provider metadata dynamically, as needed and on demand.
+ *
+ * Is instrumented to collect timming metrics.
+ *
+ * Has a cache to ...
+ *
+ * @param <IdentifierType> The identifier type in the backing store
+ * @param <MetadataType> The metadata type in the backing store
+ */
+public abstract class AbstractDynamicOIDCMetadataResolver<IdentifierType, MetadataType>
+ extends AbstractOIDCMetadataResolver<IdentifierType, MetadataType>
+ implements DynamicOIDCMetadataResolver<MetadataType> {
+
+ /** Class logger. */
+ private final Logger log = LoggerFactory.getLogger(AbstractDynamicOIDCMetadataResolver.class);
+
+ /** The strategy used to fetch metadata from a source.*/
+ @NonnullAfterInit private final Function<CriteriaSet, MetadataType> metadataFetchingStrategy;
+
+
+ /** Constructor.*/
+ protected AbstractDynamicOIDCMetadataResolver(
+ @Nonnull final MetadataCache<MetadataType> metadataCache,
+ @Nonnull final Function<CriteriaSet, MetadataType> fetchingStrategy) {
+ super(metadataCache);
+ metadataFetchingStrategy = Constraint.isNotNull(fetchingStrategy, "Metadata fetching strategy can not be null");
+ }
+
+
+ /** {@inheritDoc} */
+ @Override
+ protected void initMetadataResolver() throws ComponentInitializationException {
+
+ if (metadataFetchingStrategy == null) {
+ throw new ComponentInitializationException("Metadating fetching strategy can not be null");
+ }
+
+ try { //TODO metrics, cache loading.
+
+// initializeMetricsInstrumentation();
+//
+// if (getPersistentCacheKeyGenerator() == null) {
+// setPersistentCacheKeyGenerator(new DefaultCacheKeyGenerator());
+// }
+//
+// if (getInitializationFromCachePredicate() == null) {
+// setInitializationFromCachePredicate(Predicates.alwaysTrue());
+// }
+//
+// persistentCacheInitMetrics = new PersistentCacheInitializationMetrics();
+// if (isPersistentCachingEnabled()) {
+// persistentCacheInitMetrics.enabled = true;
+// if (isInitializeFromPersistentCacheInBackground()) {
+// log.debug("{} Initializing from the persistent cache in the background in {} ms",
+// getLogPrefix(), getBackgroundInitializationFromCacheDelay());
+// final TimerTask initTask = new TimerTask() {
+// public void run() {
+// initializeFromPersistentCache();
+// }
+// };
+// taskTimer.schedule(initTask, getBackgroundInitializationFromCacheDelay().toMillis());
+// } else {
+// log.debug("{} Initializing from the persistent cache in the foreground", getLogPrefix());
+// initializeFromPersistentCache();
+// }
+// }
+
+
+ } finally {
+
+ }
+ }
+
+
+ /** {@inheritDoc} */
+ @Override
+ @Nonnull public Iterable<MetadataType> resolve(@Nonnull final CriteriaSet criteria) throws ResolverException {
+ ComponentSupport.ifNotInitializedThrowUninitializedComponentException(this);
+ ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+
+ //final Context contextResolve = MetricsSupport.startTimer(timerResolve);
+ try {
+ final List<MetadataType> metadata = getCache()
+ .getOrFetchIfAbsent(criteria, metadataFetchingStrategy);
+ return predicateFilterCandidates(metadata, criteria, false);
+
+ } catch (MetadataCacheException e) {
+ throw new ResolverException(e);
+ } finally {
+ //MetricsSupport.stopTimer(contextResolve);
+ }
+ }
+
+}
diff --git a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/AbstractDynamicOIDCProviderMetadataResolver.java b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/AbstractDynamicOIDCProviderMetadataResolver.java
deleted file mode 100644
index 4a7709c..0000000
--- a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/AbstractDynamicOIDCProviderMetadataResolver.java
+++ /dev/null
@@ -1,592 +0,0 @@
-package net.shibboleth.oidc.metadata.impl;
-
-import java.io.IOException;
-import java.time.Duration;
-import java.time.Instant;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Set;
-import java.util.concurrent.locks.Lock;
-
-import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
-
-import org.opensaml.core.xml.XMLObject;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
-
-import net.shibboleth.oidc.metadata.DynamicBackingStore;
-import net.shibboleth.oidc.metadata.DynamicOIDCProviderMetadataResolver;
-import net.shibboleth.oidc.metadata.MetadataManagementData;
-import net.shibboleth.oidc.metadata.filter.FilterException;
-import net.shibboleth.oidc.metadata.filter.MetadataFilterContext;
-import net.shibboleth.oidc.metadata.filter.MetadataSource;
-import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
-import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
-import net.shibboleth.utilities.java.support.annotation.constraint.Positive;
-import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
-import net.shibboleth.utilities.java.support.component.ComponentSupport;
-import net.shibboleth.utilities.java.support.logic.Constraint;
-import net.shibboleth.utilities.java.support.resolver.ResolverException;
-
-/**
- * Abstract subclass for metadata resolvers that resolve provider metadata dynamically, as needed and on demand.
- *
- * Is instrumented to collect timming metrics.
- *
- * Has a cache to ...
- *
- * @param <MetadataIdentifier> The identifier type in the backing store
- * @param <MetadataType> The metadata type in the backing store
- * @param <CriteriaType>
- */
-public abstract class AbstractDynamicOIDCProviderMetadataResolver<MetadataIdentifier, MetadataType, CriteriaType>
- extends AbstractOIDCMetadataResolver<MetadataIdentifier, MetadataType, CriteriaType>
- implements DynamicOIDCProviderMetadataResolver<MetadataType, CriteriaType> {
-
- /** Class logger. */
- private final Logger log = LoggerFactory.getLogger(AbstractDynamicOIDCProviderMetadataResolver.class);
-
- /** Flag used to track state of whether currently initializing or not. */
- //TODO is this actually used/needed?
- private boolean initializing;
-
- /** Maximum cache duration. */
- @Nonnull private Duration maxCacheDuration;
-
- /** Minimum cache duration. */
- @Nonnull private Duration minCacheDuration;
-
- /** Factor used to compute when the next refresh interval will occur. Default value: 0.75 */
- @Positive private Float refreshDelayFactor;
-
-
-
- /** Constructor.*/
- protected AbstractDynamicOIDCProviderMetadataResolver() {
- super();
-
- maxCacheDuration = Duration.ofHours(8);
- minCacheDuration = Duration.ofMinutes(10);
- refreshDelayFactor = 0.75f;
- }
-
- /**
- * Get the maximum cache duration for metadata.
- *
- * <p>Defaults to: 8 hours.</p>
- *
- * @return the maximum cache duration
- */
- @Nonnull public Duration getMaxCacheDuration() {
- return maxCacheDuration;
- }
-
- /**
- * Set the maximum cache duration for metadata.
- *
- * <p>Defaults to: 8 hours.</p>
- *
- * @param duration the maximum cache duration
- */
- public void setMaxCacheDuration(@Nonnull final Duration duration) {
- ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
- ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
-
- Constraint.isNotNull(duration, "Duration cannot be null");
- Constraint.isFalse(duration.isNegative(), "Duration cannot be negative");
-
- maxCacheDuration = duration;
- }
-
- /**
- * Get the minimum cache duration for metadata.
- *
- * <p>Defaults to: 10 minutes.</p>
- *
- * @return the minimum cache duration
- */
- @Nonnull public Duration getMinCacheDuration() {
- return minCacheDuration;
- }
-
-
- /**
- * Set the minimum cache duration for metadata.
- *
- * <p>Defaults to: 10 minutes.</p>
- *
- * @param duration the minimum cache duration
- */
- public void setMinCacheDuration(@Nonnull final Duration duration) {
- ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
- ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
-
- Constraint.isNotNull(duration, "Duration cannot be null");
- Constraint.isFalse(duration.isNegative(), "Duration cannot be negative");
-
- minCacheDuration = duration;
- }
-
- /**
- * Gets the delay factor used to compute the next refresh time.
- *
- * <p>Defaults to: 0.75.</p>
- *
- * @return delay factor used to compute the next refresh time
- */
- @Nonnull public Float getRefreshDelayFactor() {
- return refreshDelayFactor;
- }
-
- /**
- * Sets the delay factor used to compute the next refresh time. The delay must be between 0.0 and 1.0, exclusive.
- *
- * <p>Defaults to: 0.75.</p>
- *
- * @param factor delay factor used to compute the next refresh time
- */
- public void setRefreshDelayFactor(@Nonnull final Float factor) {
- ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
- ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
-
- if (factor <= 0 || factor >= 1) {
- throw new IllegalArgumentException("Refresh delay factor must be a number between 0.0 and 1.0, exclusive");
- }
-
- refreshDelayFactor = factor;
- }
-
- /**
- * Fetch the metadata from the origin source.
- *
- * @param criteria the input criteria set
- * @return the resolved metadata parsed as a {@link OIDCProviderMetadata}, or null if metadata could not be fetched
- * @throws IOException if there is a fatal error fetching metadata from the origin source
- */
- @Nullable protected abstract MetadataType fetchFromOriginSource(@Nonnull final CriteriaType criteria)
- throws IOException;
-
-
-
- /** {@inheritDoc} */
- @Override
- protected void initMetadataResolver() throws ComponentInitializationException {
- try {
- initializing = true;
-//
-// super.initMetadataResolver();
-//
-// initializeMetricsInstrumentation();
-//
- setBackingStore(createNewBackingStore());
-//
-// if (getPersistentCacheKeyGenerator() == null) {
-// setPersistentCacheKeyGenerator(new DefaultCacheKeyGenerator());
-// }
-//
-// if (getInitializationFromCachePredicate() == null) {
-// setInitializationFromCachePredicate(Predicates.alwaysTrue());
-// }
-//
-// persistentCacheInitMetrics = new PersistentCacheInitializationMetrics();
-// if (isPersistentCachingEnabled()) {
-// persistentCacheInitMetrics.enabled = true;
-// if (isInitializeFromPersistentCacheInBackground()) {
-// log.debug("{} Initializing from the persistent cache in the background in {} ms",
-// getLogPrefix(), getBackgroundInitializationFromCacheDelay());
-// final TimerTask initTask = new TimerTask() {
-// public void run() {
-// initializeFromPersistentCache();
-// }
-// };
-// taskTimer.schedule(initTask, getBackgroundInitializationFromCacheDelay().toMillis());
-// } else {
-// log.debug("{} Initializing from the persistent cache in the foreground", getLogPrefix());
-// initializeFromPersistentCache();
-// }
-// }
-//
-// cleanupTask = new BackingStoreCleanupSweeper();
-// // Start with a delay of 1 minute, run at the user-specified interval
-// taskTimer.schedule(cleanupTask, 1*60*1000, getCleanupTaskInterval().toMillis());
-//
- } finally {
- initializing = false;
- }
- }
-
- /** {@inheritDoc} */
- @Override
- @Nonnull protected DynamicBackingStore<MetadataIdentifier, MetadataType> createNewBackingStore() {
- return new DefaultDynamicBackingStore<>(getMaxCacheDuration());
- }
-
- /** {@inheritDoc} */
- @Override
- @NonnullAfterInit protected DynamicBackingStore<MetadataIdentifier, MetadataType> getBackingStore() {
- return (DynamicBackingStore<MetadataIdentifier, MetadataType>) super.getBackingStore();
- }
-
- /** {@inheritDoc} */
- @Override
- @Nonnull public Iterable<MetadataType> resolve(@Nullable final CriteriaType criteria) throws ResolverException {
- ComponentSupport.ifNotInitializedThrowUninitializedComponentException(this);
- ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
-
- // Find identifier from criteria (impl specific)
-
- // Lock and check cache for metadata relating to entity
- // // If exists check it should not be refershed.
-
- // if not in cache or requires refresh lookup from source.
-
- // Filter candidates
-
- //final Context contextResolve = MetricsSupport.startTimer(timerResolve);
- try {
- Iterable<MetadataType> candidates = null;
-
- final MetadataIdentifier identifier = resolveIdentifier(criteria);
- if (identifier != null) {
- log.debug("{} Resolved criteria to identifier: {}", getLogPrefix(), identifier);
-
- final MetadataManagementData<MetadataIdentifier> mgmtData =
- getBackingStore().computeManagementDataIfAbsent(identifier);
- final Lock readLock = mgmtData.getReadWriteLock().readLock();
- try {
- readLock.lock();
-
- final List<MetadataType> metadata = lookupIdentifier(identifier);
- if (metadata.isEmpty()) {
-// if (mgmtData.isNegativeLookupCacheActive()) {
-// log.debug("{} Did not find requested metadata in backing store, "
-// + "and negative lookup cache is active, returning empty result",
-// getLogPrefix());
-// return Collections.emptyList();
-// }
- log.debug("{} Did not find requested metadata in backing store, "
- + "attempting to resolve dynamically",
- getLogPrefix());
- } else {
- if (shouldAttemptRefresh(mgmtData)) {
- log.debug("{} Metadata was indicated to be refreshed based on refresh trigger time",
- getLogPrefix());
- } else {
- log.debug("{} Found requested metadata in backing store", getLogPrefix());
- candidates = metadata;
- }
- }
- } finally {
- readLock.unlock();
- }
- } else {
- log.debug("{} Single entityID unresolveable from criteria, will resolve from origin by criteria only",
- getLogPrefix());
- }
-
- if (candidates == null) {
- candidates = resolveFromOriginSource(criteria, identifier);
- }
-
- //return predicateFilterCandidates(candidates, criteria, false);
- return candidates;
- } finally {
- //MetricsSupport.stopTimer(contextResolve);
- }
- }
-
- /**
- * Determine whether should attempt to refresh the metadata, based on stored refresh trigger time.
- *
- * @param mgmtData the entity'd management data
- * @return true if should attempt refresh, false otherwise
- */
- protected boolean shouldAttemptRefresh(@Nonnull final MetadataManagementData<MetadataIdentifier> mgmtData) {
- return Instant.now().isAfter(mgmtData.getRefreshTriggerTime());
-
- }
-
- //TODO We do not support fetching without an identifier for now.
- /**
- * Fetch metadata from an origin source based on the input criteria, store it in the backing store
- * and then return it.
- *
- * @param criteria the input criteria set
- * @param identifier the previously resolved single identifier
- * @return the resolved metadata
- * @throws ResolverException if there is a fatal error attempting to resolve the metadata
- */
- @Nonnull @NonnullElements protected Iterable<MetadataType> resolveFromOriginSource(
- @Nonnull final CriteriaType criteria, @Nonnull final MetadataIdentifier identifier) throws ResolverException {
-
- log.debug("{} Resolving from origin source based on identifier: {}", getLogPrefix(), identifier);
- final MetadataManagementData<MetadataIdentifier> mgmtData = getBackingStore().computeManagementDataIfAbsent(identifier);
- final Lock writeLock = mgmtData.getReadWriteLock().writeLock();
-
- try {
- writeLock.lock();
-
- // It's possible that multiple threads fall into here and attempt to preemptively refresh.
- // This check should ensure that only 1 actually successfully does it, b/c the refresh
- // trigger time will be updated as seen by the subsequent ones.
- final List<MetadataType> descriptors = lookupIdentifier(identifier);
- if (!descriptors.isEmpty() && !shouldAttemptRefresh(mgmtData)) {
- log.debug("{} Metadata was resolved and stored by another thread "
- + "while this thread was waiting on the write lock", getLogPrefix());
- return descriptors;
- }
- log.debug("{} Resolving metadata dynamically for ID: {}", getLogPrefix(), identifier);
-
- //final Context contextFetchFromOriginSource = MetricsSupport.startTimer(timerFetchFromOriginSource);
- MetadataType metadata = null;
- try {
- metadata = fetchFromOriginSource(criteria);
- } finally {
- // MetricsSupport.stopTimer(contextFetchFromOriginSource);
- }
-
- if (metadata == null) {
- // mgmtData.initNegativeLookupCache();
- log.debug("{} No metadata was fetched from the origin source", getLogPrefix());
-
- if (!descriptors.isEmpty()) {
- mgmtData.setRefreshTriggerTime(computeRefreshTriggerTime(mgmtData.getExpirationTime(),
- Instant.now()));
- log.debug("{} Had existing data, recalculated refresh trigger time as: {}",
- getLogPrefix(), mgmtData.getRefreshTriggerTime());
- }
- } else {
- // mgmtData.clearNegativeLookupCache();
- try {
- processNewMetadata(metadata, identifier, false);
- } catch (final FilterException e) {
- log.error("{} Metadata filtering problem processing new metadata", getLogPrefix(), e);
- }
- }
- // Return the metadata from the cache if it now exists.
- return lookupIdentifier(identifier);
-
- } catch (final IOException e) {
- log.error("{} Error fetching metadata from origin source", getLogPrefix(), e);
- return lookupIdentifier(identifier);
- } finally {
- writeLock.unlock();
- }
-
- }
-
-
- // Happens inside a lock
- @Nonnull protected void processNewMetadata(@Nonnull final MetadataType metadata,
- @Nonnull final MetadataIdentifier expectedIdentifier, final boolean fromPersistentCache)
- throws FilterException, ResolverException {
-
-
- // Filter metadata if required and configured.
- final MetadataType filteredMetadata = filterMetadata(prepareForFiltering(metadata));
- if (filteredMetadata == null) {
- log.info("{} Metadata filtering process produced a null document, resulting in an empty data set",
- getLogPrefix());
- finalizeMetadataProcessing(metadata);
- return;
- }
-
- if (!isNewMetadataValid(filteredMetadata, expectedIdentifier)) {
- return;
- }
-
- // determine identifier
- final MetadataIdentifier identifier = extractIdentifier(metadata);
- if (identifier == null) {
- //FIXME do what?
- }
-
- // remove the existing
- removeByIdentifier(identifier, getBackingStore());
-
- // add new metadata back to ordered list
- getBackingStore().getOrderedDescriptors().add(metadata);
-
- // add new metadata to index
- List<MetadataType> existingMetadata = getBackingStore().getIndexedDescriptors().get(identifier);
- if (existingMetadata == null) {
- existingMetadata = new ArrayList<>();
- getBackingStore().getIndexedDescriptors().put(identifier, existingMetadata);
- } else if (!existingMetadata.isEmpty()) {
- log.warn("{} Detected duplicate metadata for identifier: {}", getLogPrefix(), identifier);
- }
- existingMetadata.add(metadata);
-
- // Update/create managment data for entity
- final MetadataManagementData<MetadataIdentifier> mgmtData = getBackingStore().computeManagementDataIfAbsent(identifier);
-
- final Instant now = Instant.now();
- log.debug("{} For metadata expiration and refresh computation, 'now' is : {}", getLogPrefix(), now);
-
- mgmtData.setLastUpdateTime(now);
-
- mgmtData.setExpirationTime(computeExpirationTime(metadata, now));
- log.debug("{} Computed metadata expiration time: {}", getLogPrefix(), mgmtData.getExpirationTime());
-
- mgmtData.setRefreshTriggerTime(computeRefreshTriggerTime(mgmtData.getExpirationTime(), now));
- log.debug("{} Computed refresh trigger time: {}", getLogPrefix(), mgmtData.getRefreshTriggerTime());
-
- log.info("{} Successfully loaded new EntityDescriptor with entityID '{}' from {}",
- getLogPrefix(), identifier,
- fromPersistentCache ? "persistent cache" : "origin source");
-
- //finalize processing of both the filtered and original metadata.
- finalizeMetadataProcessing(filteredMetadata);
- finalizeMetadataProcessing(metadata);
-
- //TODO log the new metadata expiration
-
- //TODO save metadata to persistent cache if enabled.
-
- }
-
- /**
- * Check the metadata is valid e.g. is the correct type. Is implementation specific.
- *
- * @param metadata the metadata to check.
- * @param expectedIdentifier the expected identifier of the metadata.
- *
- * @return true iff the metadata is valid, false otherwise.
- *
- * @throws FilterException if there is a fatal error validating the metadata.
- */
- @Nonnull protected abstract boolean isNewMetadataValid(@Nonnull final MetadataType metadata,
- @Nonnull final MetadataIdentifier expectedIdentifier) throws ResolverException;
-
- /**
- * Prepare the object for filtering. This is implementation specific.
- *
- * @param input the metadata on which to operate
- *
- * @return the metadata instance to be filtered
- */
- @Nonnull protected abstract MetadataType prepareForFiltering(@Nonnull final MetadataType input);
-
- /**
- * Finalize the metadata object after processing. This is implementation specific.
- *
- * @param input the metadata on which to operate
- *
- * @return the metadata instance to be filtered
- */
- @Nonnull protected abstract void finalizeMetadataProcessing(@Nonnull final MetadataType input);
-
- /**
- * Filters the given metadata.
- *
- * @param metadata the metadata to be filtered
- *
- * @return the filtered metadata
- *
- * @throws FilterException thrown if there is an error filtering the metadata
- */
- @Nullable protected MetadataType filterMetadata(@Nullable final MetadataType metadata) throws FilterException {
- if (getMetadataFilter() != null) {
- log.debug("{} Applying metadata filter", getLogPrefix());
- return getMetadataFilter().filter(metadata, newFilterContext());
- }
- return metadata;
- }
-
- /**
- * Get a new instance of {@link MetadataFilterContext} to be used when filtering metadata.
- *
- * <p>
- * This default implementation will just return an empty context. Subclasses would override
- * to add contextual info specific to the implementation.
- * </p>
- *
- * @return the new filter context instance
- */
- @Nonnull protected MetadataFilterContext newFilterContext() {
-
- final MetadataSource source = new MetadataSource();
- source.setSourceId(getId());
-
- final MetadataFilterContext context = new MetadataFilterContext();
- context.add(source);
-
- return context;
- }
-
- /**
- * Compute the effective expiration time for the specified metadata.
- *
- * @param entityDescriptor the EntityDescriptor instance to evaluate
- * @param now the current date time instant
- * @return the effective expiration time for the metadata
- */
- @Nonnull protected abstract Instant computeExpirationTime(@Nonnull final MetadataType metadata,
- @Nonnull final Instant now);
-
- @Nullable protected abstract MetadataIdentifier extractIdentifier(@Nonnull final MetadataType metadata);
-
- /**
- * Compute the refresh trigger time.
- *
- * @param expirationTime the time at which the metadata effectively expires
- * @param nowDateTime the current date time instant
- *
- * @return the time after which refresh attempt(s) should be made
- */
- @Nonnull protected Instant computeRefreshTriggerTime(@Nullable final Instant expirationTime,
- @Nonnull final Instant nowDateTime) {
-
- final long now = nowDateTime.toEpochMilli();
-
- long expireInstant = 0;
- if (expirationTime != null) {
- expireInstant = expirationTime.toEpochMilli();
- }
- long refreshDelay = (long) ((expireInstant - now) * getRefreshDelayFactor());
-
- // if the expiration time was null or the calculated refresh delay was less than the floor
- // use the floor
- if (refreshDelay < getMinCacheDuration().toMillis()) {
- refreshDelay = getMinCacheDuration().toMillis();
- }
-
- return nowDateTime.plusMillis(refreshDelay);
- }
-
- /**
- * Attempt to resolve the single identifier for the operation from the criteria set.
- *
- * @param criteria the criteria set on which to operate
- * @return the resolve entityID, or null if a single entityID could not be resolved
- */
- @Nullable protected MetadataIdentifier resolveIdentifier(@Nonnull final CriteriaType criteria) {
- final Set<MetadataIdentifier> identifiers = resolveIdentifiers(criteria);
- if (identifiers.size() == 1) {
- return identifiers.iterator().next();
- }
- return null;
- }
-
- /**
- * Attempt to resolve all the identifiers represented by the criteria set.
- *
- * <p> This is an implementation specific method.</p>
- *
- * @param criteria the criteria on which to operate.
- * @return the resolved identifiers, may be empty, never null.
- */
- @Nonnull protected abstract Set<MetadataIdentifier> resolveIdentifiers(@Nonnull final CriteriaType criteria);
-
- /** {@inheritDoc} */
- @Override
- @Nonnull @NonnullElements protected List<MetadataType> lookupIdentifier(@Nonnull final MetadataIdentifier identifier)
- throws ResolverException {
- getBackingStore().computeManagementDataIfAbsent(identifier).recordEntityAccess();
- return super.lookupIdentifier(identifier);
- }
-
-}
diff --git a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/AbstractFileOIDCEntityResolver.java b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/AbstractFileOIDCEntityResolver.java
index 7d8892a..b8015bc 100644
--- a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/AbstractFileOIDCEntityResolver.java
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/AbstractFileOIDCEntityResolver.java
@@ -36,6 +36,23 @@ import net.shibboleth.utilities.java.support.component.ComponentSupport;
import net.shibboleth.utilities.java.support.logic.Constraint;
import net.shibboleth.utilities.java.support.resolver.ResolverException;
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements. See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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.
+ */
+
/**
* Based on {@link org.opensaml.saml.metadata.resolver.impl.FilesystemMetadataResolver}.
*
diff --git a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/AbstractOIDCMetadataResolver.java b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/AbstractOIDCMetadataResolver.java
index 6a266c7..dfe1689 100644
--- a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/AbstractOIDCMetadataResolver.java
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/AbstractOIDCMetadataResolver.java
@@ -1,34 +1,54 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements. See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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.oidc.metadata.impl;
-import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
+import java.util.Set;
+import java.util.function.Predicate;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
+import org.opensaml.core.criterion.SatisfyAnyCriterion;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import net.shibboleth.oidc.metadata.BackingStore;
+import com.google.common.collect.Iterables;
+
+import net.shibboleth.oidc.metadata.EvaluableMetadataCriterion;
import net.shibboleth.oidc.metadata.OIDCMetadataResolver;
-import net.shibboleth.oidc.metadata.filter.MetadataFilter;
+import net.shibboleth.oidc.metadata.cache.MetadataCache;
import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
-import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
import net.shibboleth.utilities.java.support.component.AbstractIdentifiableInitializableComponent;
import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
import net.shibboleth.utilities.java.support.component.ComponentSupport;
import net.shibboleth.utilities.java.support.logic.Constraint;
+import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
+import net.shibboleth.utilities.java.support.resolver.CriterionPredicateRegistry;
import net.shibboleth.utilities.java.support.resolver.ResolverException;
+import net.shibboleth.utilities.java.support.resolver.ResolverSupport;
+
-// TODO if this is generic enough it could replace AbstractMetadataResolver. Although that seems hard at this stage.
// TODO would also need to implement a generic metadata resolver interface.
-public abstract class AbstractOIDCMetadataResolver<MetadataIdentifier, MetadataType, CriteriaType>
- extends AbstractIdentifiableInitializableComponent
- implements OIDCMetadataResolver<MetadataType, CriteriaType> {
+public abstract class AbstractOIDCMetadataResolver<MetadataIdentifier, MetadataType>
+ extends AbstractIdentifiableInitializableComponent implements OIDCMetadataResolver<MetadataType> {
/** Class logger. */
@@ -37,12 +57,16 @@ public abstract class AbstractOIDCMetadataResolver<MetadataIdentifier, MetadataT
/** Logging prefix. */
private String logPrefix;
- // TODO does this need synchronisation?
- /** Backing store for runtime metadata.*/
- @NonnullAfterInit private BackingStore<MetadataIdentifier, MetadataType> backingStore;
+ /** The metadata cache.*/
+ @Nonnull private final MetadataCache<MetadataType> cache;
+
+
+ /** Flag which determines whether predicates used in filtering are connected by
+ * a logical 'OR' (true) or by logical 'AND' (false). Defaults to false. */
+ private boolean satisfyAnyPredicates;
- /** Filter applied to all metadata. */
- private MetadataFilter<MetadataType> mdFilter;
+ /** Registry used in resolving predicates from criteria. */
+ @NonnullAfterInit private CriterionPredicateRegistry<MetadataType> criterionPredicateRegistry;
/**
* Whether problems during initialization should cause the provider to fail or go on without metadata. The
@@ -51,8 +75,9 @@ public abstract class AbstractOIDCMetadataResolver<MetadataIdentifier, MetadataT
private boolean failFastInitialization;
/** Constructor.*/
- protected AbstractOIDCMetadataResolver() {
+ protected AbstractOIDCMetadataResolver(@Nonnull final MetadataCache<MetadataType> metadataCache) {
failFastInitialization = true;
+ cache = Constraint.isNotNull(metadataCache, "Metadata cache can not be null");
}
@@ -69,35 +94,113 @@ public abstract class AbstractOIDCMetadataResolver<MetadataIdentifier, MetadataT
}
/**
- * Gets the metadata filter applied to the metadata.
+ * Filter the supplied candidates by resolving predicates from the supplied criteria and applying
+ * the predicates to return a filtered {@link Iterable}.
*
- * @return the metadata filter applied to the metadata
+ * @param candidates the candidates to evaluate
+ * @param criteria the criteria set to evaluate
+ * @param onEmptyPredicatesReturnEmpty if true and no predicates are supplied, then return an empty iterable;
+ * otherwise return the original input candidates
+ *
+ * @return an iterable of the candidates filtered by the resolved predicates
+ *
+ * @throws ResolverException if there is a fatal error during resolution
*/
- @Nullable public MetadataFilter<MetadataType> getMetadataFilter() {
- return mdFilter;
+ protected Iterable<MetadataType> predicateFilterCandidates(@Nonnull final Iterable<MetadataType> candidates,
+ @Nonnull final CriteriaSet criteria, final boolean onEmptyPredicatesReturnEmpty)
+ throws ResolverException {
+
+ if (!candidates.iterator().hasNext()) {
+ log.debug("{} Candidates iteration was empty, nothing to filter via predicates", getLogPrefix());
+ return Collections.emptySet();
+ }
+
+ log.debug("{} Attempting to filter candidate metadata via resolved Predicates", getLogPrefix());
+
+ // TODO: The criterion should be a subtype of AbstractEvaluableMetadataCriterion to avoid errors.
+ @SuppressWarnings("unchecked") final Set<Predicate<MetadataType>> predicates =
+ ResolverSupport.getPredicates(criteria, EvaluableMetadataCriterion.class, getCriterionPredicateRegistry());
+
+ log.trace("{} Resolved {} Predicates: {}", getLogPrefix(), predicates.size(), predicates);
+
+ final boolean satisfyAny;
+ final SatisfyAnyCriterion satisfyAnyCriterion = criteria.get(SatisfyAnyCriterion.class);
+ if (satisfyAnyCriterion != null) {
+ log.trace("{} CriteriaSet contained SatisfyAnyCriterion", getLogPrefix());
+ satisfyAny = satisfyAnyCriterion.isSatisfyAny();
+ } else {
+ log.trace("{} CriteriaSet did NOT contain SatisfyAnyCriterion", getLogPrefix());
+ satisfyAny = isSatisfyAnyPredicates();
+ }
+
+ log.trace("{} Effective satisyAny value: {}", getLogPrefix(), satisfyAny);
+
+ final Iterable<MetadataType> result =
+ ResolverSupport.getFilteredIterable(candidates, predicates, satisfyAny, onEmptyPredicatesReturnEmpty);
+ if (log.isDebugEnabled()) {
+ log.debug("{} After predicate filtering {} Metadata entities remain",
+ getLogPrefix(), Iterables.size(result));
+ }
+ return result;
}
+
+ /**
+ * Get the flag indicating whether resolved credentials may satisfy any predicates
+ * (i.e. connected by logical 'OR') or all predicates (connected by logical 'AND').
+ *
+ * <p>Defaults to false.</p>
+ *
+ * @return true if must satisfy all, false otherwise
+ */
+ public boolean isSatisfyAnyPredicates() {
+ return satisfyAnyPredicates;
+ }
/**
- * Set the entity backing store currently in use by the metadata resolver.
+ * Set the flag indicating whether resolved credentials may satisfy any predicates
+ * (i.e. connected by logical 'OR') or all predicates (connected by logical 'AND').
+ *
+ * <p>Defaults to false.</p>
*
- * @param newBackingStore the new entity backing store
+ * @param flag true if must satisfy all, false otherwise
*/
- protected void setBackingStore(@Nonnull final BackingStore<MetadataIdentifier, MetadataType> newBackingStore) {
+ public void setSatisfyAnyPredicates(final boolean flag) {
ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
-
- backingStore = Constraint.isNotNull(newBackingStore, "BackingStore may not be null");
+ satisfyAnyPredicates = flag;
}
/**
- * Get the EntityDescriptor backing store currently in use by the metadata resolver.
+ * Get the registry used in resolving predicates from criteria.
*
- * @return the current effective entity backing store
+ * @return the effective registry instance used
*/
- @Nonnull protected BackingStore<MetadataIdentifier, MetadataType> getBackingStore() {
- return backingStore;
+ @NonnullAfterInit public CriterionPredicateRegistry<MetadataType> getCriterionPredicateRegistry() {
+ return criterionPredicateRegistry;
}
+
+ /**
+ * Set the registry used in resolving predicates from criteria.
+ *
+ * @param registry the registry instance to use
+ */
+ public void setCriterionPredicateRegistry(@Nullable final CriterionPredicateRegistry<MetadataType> registry) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+ ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+ criterionPredicateRegistry = registry;
+ }
+
+
+ /**
+ * Get the metadata cache currently in use by the metadata resolver.
+ *
+ * @return the current effective entity backing store
+ */
+ @Nonnull protected MetadataCache<MetadataType> getCache() {
+ return cache;
+ }
+
/**
* Gets whether problems during initialization should cause the provider to fail or go on without metadata. The
@@ -137,13 +240,17 @@ public abstract class AbstractOIDCMetadataResolver<MetadataIdentifier, MetadataT
log.error("{} Metadata provider failed to properly initialize, fail-fast=false, "
+ "continuing on in a degraded state", getLogPrefix(), e);
}
- }
+ }
+
+ @Override protected void doDestroy() {
+ log.warn("Destroying");
+ }
/** Initialise this metadata provider. Subclasses will need to override this method.*/
protected abstract void initMetadataResolver() throws ComponentInitializationException;
/** {@inheritDoc} */
- @Override @Nullable public MetadataType resolveSingle(final CriteriaType criteria) throws ResolverException {
+ @Override @Nullable public MetadataType resolveSingle(final CriteriaSet criteria) throws ResolverException {
ComponentSupport.ifNotInitializedThrowUninitializedComponentException(this);
ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
@@ -157,83 +264,4 @@ public abstract class AbstractOIDCMetadataResolver<MetadataIdentifier, MetadataT
return null;
}
- /**
- * Get list of metadata matching an identifier.
- *
- * @param indentifier indentifier to lookup
- * @return a list of metadata
- * @throws ResolverException if an error occurs
- */
- @Nonnull @NonnullElements protected List<MetadataType> lookupIdentifier(@Nonnull @NotEmpty final MetadataIdentifier identifier)
- throws ResolverException {
- if (!isInitialized()) {
- throw new ResolverException("Metadata resolver has not been initialized");
- }
-
- if (identifier == null) { // add a null check? || Strings.isNullOrEmpty(identifier)) {
- log.debug("{} Identifier was null or empty, skipping search for it", getLogPrefix());
- return Collections.emptyList();
- }
-
- final List<MetadataType> allMetadata = lookupIndexedIdentifier(identifier);
- if (allMetadata.isEmpty()) {
- log.debug("{} Metadata backing store does not contain any metadata with the ID: {}",
- getLogPrefix(), identifier);
- return allMetadata;
- }
-
- // TODO do isValid via strategy function?
-// final Iterator<MetadataType> metadataIter = allMetadata.iterator();
-// while (metadataIter.hasNext()) {
-// final MetadataType metadata = metadataIter.next();
-//
-// if (!isValid(metadata)) {
-// log.warn("{} Metadata backing store contained metadata with the ID: {}, "
-// + " but it was no longer valid", getLogPrefix(), identifier);
-// metadataIter.remove();
-// }
-// }
-
- return allMetadata;
- }
-
- /**
- * Remove from the backing store all metadata for the entity with the given identifier.
- *
- * @param identifier the ID of the metadata to remove
- * @param backingStore the backing store instance to update
- */
- protected void removeByIdentifier(@Nonnull final MetadataIdentifier identifier,
- @Nonnull final BackingStore<MetadataIdentifier, MetadataType> backingStore) {
- final Map<MetadataIdentifier, List<MetadataType>> indexedDescriptors = backingStore.getIndexedDescriptors();
- final List<MetadataType> descriptors = indexedDescriptors.get(identifier);
- if (descriptors != null) {
- backingStore.getOrderedDescriptors().removeAll(descriptors);
- }
- indexedDescriptors.remove(identifier);
- }
-
- /**
- * Lookup the specified identifier from the index. The returned list will be a copy of what is stored in the backing
- * index, and so the list is safe to be manipulated by callers.
- *
- * @param identifier the entityID to lookup
- *
- * @return list copy of indexed metadata, may be empty, will never be null
- */
- @Nonnull @NonnullElements protected List<MetadataType> lookupIndexedIdentifier(
- @Nonnull @NotEmpty final MetadataIdentifier identifier) {
- final List<MetadataType> metadata = getBackingStore().getIndexedDescriptors().get(identifier);
- if (metadata != null) {
- return new ArrayList<>(metadata);
- }
- return Collections.emptyList();
- }
-
- /** Create a new backing store.*/
- @Nonnull protected BackingStore<MetadataIdentifier, MetadataType> createNewBackingStore() {
- return new DefaultBackingStore<>();
- }
-
-
}
diff --git a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/DefaultBackingStore.java b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/DefaultBackingStore.java
index b4dcfce..a2a7460 100644
--- a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/DefaultBackingStore.java
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/DefaultBackingStore.java
@@ -1,14 +1,41 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements. See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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.oidc.metadata.impl;
+import java.time.Duration;
+import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import javax.annotation.Nonnull;
+import javax.annotation.concurrent.ThreadSafe;
import net.shibboleth.oidc.metadata.BackingStore;
+import net.shibboleth.oidc.metadata.MetadataManagementData;
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
+import net.shibboleth.utilities.java.support.annotation.constraint.NotLive;
+import net.shibboleth.utilities.java.support.annotation.constraint.Unmodifiable;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+ at ThreadSafe
public class DefaultBackingStore<I, T> implements BackingStore<I, T> {
/** Index of entity IDs to their descriptors. */
@@ -17,20 +44,61 @@ public class DefaultBackingStore<I, T> implements BackingStore<I, T> {
/** Ordered list of entity descriptors. */
private List<T> orderedDescriptors;
- /** Constructor.*/
- public DefaultBackingStore() {
+ /** Map holding management data for each entityID. */
+ private final Map<I, MetadataManagementData<I>> mgmtDataMap;
+
+ /** The maximum cache duration for metadata.*/
+ @Nonnull private final Duration maxCacheDuration;
+
+ /**
+ * Constructor.
+ *
+ * @param cacheDuration TODO should this be here?
+ */
+ public DefaultBackingStore(@Nonnull final Duration cacheDuration) {
+ super();
+ maxCacheDuration = Constraint.isNotNull(cacheDuration,"Max cache duration can not be null");
indexedDescriptors = new ConcurrentHashMap<>();
- orderedDescriptors = new ArrayList<>();
+ orderedDescriptors = new ArrayList<>();
+ mgmtDataMap = new ConcurrentHashMap<>();
}
@Override
- @Nonnull public Map<I, List<T>> getIndexedDescriptors() {
+ @Nonnull public Map<I, List<T>> getIndexedValues() {
return indexedDescriptors;
}
@Override
- @Nonnull public List<T> getOrderedDescriptors() {
+ @Nonnull public List<T> getOrderedValues() {
return orderedDescriptors;
}
+
+ @Override
+ public MetadataManagementData<I> computeManagementDataIfAbsent(@Nonnull final I identifier) {
+ Constraint.isNotNull(identifier, "identifier may not be null");
+ Constraint.isNotNull(maxCacheDuration, "Max cache duration can not be null");
+
+ return mgmtDataMap.computeIfAbsent(identifier, id -> {
+ final Instant now = Instant.now();
+ final MetadataManagementData<I> mgmt = new MetadataManagementData<>(id);
+ mgmt.setRefreshTriggerTime(now.plus(maxCacheDuration));
+ return mgmt;
+ });
+
+ }
+
+ @Override
+ //TODO is concurrent hashmap threadsafe for remove and get - do we need the synchronized
+ public synchronized void removeManagementData(@Nonnull final I identifier) {
+ Constraint.isNotNull(identifier, "Identifier may not be null");
+ mgmtDataMap.remove(identifier);
+
+ }
+
+ @Override
+ @Nonnull @NonnullElements @Unmodifiable @NotLive
+ public synchronized Set<I> getManagementDataIdentifiers() {
+ return Set.copyOf(mgmtDataMap.keySet());
+ }
}
diff --git a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/DefaultDynamicBackingStore.java b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/DefaultDynamicBackingStore.java
deleted file mode 100644
index 3624807..0000000
--- a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/DefaultDynamicBackingStore.java
+++ /dev/null
@@ -1,48 +0,0 @@
-package net.shibboleth.oidc.metadata.impl;
-
-import java.time.Duration;
-import java.util.Map;
-import java.util.Objects;
-import java.util.concurrent.ConcurrentHashMap;
-
-import javax.annotation.Nonnull;
-import javax.annotation.concurrent.ThreadSafe;
-
-import net.shibboleth.oidc.metadata.DynamicBackingStore;
-import net.shibboleth.oidc.metadata.MetadataManagementData;
-import net.shibboleth.utilities.java.support.logic.Constraint;
-
- at ThreadSafe
-public class DefaultDynamicBackingStore<Identifier, Type> extends DefaultBackingStore<Identifier, Type>
- implements DynamicBackingStore<Identifier, Type> {
-
- /** Map holding management data for each entityID. */
- private final Map<Identifier, MetadataManagementData<Identifier>> mgmtDataMap;
-
- /** The maximum cache duration for metadata.*/
- @Nonnull private final Duration maxCacheDuration;
-
- /**
- *
- * Constructor.
- *
- * @param cacheDuration TODO should this be here?
- */
- public DefaultDynamicBackingStore(@Nonnull final Duration cacheDuration) {
- super();
- maxCacheDuration = Objects.requireNonNull(cacheDuration,"Max cache duration can not be null");
- mgmtDataMap = new ConcurrentHashMap<>();
- }
-
- @Override
- public MetadataManagementData<Identifier> computeManagementDataIfAbsent(@Nonnull final Identifier identifier) {
- Constraint.isNotNull(identifier, "identifier may not be null");
-
- return mgmtDataMap.computeIfAbsent(identifier, id ->
- new MetadataManagementData<Identifier>(id, maxCacheDuration));
-
- }
-
-
-
-}
diff --git a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/DynamicOIDCProviderMetadataResolver.java b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/DynamicOIDCProviderMetadataResolver.java
new file mode 100644
index 0000000..05be062
--- /dev/null
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/DynamicOIDCProviderMetadataResolver.java
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements. See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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.oidc.metadata.impl;
+
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+
+import com.nimbusds.oauth2.sdk.id.Issuer;
+import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
+
+import net.shibboleth.oidc.metadata.ProviderMetadataResolver;
+import net.shibboleth.oidc.metadata.cache.MetadataCache;
+import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
+
+/** Concrete metadata resolver for dynamic OIDC resolution.*/
+public class DynamicOIDCProviderMetadataResolver
+ extends AbstractDynamicOIDCMetadataResolver<Issuer, OIDCProviderMetadata>
+ implements ProviderMetadataResolver {
+
+ protected DynamicOIDCProviderMetadataResolver(
+ @Nonnull final MetadataCache<OIDCProviderMetadata> metadataCache,
+ @Nonnull final Function<CriteriaSet, OIDCProviderMetadata> fetchingStrategy) {
+ super(metadataCache, fetchingStrategy);
+
+ }
+
+}
diff --git a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/HTTPProviderConfigurationMetadataResolver.java b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/HTTPProviderConfigurationFetchingStrategy.java
similarity index 57%
rename from oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/HTTPProviderConfigurationMetadataResolver.java
rename to oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/HTTPProviderConfigurationFetchingStrategy.java
index 4345015..4e37204 100644
--- a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/HTTPProviderConfigurationMetadataResolver.java
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/HTTPProviderConfigurationFetchingStrategy.java
@@ -1,10 +1,6 @@
package net.shibboleth.oidc.metadata.impl;
import java.io.IOException;
-import java.time.Instant;
-import java.util.Collections;
-import java.util.List;
-import java.util.Objects;
import java.util.Set;
import java.util.function.BiFunction;
@@ -30,22 +26,15 @@ import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
import net.shibboleth.oidc.metadata.criterion.IssuerIDCriterion;
import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
-import net.shibboleth.utilities.java.support.component.ComponentSupport;
-import net.shibboleth.utilities.java.support.logic.Constraint;
import net.shibboleth.utilities.java.support.net.MediaTypeSupport;
import net.shibboleth.utilities.java.support.primitive.StringSupport;
import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
import net.shibboleth.utilities.java.support.resolver.ResolverException;
-/**
- * A dynamic {@link OIDCProviderMetadata metadata} provider that HTTP GETs an OpenID Connect provider's
- * JSON configuration information from its well-known location. The well-known location is derived
- * by default by concatenating the string {@literal /.well-known/openid-configuration} to the Issuer of
- * the given query - although this is configurable.
- * See https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig.
- */
-public class HTTPProviderConfigurationMetadataResolver
- extends AbstractDynamicOIDCHTTPProviderMetadataResolver<Issuer, OIDCProviderMetadata, CriteriaSet> {
+
+ at ThreadSafe //? is a singleton?
+public class HTTPProviderConfigurationFetchingStrategy
+ extends AbstractDynamicHTTPFetchingStrategy<CriteriaSet, OIDCProviderMetadata> {
/** The returned content_type, must be application/json see openid-connect-discovery section 4.*/
@Nonnull private static final MediaType CONTENT_TYPE = MediaType.JSON_UTF_8;
@@ -55,96 +44,33 @@ public class HTTPProviderConfigurationMetadataResolver
private static final String DEFAULT_OPENID_PROVIDER_WELL_KNOWN_PATH = "/.well-known/openid-configuration";
/** Class logger. */
- @Nonnull private final Logger log = LoggerFactory.getLogger(HTTPProviderConfigurationMetadataResolver.class);
+ @Nonnull private final Logger log = LoggerFactory.getLogger(HTTPProviderConfigurationFetchingStrategy.class);
/** The well-known path for OpenID Provider metadata. */
@Nonnull @NotEmpty private String wellKnownPath;
/** Strategy for composing an issuer with the well-known configuration path.*/
@Nonnull private BiFunction<Issuer, String, String> wellKnownLocationCompositionStrategy;
-
- /**
- *
- * Constructor.
- *
- * @param client the instance of {@link HttpClient} used to fetch remote OIDC metadata
- * @param handler the response type handler use to convert the HTTP response to an {@link OIDCProviderMetadata} instance.
- */
- public HTTPProviderConfigurationMetadataResolver(@Nonnull final HttpClient client) {
- // The response handler is fixed to that provided by this resolver.
- super(client, new OIDCProviderMetadataResponseHandler());
+
+ protected HTTPProviderConfigurationFetchingStrategy(@Nonnull final HttpClient client,
+ @Nonnull final ResponseHandler<OIDCProviderMetadata> handler) {
+ super(client, handler);
wellKnownPath = DEFAULT_OPENID_PROVIDER_WELL_KNOWN_PATH;
wellKnownLocationCompositionStrategy = new DefaultWellKnownPathCompositionStrategy();
- setSupportedContentTypes(List.of(CONTENT_TYPE.toString()));
- }
-
- /**
- * Set the strategy for composing a string URL to fetch the provider's configuration information.
- * Derived from the issuer and the well-known path.
- *
- * @param strategy the strategy.
- */
- public void setWellKnownLocationCompositionStrategy(@Nonnull final
- BiFunction<Issuer, String, String> strategy) {
- ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
- ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
-
- this.wellKnownLocationCompositionStrategy =
- Constraint.isNotNull(strategy, "WellKnownLocationCompositionStrategy can not be null");
}
-
- /**
- * Set the well-known provider configuration path.
- *
- * @param path the well-known path.
- */
- public void setWellKnownPath(@Nonnull @NotEmpty final String path) {
- ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
- ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
-
- this.wellKnownPath = Constraint.isNotEmpty(path, wellKnownPath);
- }
-
+
@Override
- @Nullable protected String buildRequestURL(CriteriaSet criteria) {
+ protected String buildRequestURL(@Nonnull final CriteriaSet criteria) {
if (criteria.contains(IssuerIDCriterion.class)) {
final String url = wellKnownLocationCompositionStrategy
.apply(criteria.get(IssuerIDCriterion.class).getIssuerID(),wellKnownPath);
- log.debug("{} URL generated by request builder was: {}", getLogPrefix(), url);
+ log.debug("URL generated by request builder was: {}", url);
return url;
}
return null;
}
-
-
- @Override
- protected Set<Issuer> resolveIdentifiers(@Nonnull final CriteriaSet criteria) {
- final IssuerIDCriterion issuerIDCriterion = criteria.get(IssuerIDCriterion.class);
- if (issuerIDCriterion != null) {
- log.debug("{} Found issuer in criteria: {}", getLogPrefix(), issuerIDCriterion.getIssuerID());
- return Collections.singleton(issuerIDCriterion.getIssuerID());
- }
- log.debug("{} No issuerIDs resolved.", getLogPrefix());
- return Collections.emptySet();
- }
- /**
- * {@inheritDoc}
- * OIDC Provider configuration information does not expire. There is no specification that describes that.
- *
- * TODO maybe based on the HTTP response headers?
- */
- @Override
- protected Instant computeExpirationTime(@Nonnull final OIDCProviderMetadata metadata, @Nonnull final Instant now) {
- return now.plus(getMaxCacheDuration());
- }
-
- @Override
- protected Issuer extractIdentifier(@Nonnull final OIDCProviderMetadata metadata) {
- return metadata.getIssuer();
- }
-
/** Default strategy for composing a well-known URL to fetch a provider's configuration document from.*/
@Immutable
@ThreadSafe
@@ -235,40 +161,4 @@ public class HTTPProviderConfigurationMetadataResolver
}
- @Override
- @Nonnull protected boolean isNewMetadataValid(@Nonnull final OIDCProviderMetadata metadata,
- @Nonnull final Issuer expectedIdentifier) throws ResolverException {
-
- if (!Objects.equals(metadata.getIssuer(), expectedIdentifier)) {
- log.warn("{} New metadata's issuer '{}' does not match expected issuer '{}', will not process",
- getLogPrefix(), metadata.getIssuer(), expectedIdentifier);
- return false;
- }
- return true;
- }
-
- /**
- * {@inheritDoc}
- *
- * No-op method for this resolver.
- */
- @Override
- @Nonnull protected OIDCProviderMetadata prepareForFiltering(@Nonnull final OIDCProviderMetadata input) {
- //do nothing, just return
- return input;
- }
-
- /**
- * {@inheritDoc}
- *
- * No-op method for this resolver.
- */
- @Override
- @Nonnull protected void finalizeMetadataProcessing(@Nonnull final OIDCProviderMetadata input) {
- //do nothing, just return
- return;
- }
-
-
-
}
diff --git a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/ReloadingProviderMetadataProvider.java b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/ReloadingProviderMetadataProvider.java
index 9e13bf8..f23f19c 100644
--- a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/ReloadingProviderMetadataProvider.java
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/ReloadingProviderMetadataProvider.java
@@ -42,7 +42,7 @@ public class ReloadingProviderMetadataProvider extends AbstractIdentifiableIniti
}
@Override
- public Iterable<OIDCProviderMetadata> resolve(CriteriaSet criteria) throws ResolverException {
+ public Iterable<OIDCProviderMetadata> resolve(@Nonnull final CriteriaSet criteria) throws ResolverException {
ComponentSupport.ifNotInitializedThrowUninitializedComponentException(this);
ServiceableComponent<ProviderMetadataResolver> component = null;
try {
@@ -65,7 +65,7 @@ public class ReloadingProviderMetadataProvider extends AbstractIdentifiableIniti
}
@Override
- public OIDCProviderMetadata resolveSingle(CriteriaSet criteria) throws ResolverException {
+ public OIDCProviderMetadata resolveSingle(@Nonnull final CriteriaSet criteria) throws ResolverException {
ComponentSupport.ifNotInitializedThrowUninitializedComponentException(this);
ServiceableComponent<ProviderMetadataResolver> component = null;
try {
diff --git a/oidc-common-metadata-impl/src/test/java/net/shibboleth/oidc/metadata/cache/impl/DefaultMetadataCacheTest.java b/oidc-common-metadata-impl/src/test/java/net/shibboleth/oidc/metadata/cache/impl/DefaultMetadataCacheTest.java
new file mode 100644
index 0000000..fb6e320
--- /dev/null
+++ b/oidc-common-metadata-impl/src/test/java/net/shibboleth/oidc/metadata/cache/impl/DefaultMetadataCacheTest.java
@@ -0,0 +1,386 @@
+
+package net.shibboleth.oidc.metadata.cache.impl;
+
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertSame;
+import static org.testng.Assert.assertTrue;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+
+import org.opensaml.core.criterion.EntityIdCriterion;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.nimbusds.oauth2.sdk.id.Issuer;
+import com.nimbusds.openid.connect.sdk.SubjectType;
+import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
+
+import net.shibboleth.oidc.metadata.MetadataManagementData;
+import net.shibboleth.oidc.metadata.cache.MetadataCacheException;
+import net.shibboleth.oidc.metadata.criterion.IssuerIDCriterion;
+import net.shibboleth.oidc.metadata.impl.AbstractDynamicOIDCMetadataResolver;
+import net.shibboleth.oidc.metadata.impl.DefaultBackingStore;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
+
+/** Test for the MetadataCache. */
+public class DefaultMetadataCacheTest {
+
+ /** Class logger. */
+ private final Logger log = LoggerFactory.getLogger(DefaultMetadataCacheTest.class);
+
+ // OIDC provider metadata cache
+ private DefaultMetadataCache<Issuer, OIDCProviderMetadata> cache;
+
+ @BeforeMethod
+ void setup() throws Exception {
+
+ // Give our own executor, so we can manually handle the cleanup task
+ ManuallyTriggeredScheduledExecutorService scheduler = new ManuallyTriggeredScheduledExecutorService();
+ cache = new DefaultMetadataCache<>(new DefaultBackingStore<>(Duration.ofMinutes(5)), scheduler);
+ cache.setIdentifierExtractionStrategy(m -> m.getIssuer());
+ cache.setMetadataExpirationTimeStrategy((m, time) -> time.plus(Duration.ofMinutes(5)));
+ cache.setCriteriaToIdentifierStrategy(crit -> {
+ final IssuerIDCriterion issuerId = crit.get(IssuerIDCriterion.class);
+ if (issuerId != null) {
+ return issuerId.getIssuerID();
+ }
+ return null;});
+ cache.setCleanupTaskInterval(Duration.ofSeconds(100));
+
+ cache.setInitialCleanupTaskDelay(Duration.ofSeconds(1));
+ cache.setMaxIdleEntityData(Duration.ofMinutes(10));
+ cache.setRemoveIdleEntityData(true);
+ cache.setRefreshDelayFactor(0.75f);
+ cache.setMinCacheDuration(Duration.ofMinutes(10));
+ cache.setMaxCacheDuration(Duration.ofMinutes(20));
+ cache.setMetadataFilterStrategy((metadata, context) -> metadata);
+ cache.setId("MockCache");
+ // Initialise when you need to use it, if creating a local version, do not init this one.
+ //cache.initialize();
+
+ }
+
+ @Test
+ public void testBackgroundCleanup_Expired_Success() throws ComponentInitializationException, URISyntaxException, InterruptedException {
+
+ // Give our own executor, so we do not need to wait.
+ ManuallyTriggeredScheduledExecutorService scheduler = new ManuallyTriggeredScheduledExecutorService();
+ DefaultMetadataCache<Issuer, OIDCProviderMetadata> cacheLocal =
+ new DefaultMetadataCache<Issuer, OIDCProviderMetadata>(new DefaultBackingStore<>(Duration.ofMinutes(5)), scheduler);
+
+ // use a cache local to this method
+ cacheLocal.setIdentifierExtractionStrategy(m -> m.getIssuer());
+ cacheLocal.setMetadataExpirationTimeStrategy((m, time) -> time.plus(Duration.ofMinutes(10)));
+ cacheLocal.setCriteriaToIdentifierStrategy(crit -> crit.get(IssuerIDCriterion.class).getIssuerID());
+ //test a simple logging hook
+ cacheLocal.setMetadataBeforeRemovalHook((metadata, identifer) -> log.info("Before removal hook ran"));
+
+ // background task settings make no difference as it is manually scheduled.
+ cacheLocal.setCleanupTaskInterval(Duration.ofSeconds(20));
+ cacheLocal.setInitialCleanupTaskDelay(Duration.ofSeconds(1));
+
+ cacheLocal.setMaxIdleEntityData(Duration.ofSeconds(10));
+ cacheLocal.setRemoveIdleEntityData(true);
+ cacheLocal.setRefreshDelayFactor(0.75f);
+ cacheLocal.setMinCacheDuration(Duration.ofMinutes(10));
+ cacheLocal.setMaxCacheDuration(Duration.ofMinutes(20));
+ cacheLocal.setMetadataFilterStrategy((metadata, context) -> metadata);
+ cacheLocal.setId("MockCache");
+ cacheLocal.initialize();
+
+ final Issuer iss = new Issuer("https://example.oidc.op.org");
+ final MetadataManagementData<Issuer> mgmtData = cacheLocal.getBackingStore().computeManagementDataIfAbsent(iss);
+ final Instant now = Instant.now();
+ mgmtData.setLastUpdateTime(now);
+ // expire metadata
+ mgmtData.setExpirationTime(now.minus(Duration.ofMinutes(10)));
+ // refresh is OK
+ mgmtData.setRefreshTriggerTime(now.plus(Duration.ofMinutes(10)));
+
+
+ // create some metadata to add - probably ignored as needs refereshing
+ OIDCProviderMetadata metadata =
+ new OIDCProviderMetadata(iss, List.of(SubjectType.PUBLIC), new URI("http://example.oidc.op.org/metadata"));
+ cacheLocal.getBackingStore().getOrderedValues().add(metadata);
+ cacheLocal.getBackingStore().getIndexedValues().put(iss, List.of(metadata));
+
+ //should exist in the cache
+ assertFalse(cacheLocal.getBackingStore().getIndexedValues().isEmpty());
+ assertFalse(cacheLocal.getBackingStore().getOrderedValues().isEmpty());
+
+ // manually trigger the task
+ scheduler.triggerScheduledTasks();
+
+ //should no longer exist in the cache
+ assertTrue(cacheLocal.getBackingStore().getIndexedValues().isEmpty());
+ assertTrue(cacheLocal.getBackingStore().getOrderedValues().isEmpty());
+ }
+
+ /* Should fail to fetch because the cache is not properly initialized.*/
+ @Test(expectedExceptions = MetadataCacheException.class)
+ public void testCacheNotInitialized() throws Exception {
+
+ // Create but do not initialise
+ DefaultMetadataCache<Issuer, OIDCProviderMetadata> cacheLocal = new DefaultMetadataCache<>(
+ new DefaultBackingStore<>(Duration.ofMinutes(5)));
+ final Issuer iss = new Issuer("https://example.oidc.op.org");
+ cacheLocal.getOrFetchIfAbsent(new CriteriaSet(new IssuerIDCriterion(iss)), id -> {
+ try {
+ return new OIDCProviderMetadata(iss, List.of(SubjectType.PUBLIC),
+ new URI("http://example.oidc.op.org"));
+ } catch (URISyntaxException e) {
+ return null;
+ }
+ });
+ }
+
+ @Test(enabled = true)
+ public void testBackgroundCleanup_Idle_Success() throws Exception {
+
+ // Give our own executor, so we do not need to wait.
+ ManuallyTriggeredScheduledExecutorService scheduler = new ManuallyTriggeredScheduledExecutorService();
+ DefaultMetadataCache<Issuer, OIDCProviderMetadata> cacheLocal =
+ new DefaultMetadataCache<Issuer, OIDCProviderMetadata>(
+ new DefaultBackingStore<>(Duration.ofMinutes(5)), scheduler);
+
+ // use a cache local to this method
+ cacheLocal.setIdentifierExtractionStrategy(m -> m.getIssuer());
+ cacheLocal.setMetadataExpirationTimeStrategy((m, time) -> time.plus(Duration.ofMinutes(10)));
+ cacheLocal.setCriteriaToIdentifierStrategy(crit -> crit.get(IssuerIDCriterion.class).getIssuerID());
+
+ // background task settings make no difference as it is manually scheduled.
+ cacheLocal.setCleanupTaskInterval(Duration.ofSeconds(20));
+ cacheLocal.setInitialCleanupTaskDelay(Duration.ofSeconds(1));
+
+ // should be immediately idle
+ cacheLocal.setMaxIdleEntityData(Duration.ofMillis(0));
+
+ cacheLocal.setRemoveIdleEntityData(true);
+ cacheLocal.setRefreshDelayFactor(0.75f);
+ cacheLocal.setMinCacheDuration(Duration.ofMinutes(10));
+ cacheLocal.setMaxCacheDuration(Duration.ofMinutes(20));
+ cacheLocal.setMetadataFilterStrategy((metadata, context) -> metadata);
+ cacheLocal.setId("MockCache");
+ cacheLocal.initialize();
+
+
+ final Issuer iss = new Issuer("https://example.oidc.op.org");
+ final MetadataManagementData<Issuer> mgmtData = cacheLocal.getBackingStore().computeManagementDataIfAbsent(iss);
+ final Instant now = Instant.now();
+ mgmtData.setLastUpdateTime(now);
+ // metadata not expired
+ mgmtData.setExpirationTime(now.plus(Duration.ofMinutes(10)));
+ // refresh is OK
+ mgmtData.setRefreshTriggerTime(now.plus(Duration.ofMinutes(10)));
+ // Is idle for too long
+ mgmtData.recordEntityAccess();
+
+ // create some metadata to add - probably ignored as needs refereshing
+ OIDCProviderMetadata metadata =
+ new OIDCProviderMetadata(iss, List.of(SubjectType.PUBLIC), new URI("http://example.oidc.op.org/metadata"));
+ cacheLocal.getBackingStore().getOrderedValues().add(metadata);
+ cacheLocal.getBackingStore().getIndexedValues().put(iss, List.of(metadata));
+
+ // should exist in the cache
+ assertFalse(cacheLocal.getBackingStore().getIndexedValues().isEmpty());
+ assertFalse(cacheLocal.getBackingStore().getOrderedValues().isEmpty());
+
+ // manually trigger the task
+ scheduler.triggerScheduledTasks();
+
+ // should no longer exist in the cache
+ assertTrue(cacheLocal.getBackingStore().getIndexedValues().isEmpty());
+ assertTrue(cacheLocal.getBackingStore().getOrderedValues().isEmpty());
+ }
+
+
+ @Test(enabled = true)
+ public void testStaleMetadata_Success() throws Exception {
+ cache.initialize();
+
+ final Issuer iss = new Issuer("https://example.oidc.op.org");
+ final MetadataManagementData<Issuer> mgmtData = cache.getBackingStore().computeManagementDataIfAbsent(iss);
+ final Instant now = Instant.now();
+ mgmtData.setLastUpdateTime(now);
+ // expire metadata
+ mgmtData.setExpirationTime(now.minus(Duration.ofMinutes(10)));
+ // refresh is needed
+ mgmtData.setRefreshTriggerTime(now.minus(Duration.ofMinutes(10)));
+
+ // create some metadata to add - probably ignored as needs refreshing
+ OIDCProviderMetadata metadata =
+ new OIDCProviderMetadata(iss, List.of(SubjectType.PUBLIC), new URI("http://example.oidc.op.org/metadata"));
+ cache.getBackingStore().getOrderedValues().add(metadata);
+ cache.getBackingStore().getIndexedValues().put(iss, List.of(metadata));
+
+ //should exist in the cache
+ assertFalse(cache.getBackingStore().getIndexedValues().isEmpty());
+ assertFalse(cache.getBackingStore().getOrderedValues().isEmpty());
+ // should have been created now
+ assertTrue(mgmtData.getLastUpdateTime().equals(now));
+
+ cache.getOrFetchIfAbsent(new CriteriaSet(new IssuerIDCriterion(iss)), id -> {
+ try {
+ return new OIDCProviderMetadata(iss, List.of(SubjectType.PUBLIC),
+ new URI("http://example.oidc.op.org"));
+ } catch (URISyntaxException e) {
+ return null;
+ }
+ });
+
+ // should have been updated after now
+ assertFalse(mgmtData.getLastUpdateTime().equals(now));
+
+ }
+
+ @Test
+ public void testGetNotCached_Success() throws MetadataCacheException, ComponentInitializationException {
+ cache.initialize();
+ List<OIDCProviderMetadata> metadata =
+ cache.getOrFetchIfAbsent(new CriteriaSet(new IssuerIDCriterion(new Issuer("the-issuer"))), id -> {
+ try {
+ return new OIDCProviderMetadata(new Issuer("the-issuer"), List.of(SubjectType.PUBLIC),
+ new URI("http://example.com/metadata"));
+ } catch (URISyntaxException e) {
+ return null;
+ }
+ });
+ assertTrue(metadata.isEmpty() == false);
+
+ }
+
+ @Test
+ public void testGetNotCached_WrongIdentifier_Fail() throws MetadataCacheException, ComponentInitializationException {
+ cache.initialize();
+ List<OIDCProviderMetadata> metadata =
+ cache.getOrFetchIfAbsent(new CriteriaSet(new IssuerIDCriterion(new Issuer("the-issuer"))), id -> {
+ try {
+ return new OIDCProviderMetadata(new Issuer("different-issuer"), List.of(SubjectType.PUBLIC),
+ new URI("http://example.com/metadata"));
+ } catch (URISyntaxException e) {
+ return null;
+ }
+ });
+ assertTrue(metadata.isEmpty());
+
+ }
+
+ @Test
+ public void testGetNotCached_WrongCriteria_Fail() throws MetadataCacheException, ComponentInitializationException {
+ cache.initialize();
+ List<OIDCProviderMetadata> metadata =
+ cache.getOrFetchIfAbsent(new CriteriaSet(new EntityIdCriterion("wrong-criteria")), id -> {
+ try {
+ return new OIDCProviderMetadata(new Issuer("different-issuer"), List.of(SubjectType.PUBLIC),
+ new URI("http://example.com/metadata"));
+ } catch (URISyntaxException e) {
+ return null;
+ }
+ });
+ assertTrue(metadata.isEmpty());
+
+ }
+
+ @Test(enabled = true)
+ public void testMultiGet_Success() throws InterruptedException, ExecutionException,
+ MetadataCacheException, ComponentInitializationException {
+ cache.initialize();
+ final ExecutorService service = Executors.newFixedThreadPool(3);
+ Future<?> futureOne = service.submit(() -> {
+ return cache.getOrFetchIfAbsent(new CriteriaSet(new IssuerIDCriterion(new Issuer("the-issuer"))), id -> {
+ try {
+ return new OIDCProviderMetadata(new Issuer("the-issuer"), List.of(SubjectType.PUBLIC),
+ new URI("http://example.com/metadata"));
+ } catch (URISyntaxException e) {
+ return null;
+ }
+ });
+ });
+ Future<?> futureTwo = service.submit(() -> {
+ return cache.getOrFetchIfAbsent(new CriteriaSet(new IssuerIDCriterion(new Issuer("the-issuer"))), id -> {
+ try {
+ return new OIDCProviderMetadata(new Issuer("the-issuer"), List.of(SubjectType.PUBLIC),
+ new URI("http://example.com/metadata"));
+ } catch (URISyntaxException e) {
+ return null;
+ }
+ });
+ });
+ // different entity
+ Future<?> futureThree = service.submit(() -> {
+ return cache.getOrFetchIfAbsent(new CriteriaSet(new IssuerIDCriterion(new Issuer("the-issuer"))), id -> {
+ try {
+ return new OIDCProviderMetadata(new Issuer("the-issuer"), List.of(SubjectType.PUBLIC),
+ new URI("http://example.com/metadata"));
+ } catch (URISyntaxException e) {
+ return null;
+ }
+ });
+ });
+ List<?> firstMetadata = (List<?>) futureOne.get();
+ List<?> secondMetadata = (List<?>) futureThree.get();
+ List<?> thirdMetadata = (List<?>) futureTwo.get();
+
+ // non-interleaved request
+ List<OIDCProviderMetadata> provider =
+ cache.getOrFetchIfAbsent(new CriteriaSet(new IssuerIDCriterion(new Issuer("the-issuer"))), id -> {
+ try {
+ return new OIDCProviderMetadata(new Issuer("the-issuer"), List.of(SubjectType.PUBLIC),
+ new URI("http://example.com/metadata"));
+ } catch (URISyntaxException e) {
+ return null;
+ }
+ });
+ assertTrue(firstMetadata.size() == 1);
+ assertTrue(secondMetadata.size() == 1);
+ assertTrue(thirdMetadata.size() == 1);
+ assertTrue(provider.size() == 1);
+
+ // all three should have the same metadata reference
+ assertSame(firstMetadata.get(0), secondMetadata.get(0));
+ assertSame(firstMetadata.get(0), thirdMetadata.get(0));
+ assertSame(firstMetadata.get(0), provider.get(0));
+
+ }
+
+ @Test
+ public void testGetCached_Success() throws MetadataCacheException, ComponentInitializationException {
+ cache.initialize();
+ List<OIDCProviderMetadata> metadata =
+ cache.getOrFetchIfAbsent(new CriteriaSet(new IssuerIDCriterion(new Issuer("the-issuer"))), id -> {
+ try {
+ return new OIDCProviderMetadata(new Issuer("the-issuer"), List.of(SubjectType.PUBLIC),
+ new URI("http://example.com/metadata"));
+ } catch (URISyntaxException e) {
+ return null;
+ }
+ });
+ assertTrue(metadata.size() == 1);
+
+ List<OIDCProviderMetadata> metadataCached =
+ cache.getOrFetchIfAbsent(new CriteriaSet(new IssuerIDCriterion(new Issuer("the-issuer"))), id -> {
+ try {
+ return new OIDCProviderMetadata(new Issuer("the-issuer"), List.of(SubjectType.PUBLIC),
+ new URI("http://example.com/metadata"));
+ } catch (URISyntaxException e) {
+ return null;
+ }
+ });
+ assertTrue(metadata.size() == 1);
+ // first was cached, so this should be the same
+ assertTrue(metadata.get(0) == metadataCached.get(0));
+
+ }
+
+}
diff --git a/oidc-common-metadata-impl/src/test/java/net/shibboleth/oidc/metadata/cache/impl/ManuallyTriggeredScheduledExecutorService.java b/oidc-common-metadata-impl/src/test/java/net/shibboleth/oidc/metadata/cache/impl/ManuallyTriggeredScheduledExecutorService.java
new file mode 100644
index 0000000..e872c0c
--- /dev/null
+++ b/oidc-common-metadata-impl/src/test/java/net/shibboleth/oidc/metadata/cache/impl/ManuallyTriggeredScheduledExecutorService.java
@@ -0,0 +1,336 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.oidc.metadata.cache.impl;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+import javax.annotation.Nonnull;
+
+
+/**
+ * This code is copied verbatim from org.apache.flink.core.testutils.ManuallyTriggeredScheduledExecutorService
+ * <p>
+ * Simple {@link ScheduledExecutorService} implementation for testing purposes. It spawns no
+ * threads, but lets you trigger the execution of tasks manually.
+ *
+ * <p>This class is helpful when implementing tests tasks synchronous and control when they run,
+ * which would otherwise asynchronous and require complex triggers and latches to test.
+ */
+public class ManuallyTriggeredScheduledExecutorService implements ScheduledExecutorService {
+
+ private final ArrayDeque<Runnable> queuedRunnables = new ArrayDeque<>();
+
+ private final ConcurrentLinkedQueue<ScheduledTask<?>> nonPeriodicScheduledTasks =
+ new ConcurrentLinkedQueue<>();
+
+ private final ConcurrentLinkedQueue<ScheduledTask<?>> periodicScheduledTasks =
+ new ConcurrentLinkedQueue<>();
+
+ private boolean shutdown;
+
+ // ------------------------------------------------------------------------
+ // (scheduled) execution
+ // ------------------------------------------------------------------------
+
+ @Override
+ public void execute(@Nonnull Runnable command) {
+ synchronized (queuedRunnables) {
+ queuedRunnables.addLast(command);
+ }
+ }
+
+ @Override
+ public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit) {
+ return insertNonPeriodicTask(command, delay, unit);
+ }
+
+ @Override
+ public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit) {
+ return insertNonPeriodicTask(callable, delay, unit);
+ }
+
+ @Override
+ public ScheduledFuture<?> scheduleAtFixedRate(
+ Runnable command, long initialDelay, long period, TimeUnit unit) {
+ return insertPeriodicRunnable(command, initialDelay, period, unit);
+ }
+
+ @Override
+ public ScheduledFuture<?> scheduleWithFixedDelay(
+ Runnable command, long initialDelay, long delay, TimeUnit unit) {
+ return insertPeriodicRunnable(command, initialDelay, delay, unit);
+ }
+
+ // ------------------------------------------------------------------------
+ // service shutdown
+ // ------------------------------------------------------------------------
+
+ @Override
+ public void shutdown() {
+ shutdown = true;
+ }
+
+ @Override
+ public List<Runnable> shutdownNow() {
+ shutdown();
+ return Collections.emptyList();
+ }
+
+ @Override
+ public boolean isShutdown() {
+ return false;
+ }
+
+ @Override
+ public boolean isTerminated() {
+ return shutdown;
+ }
+
+ @Override
+ public boolean awaitTermination(long timeout, TimeUnit unit) {
+ return true;
+ }
+
+ // ------------------------------------------------------------------------
+ // non-implemented future task methods
+ // ------------------------------------------------------------------------
+
+ @Override
+ public <T> Future<T> submit(Callable<T> task) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public <T> Future<T> submit(Runnable task, T result) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Future<?> submit(Runnable task) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public <T> List<Future<T>> invokeAll(
+ Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public <T> T invokeAny(Collection<? extends Callable<T>> tasks) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public <T> T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) {
+ throw new UnsupportedOperationException();
+ }
+
+ // ------------------------------------------------------------------------
+ // Execution triggering and access to the queued tasks
+ // ------------------------------------------------------------------------
+
+ /**
+ * Executes all runnable and scheduled non-periodic tasks until none are left to run. This is
+ * essentially a combination of {@link #triggerAll()} and {@link
+ * #triggerNonPeriodicScheduledTasks()} that allows making a test agnostic of how exactly a
+ * runnable is passed to the executor.
+ */
+ public void triggerAllNonPeriodicTasks() {
+ while (numQueuedRunnables() > 0 || !nonPeriodicScheduledTasks.isEmpty()) {
+ triggerAll();
+ triggerNonPeriodicScheduledTasks();
+ }
+ }
+
+ /** Triggers all {@code queuedRunnables}. */
+ public void triggerAll() {
+ while (numQueuedRunnables() > 0) {
+ trigger();
+ }
+ }
+
+ /**
+ * Triggers the next queued runnable and executes it synchronously. This method throws an
+ * exception if no Runnable is currently queued.
+ */
+ public void trigger() {
+ final Runnable next;
+
+ synchronized (queuedRunnables) {
+ next = queuedRunnables.removeFirst();
+ }
+
+ next.run();
+ }
+
+ /** Gets the number of Runnables currently queued. */
+ public int numQueuedRunnables() {
+ synchronized (queuedRunnables) {
+ return queuedRunnables.size();
+ }
+ }
+
+ public Collection<ScheduledFuture<?>> getActiveScheduledTasks() {
+ final ArrayList<ScheduledFuture<?>> scheduledTasks =
+ new ArrayList<>(nonPeriodicScheduledTasks.size() + periodicScheduledTasks.size());
+ scheduledTasks.addAll(getActiveNonPeriodicScheduledTask());
+ scheduledTasks.addAll(getActivePeriodicScheduledTask());
+ return scheduledTasks;
+ }
+
+ public Collection<ScheduledFuture<?>> getActivePeriodicScheduledTask() {
+ return periodicScheduledTasks.stream()
+ .filter(scheduledTask -> !scheduledTask.isCancelled())
+ .collect(Collectors.toList());
+ }
+
+ public Collection<ScheduledFuture<?>> getActiveNonPeriodicScheduledTask() {
+ return nonPeriodicScheduledTasks.stream()
+ .filter(scheduledTask -> !scheduledTask.isCancelled())
+ .collect(Collectors.toList());
+ }
+
+ public List<ScheduledFuture<?>> getAllScheduledTasks() {
+ final ArrayList<ScheduledFuture<?>> scheduledTasks =
+ new ArrayList<>(nonPeriodicScheduledTasks.size() + periodicScheduledTasks.size());
+ scheduledTasks.addAll(getAllNonPeriodicScheduledTask());
+ scheduledTasks.addAll(getAllPeriodicScheduledTask());
+ return scheduledTasks;
+ }
+
+ public List<ScheduledFuture<?>> getAllPeriodicScheduledTask() {
+ return new ArrayList<>(periodicScheduledTasks);
+ }
+
+ public List<ScheduledFuture<?>> getAllNonPeriodicScheduledTask() {
+ return new ArrayList<>(nonPeriodicScheduledTasks);
+ }
+
+ /** Triggers all registered tasks. */
+ public void triggerScheduledTasks() {
+ triggerPeriodicScheduledTasks();
+ triggerNonPeriodicScheduledTasks();
+ }
+
+ /**
+ * Triggers a single non-periodically scheduled task.
+ *
+ * @throws NoSuchElementException If there is no such task.
+ */
+ public void triggerNonPeriodicScheduledTask() {
+ final ScheduledTask<?> poll = nonPeriodicScheduledTasks.remove();
+ if (poll != null) {
+ poll.execute();
+ }
+ }
+
+ /**
+ * Triggers all non-periodically scheduled tasks. In contrast to {@link
+ * #triggerNonPeriodicScheduledTasks()}, if such a task schedules another non-periodically
+ * schedule task, then this new task will also be triggered.
+ */
+ public void triggerNonPeriodicScheduledTasksWithRecursion() {
+ while (!nonPeriodicScheduledTasks.isEmpty()) {
+ final ScheduledTask<?> scheduledTask = nonPeriodicScheduledTasks.poll();
+
+ if (!scheduledTask.isCancelled()) {
+ scheduledTask.execute();
+ }
+ }
+ }
+
+ public void triggerNonPeriodicScheduledTasks() {
+ final Iterator<ScheduledTask<?>> iterator = nonPeriodicScheduledTasks.iterator();
+
+ while (iterator.hasNext()) {
+ final ScheduledTask<?> scheduledTask = iterator.next();
+
+ if (!scheduledTask.isCancelled()) {
+ scheduledTask.execute();
+ }
+ iterator.remove();
+ }
+ }
+
+ public void triggerPeriodicScheduledTasks() {
+ for (ScheduledTask<?> scheduledTask : periodicScheduledTasks) {
+ if (!scheduledTask.isCancelled()) {
+ scheduledTask.execute();
+ }
+ }
+ }
+
+ private ScheduledFuture<?> insertPeriodicRunnable(
+ Runnable command, long delay, long period, TimeUnit unit) {
+
+ final ScheduledTask<?> scheduledTask =
+ new ScheduledTask<>(
+ () -> {
+ command.run();
+ return null;
+ },
+ unit.convert(delay, TimeUnit.MILLISECONDS),
+ unit.convert(period, TimeUnit.MILLISECONDS));
+
+ periodicScheduledTasks.offer(scheduledTask);
+
+ return scheduledTask;
+ }
+
+ private ScheduledFuture<?> insertNonPeriodicTask(Runnable command, long delay, TimeUnit unit) {
+ return insertNonPeriodicTask(
+ () -> {
+ command.run();
+ return null;
+ },
+ delay,
+ unit);
+ }
+
+ private <V> ScheduledFuture<V> insertNonPeriodicTask(
+ Callable<V> callable, long delay, TimeUnit unit) {
+ final ScheduledTask<V> scheduledTask =
+ new ScheduledTask<>(callable, unit.convert(delay, TimeUnit.MILLISECONDS));
+
+ nonPeriodicScheduledTasks.offer(scheduledTask);
+
+ return scheduledTask;
+ }
+
+}
diff --git a/oidc-common-metadata-impl/src/test/java/net/shibboleth/oidc/metadata/cache/impl/ScheduledTask.java b/oidc-common-metadata-impl/src/test/java/net/shibboleth/oidc/metadata/cache/impl/ScheduledTask.java
new file mode 100644
index 0000000..e17c67e
--- /dev/null
+++ b/oidc-common-metadata-impl/src/test/java/net/shibboleth/oidc/metadata/cache/impl/ScheduledTask.java
@@ -0,0 +1,123 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.oidc.metadata.cache.impl;
+
+import java.util.Objects;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Delayed;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import javax.annotation.Nonnull;
+
+
+/**
+ * This code is copied verbatim from org.apache.flink.core.testutils.ScheduledTask
+ *
+ * ScheduledTask represents a task which is executed at a later point in time.
+ *
+ * @param <T> type of the result
+ */
+public class ScheduledTask<T> implements ScheduledFuture<T> {
+
+ private final Callable<T> callable;
+
+ private final long delay;
+
+ private final long period;
+
+ private final CompletableFuture<T> result;
+
+ public ScheduledTask(Callable<T> callable, long delay) {
+ this(callable, delay, 0);
+ }
+
+ public ScheduledTask(Callable<T> callable, long delay, long period) {
+ this.callable = Objects.requireNonNull(callable);
+ this.result = new CompletableFuture<>();
+ this.delay = delay;
+ this.period = period;
+ }
+
+ public boolean isPeriodic() {
+ return period > 0;
+ }
+
+ public void execute() {
+ if (!result.isDone()) {
+ if (!isPeriodic()) {
+ try {
+ result.complete(callable.call());
+ } catch (Exception e) {
+ result.completeExceptionally(e);
+ }
+ } else {
+ try {
+ callable.call();
+ } catch (Exception e) {
+ result.completeExceptionally(e);
+ }
+ }
+ }
+ }
+
+ @Override
+ public long getDelay(TimeUnit unit) {
+ return unit.convert(delay, TimeUnit.MILLISECONDS);
+ }
+
+ @Override
+ public int compareTo(Delayed o) {
+ return Long.compare(getDelay(TimeUnit.MILLISECONDS), o.getDelay(TimeUnit.MILLISECONDS));
+ }
+
+ @Override
+ public boolean cancel(boolean mayInterruptIfRunning) {
+ return result.cancel(mayInterruptIfRunning);
+ }
+
+ @Override
+ public boolean isCancelled() {
+ return result.isCancelled();
+ }
+
+ @Override
+ public boolean isDone() {
+ return result.isDone();
+ }
+
+ @Override
+ public T get() throws InterruptedException, ExecutionException {
+ return result.get();
+ }
+
+ @Override
+ public T get(long timeout, @Nonnull TimeUnit unit)
+ throws InterruptedException, ExecutionException, TimeoutException {
+ return result.get(timeout, unit);
+ }
+
+ public Callable<T> getCallable() {
+ return this.callable;
+ }
+
+}
diff --git a/oidc-common-metadata-impl/src/test/java/net/shibboleth/oidc/metadata/impl/HTTPProviderConfigurationMetadataResolverTest.java b/oidc-common-metadata-impl/src/test/java/net/shibboleth/oidc/metadata/impl/DynamicOIDCProviderMetadataResolverTest.java
similarity index 58%
rename from oidc-common-metadata-impl/src/test/java/net/shibboleth/oidc/metadata/impl/HTTPProviderConfigurationMetadataResolverTest.java
rename to oidc-common-metadata-impl/src/test/java/net/shibboleth/oidc/metadata/impl/DynamicOIDCProviderMetadataResolverTest.java
index d15b604..92fcc3f 100644
--- a/oidc-common-metadata-impl/src/test/java/net/shibboleth/oidc/metadata/impl/HTTPProviderConfigurationMetadataResolverTest.java
+++ b/oidc-common-metadata-impl/src/test/java/net/shibboleth/oidc/metadata/impl/DynamicOIDCProviderMetadataResolverTest.java
@@ -9,6 +9,10 @@ import static org.testng.Assert.assertNull;
import static org.testng.Assert.assertTrue;
import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.time.Duration;
+import java.time.Instant;
import java.util.List;
import org.apache.http.HttpHeaders;
@@ -21,20 +25,29 @@ import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.message.BasicHeader;
import org.apache.http.message.BasicHttpResponse;
import org.apache.http.protocol.HttpContext;
+import org.opensaml.saml.saml2.metadata.EntityDescriptor;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
+import com.nimbusds.oauth2.sdk.ParseException;
import com.nimbusds.oauth2.sdk.id.Issuer;
+import com.nimbusds.openid.connect.sdk.SubjectType;
import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
+import net.shibboleth.oidc.metadata.AbstractEvaluableMetadataCriterion;
+import net.shibboleth.oidc.metadata.EvaluableMetadataCriterion;
+import net.shibboleth.oidc.metadata.MetadataManagementData;
+import net.shibboleth.oidc.metadata.cache.impl.DefaultMetadataCache;
+import net.shibboleth.oidc.metadata.cache.impl.MetadataCacheBuilder;
+import net.shibboleth.oidc.metadata.cache.impl.OIDCProviderMetadataCacheFactoryBean;
import net.shibboleth.oidc.metadata.criterion.IssuerIDCriterion;
-import net.shibboleth.oidc.metadata.impl.HTTPProviderConfigurationMetadataResolver.OIDCProviderMetadataResponseHandler;
+import net.shibboleth.oidc.metadata.impl.HTTPProviderConfigurationFetchingStrategy.OIDCProviderMetadataResponseHandler;
import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
import net.shibboleth.utilities.java.support.resolver.ResolverException;
-/** Tests for the {@link HTTPProviderConfigurationMetadataResolver} .*/
-public class HTTPProviderConfigurationMetadataResolverTest {
+/** Tests for the {@link DynamicOIDCProviderMetadataResolver} .*/
+public class DynamicOIDCProviderMetadataResolverTest {
private final String GOOD_PROVIDER_CONFIGURATION_INFO = "{\n"
+ "\"issuer\": \"https://example.oidc.op.org\",\n"
@@ -95,9 +108,13 @@ public class HTTPProviderConfigurationMetadataResolverTest {
+ "]\n"
+ "}";
- private HTTPProviderConfigurationMetadataResolver resolver;
+ private DynamicOIDCProviderMetadataResolver resolver;
private HttpClient httpClient;
+
+ private DefaultMetadataCache<Issuer, OIDCProviderMetadata> cache;
+
+ private MetadataCacheBuilder<Issuer, OIDCProviderMetadata> builder;
@SuppressWarnings("unchecked")
@BeforeMethod
@@ -110,9 +127,28 @@ public class HTTPProviderConfigurationMetadataResolverTest {
execute(any(HttpUriRequest.class),any(ResponseHandler.class),any(HttpContext.class)))
.thenReturn(OIDCProviderMetadata.parse(GOOD_PROVIDER_CONFIGURATION_INFO));
- resolver = new HTTPProviderConfigurationMetadataResolver(httpClient);
+ builder = new OIDCProviderMetadataCacheFactoryBean();
+ builder.setSingleton(true);
+
+ //setup mock functions
+ builder.setIdentifierExtractionStrategy(m -> m.getIssuer());
+ builder.setMetadataExpirationTimeStrategy((m, time) -> time.plus(Duration.ofMinutes(15)) );
+ builder.setCriteriaToIdentifierStrategy(c -> c.get(IssuerIDCriterion.class).getIssuerID());
+ builder.setMinCacheDuration(Duration.ofMinutes(5));
+ builder.setMaxCacheDuration(Duration.ofMinutes(15));
+ builder.setInitialCleanupTaskDelay(Duration.ofSeconds(1));
+
+ builder.afterPropertiesSet();
+ cache = builder.getObject();
+
+ final HTTPProviderConfigurationFetchingStrategy fetchingStrategy =
+ new HTTPProviderConfigurationFetchingStrategy(httpClient, new OIDCProviderMetadataResponseHandler());
+ fetchingStrategy.setId("Mock HTTP Fetching Strategy");
+ fetchingStrategy.initialize();
+
+ resolver = new DynamicOIDCProviderMetadataResolver(cache, fetchingStrategy);
resolver.setId("mockHttpOIDCProvider");
- resolver.setSupportedContentTypes(List.of("application/json"));
+ //resolver.setMetadataFetchingStrategy(fetchingStrategy);
resolver.initialize();
}
@@ -124,13 +160,6 @@ public class HTTPProviderConfigurationMetadataResolverTest {
}
}
- @Test
- void testFetch_Success() throws ResolverException, IOException {
- OIDCProviderMetadata metadata = resolver
- .fetchFromOriginSource(new CriteriaSet(new IssuerIDCriterion(new Issuer("https://example.oidc.op.org"))));
- assertNotNull(metadata);
- assertTrue("https://example.oidc.op.org".equals(metadata.getIssuer().getValue()));
- }
@Test
void testResolve_Success() throws ResolverException, IOException {
@@ -140,6 +169,83 @@ public class HTTPProviderConfigurationMetadataResolverTest {
assertTrue(found.iterator().hasNext());
}
+ @Test
+ void testResolve_Filter_Success() throws ResolverException, IOException {
+ Iterable<OIDCProviderMetadata> found =
+ resolver.resolve(new CriteriaSet(
+ new IssuerIDCriterion(new Issuer("https://example.oidc.op.org")),
+ new AlwaysFilterEvaluableMetadataCriterion(OIDCProviderMetadata.class, true)));
+ assertNotNull(found);
+ //was filtered
+ assertTrue(found.iterator().hasNext() == false);
+ }
+
+ @Test
+ void testResolve_MetadataNeedsRefresh_Success() throws ResolverException, IOException, ParseException {
+
+ final Issuer iss = new Issuer("https://example.oidc.op.org");
+ final MetadataManagementData<Issuer> mgmtData = cache.getBackingStore()
+ .computeManagementDataIfAbsent(iss);
+ final Instant now = Instant.now();
+ mgmtData.setLastUpdateTime(now);
+ //metadata not expired
+ mgmtData.setExpirationTime(now.plus(Duration.ofMinutes(1)));
+ //refresh please
+ mgmtData.setRefreshTriggerTime(now.minus(Duration.ofMinutes(1)));
+
+ //create some metadata to add - probably ignored as needs refereshing
+ OIDCProviderMetadata metadata = OIDCProviderMetadata.parse(GOOD_PROVIDER_CONFIGURATION_INFO);
+ cache.getBackingStore().getOrderedValues().add(metadata);
+ cache.getBackingStore().getIndexedValues().put(iss, List.of(metadata));
+
+ Iterable<OIDCProviderMetadata> found =
+ resolver.resolve(new CriteriaSet(
+ new IssuerIDCriterion(iss)));
+ assertNotNull(found);
+ assertTrue(found.iterator().hasNext());
+ }
+
+ @Test
+ void testResolve_Filter_WrongType_Fail() throws ResolverException, IOException {
+ Iterable<OIDCProviderMetadata> found =
+ resolver.resolve(new CriteriaSet(
+ new IssuerIDCriterion(new Issuer("https://example.oidc.op.org")),
+ new WrongTypeEvaluableMetadataCriterion(EntityDescriptor.class, true)));
+ assertNotNull(found);
+ //was filtered
+ assertTrue(found.iterator().hasNext());
+ }
+
+ /* Supports OIDCProviderMetadata, so will filter it.*/
+ class AlwaysFilterEvaluableMetadataCriterion extends AbstractEvaluableMetadataCriterion<OIDCProviderMetadata> {
+
+ protected AlwaysFilterEvaluableMetadataCriterion(final Class<OIDCProviderMetadata> claz,
+ final boolean defaultResult) {
+ super(claz, defaultResult);
+ }
+
+ @Override
+ public boolean doTest(final OIDCProviderMetadata metadata) {
+ return false;
+ }
+
+ }
+
+ /* Does not support OIDCProviderMetadata, should just return the default.*/
+ class WrongTypeEvaluableMetadataCriterion extends AbstractEvaluableMetadataCriterion<EntityDescriptor> {
+
+ protected WrongTypeEvaluableMetadataCriterion(final Class<EntityDescriptor> claz,
+ final boolean defaultResult) {
+ super(claz, defaultResult);
+ }
+
+ @Override
+ public boolean doTest(final EntityDescriptor metadata) {
+ return false;
+ }
+
+ }
+
@Test
void testResponseHandler_Success() throws IOException {
final OIDCProviderMetadataResponseHandler handler = new OIDCProviderMetadataResponseHandler();
@@ -171,7 +277,7 @@ public class HTTPProviderConfigurationMetadataResolverTest {
void testResolve_FromCache_Success() throws ResolverException, IOException {
// test not in cache
- assertFalse(resolver.getBackingStore().getIndexedDescriptors().containsKey(new Issuer("https://example.oidc.op.org")));
+ assertFalse(cache.getBackingStore().getIndexedValues().containsKey(new Issuer("https://example.oidc.op.org")));
// find and cache
Iterable<OIDCProviderMetadata> found =
@@ -180,7 +286,7 @@ public class HTTPProviderConfigurationMetadataResolverTest {
assertTrue(found.iterator().hasNext());
// test is in cache
- assertTrue(resolver.getBackingStore().getIndexedDescriptors().containsKey(new Issuer("https://example.oidc.op.org")));
+ assertTrue(cache.getBackingStore().getIndexedValues().containsKey(new Issuer("https://example.oidc.op.org")));
// Take down the source and see if it still resolves from the cache.
@@ -204,5 +310,7 @@ public class HTTPProviderConfigurationMetadataResolverTest {
assertNotNull(found);
assertFalse(found.iterator().hasNext());
}
+
+
}
diff --git a/oidc-common-metadata-impl/src/test/resources/logback-test.xml b/oidc-common-metadata-impl/src/test/resources/logback-test.xml
index 0769b60..39274d0 100644
--- a/oidc-common-metadata-impl/src/test/resources/logback-test.xml
+++ b/oidc-common-metadata-impl/src/test/resources/logback-test.xml
@@ -6,7 +6,7 @@
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
- <pattern>%level [%logger:%line] - %msg%n</pattern>
+ <pattern>%level [%logger:%line] [%t] - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list