[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