[java-oidc-common] 01/02: Basic generic dynamic OIDC provider metadata resolver functionality

Phil Smart philip.smart at jisc.ac.uk
Thu Sep 2 15:40:52 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=ec3b52d7aa31a1a097952840aef6158871eb2cce

commit ec3b52d7aa31a1a097952840aef6158871eb2cce
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Thu Sep 2 13:32:17 2021 +0100

    Basic generic dynamic OIDC provider metadata resolver functionality
    
    No cache sweeper
    No resolve without identifier
    No persistent cache
    No metrics
---
 oidc-common-crypto-impl/BackingStore.java          |   5 +
 .../net/shibboleth/oidc/metadata/BackingStore.java |  31 ++
 .../oidc/metadata/DynamicBackingStore.java         |  21 +
 .../DynamicOIDCProviderMetadataResolver.java       |  12 +
 .../oidc/metadata/MetadataManagementData.java      | 137 ++++++
 .../oidc/metadata/OIDCMetadataResolver.java        |  18 +
 .../oidc/metadata/filter/FilterException.java      |  49 ++
 .../oidc/metadata/filter/MetadataFilter.java       |  46 ++
 .../metadata/filter/MetadataFilterContext.java     |  14 +
 ...actDynamicOIDCHTTPProviderMetadataResolver.java | 289 ++++++++++++
 ...bstractDynamicOIDCProviderMetadataResolver.java | 493 +++++++++++++++++++++
 .../impl/AbstractOIDCMetadataResolver.java         | 230 ++++++++++
 .../impl/AbstractReloadingOIDCEntityResolver.java  |   5 +-
 .../oidc/metadata/impl/DefaultBackingStore.java    |  36 ++
 .../metadata/impl/DefaultDynamicBackingStore.java  |  48 ++
 .../HTTPProviderConfigurationMetadataResolver.java | 239 ++++++++++
 ...PProviderConfigurationMetadataResolverTest.java | 208 +++++++++
 17 files changed, 1879 insertions(+), 2 deletions(-)

diff --git a/oidc-common-crypto-impl/BackingStore.java b/oidc-common-crypto-impl/BackingStore.java
new file mode 100644
index 0000000..ea8e9f9
--- /dev/null
+++ b/oidc-common-crypto-impl/BackingStore.java
@@ -0,0 +1,5 @@
+package net.shibboleth.oidc.metadata.impl;
+
+public interface BackingStore {
+
+}
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
new file mode 100644
index 0000000..b68b68c
--- /dev/null
+++ b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/BackingStore.java
@@ -0,0 +1,31 @@
+package net.shibboleth.oidc.metadata;
+
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+
+/**
+ * 
+ * @param <I> the identifier type
+ * @param <T> the type of object stored, referenced by the key.
+ */
+public interface BackingStore<I, T> {
+    
+    /**
+     * Get the index - mapping keys to values. 
+     * 
+     * @return the index.
+     */
+    @Nonnull public Map<I, List<T>> getIndexedDescriptors();
+    
+    /**
+     * Get the list of ordered values.
+     * 
+     * @return the list of ordered values.
+     */
+    @Nonnull public List<T> getOrderedDescriptors();
+    
+    
+
+}
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
new file mode 100644
index 0000000..c4d4403
--- /dev/null
+++ b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/DynamicBackingStore.java
@@ -0,0 +1,21 @@
+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/DynamicOIDCProviderMetadataResolver.java b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/DynamicOIDCProviderMetadataResolver.java
new file mode 100644
index 0000000..9277068
--- /dev/null
+++ b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/DynamicOIDCProviderMetadataResolver.java
@@ -0,0 +1,12 @@
+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/MetadataManagementData.java b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/MetadataManagementData.java
new file mode 100644
index 0000000..0a1fe72
--- /dev/null
+++ b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/MetadataManagementData.java
@@ -0,0 +1,137 @@
+package net.shibboleth.oidc.metadata;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+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.*/
+//TODO removed negative lookup cache.
+public class MetadataManagementData<MetadataIdentifier> {
+    
+    /** The identifier of the entity managed by this instance. */
+    private final MetadataIdentifier id;
+    
+    /** Last update time of the associated metadata. */
+    private Instant lastUpdateTime;
+    
+    /** Expiration time of the associated metadata. */
+    private Instant expirationTime;
+    
+    /** Time at which should start attempting to refresh the metadata. */
+    private Instant refreshTriggerTime;
+    
+    /** 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;
+    
+    /** 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) {
+        id = Constraint.isNotNull(identifier, "ID was null");
+        final Instant now = Instant.now();
+        expirationTime = now.plus(maxCacheDuration);
+        refreshTriggerTime = now.plus(maxCacheDuration);
+        lastAccessedTime = now;
+        readWriteLock = new ReentrantReadWriteLock(true);
+    }
+    
+    /**
+     * Get the entity ID managed by this instance.
+     * 
+     * @return the entity ID
+     */
+    @Nonnull public MetadataIdentifier getID() {
+        return id;
+    }
+    
+    /**
+     * Get the last update time of the metadata. 
+     * 
+     * @return the last update time, or null if no metadata is yet loaded for the entity
+     */
+    @Nullable public Instant getLastUpdateTime() {
+        return lastUpdateTime;
+    }
+
+    /**
+     * Set the last update time of the metadata.
+     * 
+     * @param dateTime the last update time
+     */
+    public void setLastUpdateTime(@Nonnull final Instant dateTime) {
+        lastUpdateTime = dateTime;
+    }
+    
+    /**
+     * Get the expiration time of the metadata. 
+     * 
+     * @return the expiration time
+     */
+    @Nonnull public Instant getExpirationTime() {
+        return expirationTime;
+    }
+
+    /**
+     * Set the expiration time of the metadata.
+     * 
+     * @param dateTime the new expiration time
+     */
+    public void setExpirationTime(@Nonnull final Instant dateTime) {
+        expirationTime = Constraint.isNotNull(dateTime, "Expiration time may not be null");
+    }
+    
+    /**
+     * Get the refresh trigger time of the metadata. 
+     * 
+     * @return the refresh trigger time
+     */
+    @Nonnull public Instant getRefreshTriggerTime() {
+        return refreshTriggerTime;
+    }
+
+    /**
+     * Set the refresh trigger time of the metadata.
+     * 
+     * @param dateTime the new refresh trigger time
+     */
+    public void setRefreshTriggerTime(@Nonnull final Instant dateTime) {
+        refreshTriggerTime = Constraint.isNotNull(dateTime, "Refresh trigger time may not be null");
+    }
+
+    /**
+     * Get the last time at which the entity's backing store data was accessed.
+     * 
+     * @return last access time
+     */
+    @Nonnull public Instant getLastAccessedTime() {
+        return lastAccessedTime;
+    }
+    
+    /**
+     * Record access of the entity's backing store data.
+     */
+    public void recordEntityAccess() {
+        lastAccessedTime = Instant.now();
+    }
+    
+    /**
+     * Get the read-write lock instance which governs access to the entity's backing store data. 
+     * 
+     * @return the lock instance
+     */
+    @Nonnull public ReadWriteLock getReadWriteLock() {
+        return readWriteLock;
+    }
+
+}
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
new file mode 100644
index 0000000..b0e40e4
--- /dev/null
+++ b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/OIDCMetadataResolver.java
@@ -0,0 +1,18 @@
+package net.shibboleth.oidc.metadata;
+
+import net.shibboleth.utilities.java.support.component.IdentifiedComponent;
+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
+ */
+// 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 {
+
+}
diff --git a/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/filter/FilterException.java b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/filter/FilterException.java
new file mode 100644
index 0000000..6b0bcb7
--- /dev/null
+++ b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/filter/FilterException.java
@@ -0,0 +1,49 @@
+package net.shibboleth.oidc.metadata.filter;
+
+import javax.annotation.Nullable;
+
+/**
+ * An exception thrown during the evaluation of a {@link MetadataFilter}.
+ */
+public class FilterException extends Exception {
+
+    /**
+     * Serial version UID.
+     */
+    private static final long serialVersionUID = 6214474330141026496L;
+
+    /**
+     * Constructor.
+     */
+    public FilterException() {
+        
+    }
+    
+    /**
+     * Constructor.
+     * 
+     * @param message exception message
+     */
+    public FilterException(@Nullable final String message) {
+        super(message);
+    }
+    
+    /**
+     * Constructor.
+     * 
+     * @param wrappedException exception to be wrapped by this one
+     */
+    public FilterException(@Nullable final Exception wrappedException) {
+        super(wrappedException);
+    }
+    
+    /**
+     * Constructor.
+     * 
+     * @param message exception message
+     * @param wrappedException exception to be wrapped by this one
+     */
+    public FilterException(@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/filter/MetadataFilter.java b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/filter/MetadataFilter.java
new file mode 100644
index 0000000..74706d3
--- /dev/null
+++ b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/filter/MetadataFilter.java
@@ -0,0 +1,46 @@
+package net.shibboleth.oidc.metadata.filter;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.core.xml.XMLObject;
+
+
+/**
+ * A metadata filter is used to process a metadata document after it has been acquired from a metadata source.
+ * 
+ * <p>
+ * Some example SAML filters might remove everything but identity providers roles, decreasing the data a service provider
+ * needs to work with, or a filter could be used to perform integrity checking on the retrieved metadata by verifying a
+ * 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.
+ * </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.
+ * </p>
+ * 
+ * @param <T> The metadata type.
+ */
+public interface MetadataFilter<T> {
+    
+    /**
+     * Filters the given metadata, perhaps to remove elements that are not wanted.
+     * 
+     * @param metadata the metadata to be filtered.
+     * @param context the metadata filter context
+     * 
+     * @return the filtered XMLObject, which may or may not be the same as the XMLObject instance
+     *          passed in to the method. Maybe be null, for example if the top-level element 
+     *          was removed by the filter.
+     * 
+     * @throws FilterException thrown if an error occurs during the filtering process
+     */
+    @Nullable T filter(@Nullable final T metadata, @Nonnull final MetadataFilterContext context)
+            throws FilterException;
+
+}
diff --git a/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/filter/MetadataFilterContext.java b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/filter/MetadataFilterContext.java
new file mode 100644
index 0000000..1677f84
--- /dev/null
+++ b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/filter/MetadataFilterContext.java
@@ -0,0 +1,14 @@
+package net.shibboleth.oidc.metadata.filter;
+
+
+import net.shibboleth.utilities.java.support.collection.ClassIndexedSet;
+
+/**
+ * Class used to provide contextual information at runtime to {@link MetadataFilter} implementations.
+ */
+public class MetadataFilterContext extends ClassIndexedSet<MetadataFilterContext.Data> {
+    
+    /** Marker interface for data classes to be used with {@link MetadataFilterContext}. */
+    public interface Data {}
+
+}
\ 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/AbstractDynamicOIDCHTTPProviderMetadataResolver.java
new file mode 100644
index 0000000..adf9687
--- /dev/null
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/AbstractDynamicOIDCHTTPProviderMetadataResolver.java
@@ -0,0 +1,289 @@
+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.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;
+import org.slf4j.LoggerFactory;
+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.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> {
+    
+    /** Default list of supported content MIME types. */
+    private static final String[] DEFAULT_CONTENT_TYPES = new String[] {"application/json",
+            "application/samlmetadata+xml", "application/xml", "text/xml"};
+    
+    /** 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";
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(AbstractDynamicOIDCHTTPProviderMetadataResolver.class);
+    
+    /** HTTP Client used to pull the configuration information. */
+    @Nonnull private final HttpClient httpClient;
+    
+    /** List of supported MIME types for use in Accept request header and validation of 
+     * response Content-Type header.*/
+    @NonnullAfterInit private List<String> supportedContentTypes;
+    
+    /** Generated Accept request header value. */
+    @NonnullAfterInit private String supportedContentTypesValue;
+    
+    /**Supported {@link MediaType} instances, constructed from the {@link #supportedContentTypes} list. */
+    @NonnullAfterInit private Set<MediaType> supportedMediaTypes;
+    
+    /** HTTP client security parameters. */
+    @Nullable private HttpClientSecurityParameters httpClientSecurityParameters;
+    
+    /** HttpClient ResponseHandler instance to use. */
+    @Nonnull private final ResponseHandler<MetadataType> responseHandler;
+    
+    /**
+     * 
+     * Constructor.
+     *
+     * @param client the instance of {@link HttpClient} used to fetch remote OIDC metadata
+     */
+    protected AbstractDynamicOIDCHTTPProviderMetadataResolver(@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.
+     * 
+     * <p>
+     * For all TLS-related parameters, must be used in conjunction with an HttpClient instance 
+     * which is configured with either:
+     * </p>
+     * <ul>
+     * <li>
+     * a {@link net.shibboleth.utilities.java.support.httpclient.TLSSocketFactory}
+     * </li>
+     * <li>
+     * a {@link org.opensaml.security.httpclient.impl.SecurityEnhancedTLSSocketFactory} which wraps
+     * an instance of {@link net.shibboleth.utilities.java.support.httpclient.TLSSocketFactory}, with
+     * the latter likely configured in a "no trust" configuration.  This variant is required if either a
+     * trust engine or a client TLS credential is to be used.
+     * </li>
+     * </ul>
+     *
+     * <p>
+     * For convenience methods for building a 
+     * {@link net.shibboleth.utilities.java.support.httpclient.TLSSocketFactory}, 
+     * see {@link net.shibboleth.utilities.java.support.httpclient.HttpClientSupport}.
+     * </p>
+     *
+     * <p>
+     * If the appropriate TLS socket factory is not configured and a trust engine is specified,
+     * then this will result in no TLS trust evaluation being performed and a 
+     * {@link ResolverException} will ultimately be thrown.
+     * </p>
+     *
+     * @param params the security parameters
+     */
+    public void setHttpClientSecurityParameters(@Nullable final HttpClientSecurityParameters params) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+
+        httpClientSecurityParameters = params;
+    }
+    
+    @Override
+    protected void doDestroy() {
+        httpClientSecurityParameters = null;
+        
+        supportedContentTypes = null;
+        supportedContentTypesValue = null;
+        supportedMediaTypes = null;
+        
+        super.doDestroy();
+    }
+    
+    @Override
+    @Nullable protected MetadataType fetchFromOriginSource(@Nonnull final CriteriaType criteria) 
+            throws IOException {
+            
+        final HttpUriRequest request = buildHttpRequest(criteria);
+        if (request == null) {
+            log.debug("{} Could not build request based on input criteria, unable to query", getLogPrefix());
+            return null;
+        }
+        
+        final HttpClientContext context = buildHttpClientContext(request);
+        
+        try {
+            MDC.put(MDC_ATTRIB_CURRENT_REQUEST_URI, request.getURI().toString());
+            final MetadataType result = httpClient.execute(request, responseHandler, context);
+            HttpClientSecuritySupport.checkTLSCredentialEvaluated(context, request.getURI().getScheme());
+            return result;
+        } 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 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 the request URL based on the input criteria set.
+     * 
+     * @param criteria the input criteria set
+     * @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/AbstractDynamicOIDCProviderMetadataResolver.java b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/AbstractDynamicOIDCProviderMetadataResolver.java
new file mode 100644
index 0000000..9e39084
--- /dev/null
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/AbstractDynamicOIDCProviderMetadataResolver.java
@@ -0,0 +1,493 @@
+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.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.MetadataFilter;
+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);
+        
+        //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 {
+        
+        
+        // TODO filter metadata if required and configured.
+        
+        // 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());
+        
+        //TODO log the new metadata expiration
+        
+        //TODO  save metadata to persistent cache if enabled.
+        
+    }
+    
+    /**
+     * 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/AbstractOIDCMetadataResolver.java b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/AbstractOIDCMetadataResolver.java
new file mode 100644
index 0000000..8d95ef0
--- /dev/null
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/AbstractOIDCMetadataResolver.java
@@ -0,0 +1,230 @@
+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 javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import net.shibboleth.oidc.metadata.BackingStore;
+import net.shibboleth.oidc.metadata.OIDCMetadataResolver;
+import net.shibboleth.oidc.metadata.filter.MetadataFilter;
+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.ResolverException;
+
+// 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> {
+    
+    
+    /** Class logger. */
+    private final Logger log = LoggerFactory.getLogger(AbstractOIDCMetadataResolver.class);
+    
+    /** Logging prefix. */
+    private String logPrefix;
+    
+    // TODO does this need synchronisation?
+    /** Backing store for runtime metadata.*/
+    @NonnullAfterInit private BackingStore<MetadataIdentifier, MetadataType> backingStore;    
+    
+    /** Filter applied to all metadata. */
+    private MetadataFilter<MetadataType> mdFilter;
+    
+    /**
+     * Whether problems during initialization should cause the provider to fail or go on without metadata. The
+     * assumption being that in most cases a provider will recover at some point in the future. Default: true.
+     */
+    private boolean failFastInitialization;
+    
+    /** Constructor.*/
+    protected AbstractOIDCMetadataResolver() {
+        failFastInitialization = true;
+    }
+    
+    
+    /**
+     * Return a prefix for logging messages for this component.
+     * 
+     * @return a string for insertion at the beginning of any log messages
+     */
+    @Nonnull @NotEmpty protected String getLogPrefix() {
+        if (logPrefix == null) {
+            logPrefix = String.format("Metadata Resolver %s %s:", getClass().getSimpleName(), getId());
+        }
+        return logPrefix;
+    }
+    
+    
+    /**
+     * Set the entity backing store currently in use by the metadata resolver.
+     * 
+     * @param newBackingStore the new entity backing store
+     */
+    protected void setBackingStore(@Nonnull final BackingStore<MetadataIdentifier, MetadataType> newBackingStore) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+        
+        backingStore = Constraint.isNotNull(newBackingStore, "BackingStore may not be null");
+    }
+    
+    /**
+     * Get the EntityDescriptor backing store currently in use by the metadata resolver.
+     * 
+     * @return the current effective entity backing store
+     */
+    @Nonnull protected BackingStore<MetadataIdentifier, MetadataType> getBackingStore() {
+        return backingStore;
+    }
+    
+    /**
+     * Gets whether problems during initialization should cause the provider to fail or go on without metadata. The
+     * assumption being that in most cases a provider will recover at some point in the future.
+     * 
+     * @return whether problems during initialization should cause the provider to fail
+     */
+    public boolean isFailFastInitialization() {
+        return failFastInitialization;
+    }
+
+    /**
+     * Sets whether problems during initialization should cause the provider to fail or go on without metadata. The
+     * assumption being that in most cases a provider will recover at some point in the future.
+     * 
+     * @param failFast whether problems during initialization should cause the provider to fail
+     */
+    public void setFailFastInitialization(final boolean failFast) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+        failFastInitialization = failFast;
+    }
+    
+    
+    /** {@inheritDoc} */
+    @Override protected final void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+
+        try {
+            initMetadataResolver();
+        } catch (final ComponentInitializationException e) {
+            if (isFailFastInitialization()) {
+                log.error("{} Metadata provider failed to properly initialize, fail-fast=true, halting", 
+                        getLogPrefix());
+                throw e;
+            }
+            log.error("{} Metadata provider failed to properly initialize, fail-fast=false, "
+                    + "continuing on in a degraded state", getLogPrefix(), e);
+        }
+    }   
+    
+    /** 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 {
+        ComponentSupport.ifNotInitializedThrowUninitializedComponentException(this);
+        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+
+        final Iterable<MetadataType> iterable = resolve(criteria);
+        if (iterable != null) {
+            final Iterator<MetadataType> iterator = iterable.iterator();
+            if (iterator != null && iterator.hasNext()) {
+                return iterator.next();
+            }
+        }
+        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/AbstractReloadingOIDCEntityResolver.java b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/AbstractReloadingOIDCEntityResolver.java
index 5bcd8bd..1bf5ac0 100644
--- a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/AbstractReloadingOIDCEntityResolver.java
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/AbstractReloadingOIDCEntityResolver.java
@@ -53,10 +53,10 @@ public abstract class AbstractReloadingOIDCEntityResolver<Key extends Identifier
     private final Logger log = LoggerFactory.getLogger(AbstractReloadingOIDCEntityResolver.class);
     
     /** Timer used to schedule background metadata update tasks. */
-    private Timer taskTimer;
+    private final Timer taskTimer;
     
     /** Whether we created our own task timer during object construction. */
-    private boolean createdOwnTaskTimer;
+    private final boolean createdOwnTaskTimer;
         
     /** Current task to refresh metadata. */
     private RefreshMetadataTask refreshMetadataTask;
@@ -95,6 +95,7 @@ public abstract class AbstractReloadingOIDCEntityResolver<Key extends Identifier
             createdOwnTaskTimer = true;
         } else {
             taskTimer = backgroundTaskTimer;
+            createdOwnTaskTimer = false;
         }
     }
 
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
new file mode 100644
index 0000000..b4dcfce
--- /dev/null
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/DefaultBackingStore.java
@@ -0,0 +1,36 @@
+package net.shibboleth.oidc.metadata.impl;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import javax.annotation.Nonnull;
+
+import net.shibboleth.oidc.metadata.BackingStore;
+
+public class DefaultBackingStore<I, T> implements BackingStore<I, T> {
+    
+    /** Index of entity IDs to their descriptors. */
+    private Map<I, List<T>> indexedDescriptors;
+
+    /** Ordered list of entity descriptors. */
+    private List<T> orderedDescriptors;
+    
+    /** Constructor.*/
+    public DefaultBackingStore() {
+        indexedDescriptors = new ConcurrentHashMap<>();
+        orderedDescriptors = new ArrayList<>();
+    }
+
+    @Override
+    @Nonnull public Map<I, List<T>> getIndexedDescriptors() {
+        return indexedDescriptors;
+    }
+
+    @Override
+    @Nonnull public List<T> getOrderedDescriptors() {
+        return orderedDescriptors;
+    }
+
+}
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
new file mode 100644
index 0000000..3624807
--- /dev/null
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/DefaultDynamicBackingStore.java
@@ -0,0 +1,48 @@
+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/HTTPProviderConfigurationMetadataResolver.java b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/HTTPProviderConfigurationMetadataResolver.java
new file mode 100644
index 0000000..4c6eff5
--- /dev/null
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/HTTPProviderConfigurationMetadataResolver.java
@@ -0,0 +1,239 @@
+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.Set;
+import java.util.function.BiFunction;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+import javax.annotation.concurrent.ThreadSafe;
+
+import org.apache.commons.lang3.StringUtils;
+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.util.EntityUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.slf4j.MDC;
+
+import com.google.common.net.MediaType;
+import com.nimbusds.oauth2.sdk.id.Issuer;
+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> {
+    
+    /** 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;
+    
+    /** The default well-known path for OpenID Provider metadata.  */
+    @Nonnull @NotEmpty 
+    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);
+ 
+    /** 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());
+        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) {
+        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);            
+            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
+    private static final class DefaultWellKnownPathCompositionStrategy implements BiFunction<Issuer, String, String> {
+        
+        /** Class logger. */
+        @Nonnull private final Logger log = LoggerFactory.getLogger(DefaultWellKnownPathCompositionStrategy.class);
+
+        @Override
+        @Nullable public String apply(@Nonnull final Issuer issuer, @Nonnull @NotEmpty final String wellKnownPath) {
+            // remove trailing slash if any (see openid-connect-discovery 4.1)
+            final String normalizedIssuer = StringUtils.removeEnd(issuer.getValue(), "/");
+            StringBuilder builder = new StringBuilder();
+            builder.append(normalizedIssuer).append(wellKnownPath);
+            return builder.toString();
+        }       
+    }
+
+    /** The response handler for parsing the providers's configuration information into {@link OIDCProviderMetadata}.*/
+    @Immutable
+    @ThreadSafe
+    public static final class OIDCProviderMetadataResponseHandler implements ResponseHandler<OIDCProviderMetadata> {        
+        
+        /** Class logger. */
+        @Nonnull private final Logger log = LoggerFactory.getLogger(OIDCProviderMetadataResponseHandler.class);
+
+        @Override
+        @Nullable public OIDCProviderMetadata handleResponse(HttpResponse response) throws IOException {
+            
+            final int httpStatusCode = response.getStatusLine().getStatusCode();
+            
+            final String currentRequestURI = MDC.get(MDC_ATTRIB_CURRENT_REQUEST_URI);
+            
+            // TODO we would need to do a conditional GET.
+            if (httpStatusCode == HttpStatus.SC_NOT_MODIFIED) {
+                log.debug("Metadata document from '{}' has not changed since last retrieval", 
+                        currentRequestURI);
+                return null;
+            }
+            
+            if (httpStatusCode != HttpStatus.SC_OK) {
+                log.warn("Non-ok status code '{}' returned from remote metadata source: {}",
+                        httpStatusCode, currentRequestURI);
+                return null;
+            }
+            
+            try {
+                validateHttpResponse(response);
+            } catch (final ResolverException e) {
+                log.error("Problem validating dynamic OIDC metadata HTTP response", e);
+                return null;
+            }
+            
+            try {              
+                // this should convert the entity with the character set from the entity.
+                final String jsonDocument = EntityUtils.toString(response.getEntity());                
+                return OIDCProviderMetadata.parse(jsonDocument);
+                
+            } catch (final Exception e) {
+                // catch any of the many exceptions
+                log.error("Error parsing HTTP response stream", e);
+                return null;
+            }
+            
+        }
+        
+        /**
+         * Validate the received HTTP response instance, such as checking for supported content types.
+         * 
+         * @param response the received response
+         * @throws ResolverException if the response was not valid, or if there is a fatal error validating the response
+         */
+        protected void validateHttpResponse(@Nonnull final HttpResponse response) throws ResolverException {
+
+            String contentTypeValue = null;
+            final Header contentType = response.getEntity().getContentType();
+            if (contentType != null && contentType.getValue() != null) {
+                contentTypeValue = StringSupport.trimOrNull(contentType.getValue());
+            }
+            log.debug("Saw raw Content-Type from response header '{}'", contentTypeValue);
+            
+            if (!MediaTypeSupport.validateContentType(contentTypeValue,Set.of(CONTENT_TYPE), true, false)) {
+                throw new ResolverException("HTTP response specified an unsupported Content-Type MIME type: " 
+                        + contentTypeValue);
+            }
+            
+        }
+        
+    }
+
+   
+
+}
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/HTTPProviderConfigurationMetadataResolverTest.java
new file mode 100644
index 0000000..d15b604
--- /dev/null
+++ b/oidc-common-metadata-impl/src/test/java/net/shibboleth/oidc/metadata/impl/HTTPProviderConfigurationMetadataResolverTest.java
@@ -0,0 +1,208 @@
+package net.shibboleth.oidc.metadata.impl;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertTrue;
+
+import java.io.IOException;
+import java.util.List;
+
+import org.apache.http.HttpHeaders;
+import org.apache.http.HttpStatus;
+import org.apache.http.ProtocolVersion;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.ResponseHandler;
+import org.apache.http.client.methods.HttpUriRequest;
+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.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.nimbusds.oauth2.sdk.id.Issuer;
+import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
+
+import net.shibboleth.oidc.metadata.criterion.IssuerIDCriterion;
+import net.shibboleth.oidc.metadata.impl.HTTPProviderConfigurationMetadataResolver.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 {
+    
+    private final String GOOD_PROVIDER_CONFIGURATION_INFO = "{\n"
+            + "\"issuer\": \"https://example.oidc.op.org\",\n"
+            + "\"authorization_endpoint\": \"https://example.oidc.op.org/o/oauth2/v2/auth\",\n"
+            + "\"device_authorization_endpoint\": \"https://oauth2.googleapis.com/device/code\",\n"
+            + "\"token_endpoint\": \"https://oauth2.googleapis.com/token\",\n"
+            + "\"userinfo_endpoint\": \"https://openidconnect.googleapis.com/v1/userinfo\",\n"
+            + "\"revocation_endpoint\": \"https://oauth2.googleapis.com/revoke\",\n"
+            + "\"jwks_uri\": \"https://www.googleapis.com/oauth2/v3/certs\",\n"
+            + "\"response_types_supported\": [\n"
+            + "\"code\",\n"
+            + "\"token\",\n"
+            + "\"id_token\",\n"
+            + "\"code token\",\n"
+            + "\"code id_token\",\n"
+            + "\"token id_token\",\n"
+            + "\"code token id_token\",\n"
+            + "\"none\"\n"
+            + "],\n"
+            + "\"subject_types_supported\": [\n"
+            + "\"public\"\n"
+            + "],\n"
+            + "\"id_token_signing_alg_values_supported\": [\n"
+            + "\"RS256\"\n"
+            + "],\n"
+            + "\"scopes_supported\": [\n"
+            + "\"openid\",\n"
+            + "\"email\",\n"
+            + "\"profile\"\n"
+            + "],\n"
+            + "\"token_endpoint_auth_methods_supported\": [\n"
+            + "\"client_secret_post\",\n"
+            + "\"client_secret_basic\"\n"
+            + "],\n"
+            + "\"claims_supported\": [\n"
+            + "\"aud\",\n"
+            + "\"email\",\n"
+            + "\"email_verified\",\n"
+            + "\"exp\",\n"
+            + "\"family_name\",\n"
+            + "\"given_name\",\n"
+            + "\"iat\",\n"
+            + "\"iss\",\n"
+            + "\"locale\",\n"
+            + "\"name\",\n"
+            + "\"picture\",\n"
+            + "\"sub\"\n"
+            + "],\n"
+            + "\"code_challenge_methods_supported\": [\n"
+            + "\"plain\",\n"
+            + "\"S256\"\n"
+            + "],\n"
+            + "\"grant_types_supported\": [\n"
+            + "\"authorization_code\",\n"
+            + "\"refresh_token\",\n"
+            + "\"urn:ietf:params:oauth:grant-type:device_code\",\n"
+            + "\"urn:ietf:params:oauth:grant-type:jwt-bearer\"\n"
+            + "]\n"
+            + "}";
+    
+    private HTTPProviderConfigurationMetadataResolver resolver;
+    
+    private HttpClient httpClient;
+    
+    @SuppressWarnings("unchecked")
+    @BeforeMethod
+    public void setup() throws Exception {
+        
+        // This setup will not exercise the ResponseHandler, the result is directly produced
+        // from the execute call.
+        httpClient = mock(HttpClient.class);
+        when(httpClient.
+                execute(any(HttpUriRequest.class),any(ResponseHandler.class),any(HttpContext.class)))
+                .thenReturn(OIDCProviderMetadata.parse(GOOD_PROVIDER_CONFIGURATION_INFO));
+        
+        resolver = new HTTPProviderConfigurationMetadataResolver(httpClient);
+        resolver.setId("mockHttpOIDCProvider");
+        resolver.setSupportedContentTypes(List.of("application/json"));
+        resolver.initialize();
+        
+    }
+    
+    @AfterMethod
+    public void tearDown() {
+        if (resolver != null) {
+            resolver.destroy();
+        }
+    }
+    
+    @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 {
+        Iterable<OIDCProviderMetadata> found = 
+                resolver.resolve(new CriteriaSet(new IssuerIDCriterion(new Issuer("https://example.oidc.op.org"))));
+        assertNotNull(found);
+        assertTrue(found.iterator().hasNext());
+    }
+    
+    @Test
+    void testResponseHandler_Success() throws IOException {
+        final OIDCProviderMetadataResponseHandler handler = new OIDCProviderMetadataResponseHandler();
+        final BasicHttpResponse httpResponse = new BasicHttpResponse(new ProtocolVersion("HTTP", 1, 1), HttpStatus.SC_OK, "OK");
+        final ByteArrayEntity entity = new ByteArrayEntity(GOOD_PROVIDER_CONFIGURATION_INFO.getBytes());
+        entity.setContentType(new BasicHeader(HttpHeaders.CONTENT_TYPE, "application/json"));
+        httpResponse.setEntity(entity);
+        final OIDCProviderMetadata metadata = handler.handleResponse(httpResponse);
+        
+        assertNotNull(metadata);
+        assertTrue("https://example.oidc.op.org".equals(metadata.getIssuer().getValue()));
+    }
+    
+    @Test
+    void testResponseHandler_WrongMIMEType() throws IOException {
+        final OIDCProviderMetadataResponseHandler handler = new OIDCProviderMetadataResponseHandler();
+        final BasicHttpResponse httpResponse = new BasicHttpResponse(new ProtocolVersion("HTTP", 1, 1), HttpStatus.SC_OK, "OK");
+        final ByteArrayEntity entity = new ByteArrayEntity(GOOD_PROVIDER_CONFIGURATION_INFO.getBytes());
+        entity.setContentType(new BasicHeader(HttpHeaders.CONTENT_TYPE, "application/not_json"));
+        httpResponse.setEntity(entity);
+        final OIDCProviderMetadata metadata = handler.handleResponse(httpResponse);
+        
+        assertNull(metadata);
+    }
+    
+    /* Run it twice, so the second is resolved from cache.*/
+    @SuppressWarnings("unchecked")
+    @Test
+    void testResolve_FromCache_Success() throws ResolverException, IOException {
+        
+        // test not in cache
+        assertFalse(resolver.getBackingStore().getIndexedDescriptors().containsKey(new Issuer("https://example.oidc.op.org")));
+        
+        // find and cache
+        Iterable<OIDCProviderMetadata> found = 
+                resolver.resolve(new CriteriaSet(new IssuerIDCriterion(new Issuer("https://example.oidc.op.org"))));
+        assertNotNull(found);
+        assertTrue(found.iterator().hasNext());        
+        
+        // test is in cache
+        assertTrue(resolver.getBackingStore().getIndexedDescriptors().containsKey(new Issuer("https://example.oidc.op.org")));
+        
+        
+        // Take down the source and see if it still resolves from the cache.
+        when(httpClient.
+                execute(any(HttpUriRequest.class),any(ResponseHandler.class),any(HttpContext.class)))
+                .thenReturn(null);
+        
+        Iterable<OIDCProviderMetadata> foundFromCache = 
+                resolver.resolve(new CriteriaSet(new IssuerIDCriterion(new Issuer("https://example.oidc.op.org"))));
+        assertNotNull(foundFromCache);
+        assertTrue(foundFromCache.iterator().hasNext());
+    }
+    
+    @Test
+    void testResolve_NullResponse() throws ResolverException, IOException {
+        when(httpClient.
+                execute(any(HttpUriRequest.class),any(ResponseHandler.class),any(HttpContext.class)))
+                .thenReturn(null);
+        Iterable<OIDCProviderMetadata> found = 
+                resolver.resolve(new CriteriaSet(new IssuerIDCriterion(new Issuer("https://example.oidc.op.org"))));
+        assertNotNull(found);
+        assertFalse(found.iterator().hasNext());
+    }
+
+}

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


More information about the commits mailing list