[java-oidc-common] 02/02: JCOMOIDC-7 Move RP-metadata handling from the OIDC plugin

Henri Mikkonen henri.mikkonen at iki.fi
Mon Dec 28 12:51:24 UTC 2020


This is an automated email from the git hooks/post-receive script.

hjmikkon pushed a commit to branch main
in repository java-oidc-common.

View the commit online:
http://git.shibboleth.net/view/?p=java-oidc-common.git;a=commit;h=18f6b28c6c34c7c287d9233b57f5332daf1bef92

commit 18f6b28c6c34c7c287d9233b57f5332daf1bef92
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Mon Dec 28 14:49:58 2020 +0200

    JCOMOIDC-7 Move RP-metadata handling from the OIDC plugin
    
    https://issues.shibboleth.net/jira/browse/JCOMOIDC-6
    
    Initial import of RP metadata handling from java-idp-oidc.
---
 oidc-common-metadata-api/pom.xml                   |  40 ++
 .../oidc/metadata/ClientInformationManager.java    |  52 ++
 .../ClientInformationManagerException.java         |  59 +++
 .../oidc/metadata/ClientInformationResolver.java   |  29 +-
 .../oidc/metadata/ClientSecretValueResolver.java   |  27 +-
 .../RefreshableClientInformationResolver.java      |  60 +++
 .../RelyingPartyClientInformationProvider.java     | 203 +++++++
 .../oidc/metadata/criterion/ClientIDCriterion.java |  87 +++
 .../criterion/ClientSecretReferenceCriterion.java  |  87 +++
 .../oidc/metadata/criterion/package-info.java      |  26 +-
 .../net/shibboleth/oidc/metadata/package-info.java |  26 +-
 oidc-common-metadata-impl/pom.xml                  | 111 ++++
 .../impl/AbstractClientSecretValueResolver.java    |  26 +-
 .../impl/AbstractFileOIDCEntityResolver.java       | 130 +++++
 .../metadata/impl/AbstractOIDCEntityResolver.java  | 280 ++++++++++
 .../impl/AbstractReloadingOIDCEntityResolver.java  | 270 ++++++++++
 ...seStorageServiceClientInformationComponent.java |  76 +++
 .../impl/ChainingClientInformationResolver.java    | 220 ++++++++
 .../impl/ClientInformationNodeProcessor.java       | 587 +++++++++++++++++++++
 .../impl/FilesystemClientInformationResolver.java  | 206 ++++++++
 .../impl/PropertiesClientSecretValueResolver.java  | 125 +++++
 ...adingRelyingPartyClientInformationProvider.java | 111 ++++
 .../oidc/metadata/impl/ResolverHelper.java         |  90 ++++
 .../ResolverServiceClientSecretValueResolver.java  | 161 ++++++
 .../StorageServiceClientInformationManager.java    |  77 +++
 .../StorageServiceClientInformationResolver.java   | 151 ++++++
 .../oidc/metadata/impl/package-info.java           |  26 +-
 .../ext/impl/provider/ClientSecretProvider.java    |  86 +++
 .../provider/ClientSecretReferenceProvider.java    | 119 +++++
 .../ext/impl/provider/InlineJwksProvider.java      | 170 ++++++
 .../ext/impl/provider/JWKSReferenceProvider.java   |  94 ++++
 .../keyinfo/ext/impl/provider/package-info.java    |  26 +-
 .../impl/BaseClientSecretValueResolverTest.java    |  75 +++
 .../impl/ClientInformationNodeProcessorTest.java   | 214 ++++++++
 .../FilesystemClientInformationResolverTest.java   | 130 +++++
 .../PropertiesClientSecretValueResolverTest.java   | 113 ++++
 ...solverServiceClientSecretValueResolverTest.java | 284 ++++++++++
 ...StorageServiceClientInformationManagerTest.java | 123 +++++
 .../src/test/resources/logback-test.xml            |  19 +
 .../impl/EntitiesDescriptor-with-oidcmd.xml        | 143 +++++
 .../shibboleth/oidc/metadata/impl/RdbmsData.sql    |   7 +
 .../shibboleth/oidc/metadata/impl/RdbmsStore.sql   |   5 +
 .../impl/attribute-resolver-clientsecrets.xml      |  53 ++
 .../metadata/impl/client-secret-test.properties    |   1 +
 .../oidc/metadata/impl/ldapDataConnectorTest.ldif  |  26 +
 .../shibboleth/oidc/metadata/impl/oidc-client.json |   1 +
 .../oidc/metadata/impl/oidc-client2.json           |   1 +
 .../oidc/metadata/impl/oidc-clients.json           |   1 +
 .../net/shibboleth/oidc/metadata/impl/service.xml  |  47 ++
 .../shibboleth/oidc/saml/xmlobject/Constants.java  |  10 +-
 pom.xml                                            |  12 +
 51 files changed, 4946 insertions(+), 157 deletions(-)

diff --git a/oidc-common-metadata-api/pom.xml b/oidc-common-metadata-api/pom.xml
new file mode 100644
index 0000000..37d14fb
--- /dev/null
+++ b/oidc-common-metadata-api/pom.xml
@@ -0,0 +1,40 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>net.shibboleth.oidc</groupId>
+        <artifactId>oidc-common-parent</artifactId>
+        <version>0.0.1-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>oidc-common-metadata-api</artifactId>
+    <packaging>jar</packaging>
+    <name>Shibboleth OIDC Java Common Library - OIDC RP metadata API</name>
+    <description>Interfaces for common OIDC RP metadata functions/features.</description>
+
+    <properties>
+        <checkstyle.configLocation>${project.basedir}/../checkstyle.xml</checkstyle.configLocation>
+        <automatic.module.name>net.shibboleth.oidc.metadata.api</automatic.module.name>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>net.shibboleth.idp</groupId>
+            <artifactId>idp-core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>net.shibboleth.ext</groupId>
+            <artifactId>spring-extensions</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.opensaml</groupId>
+            <artifactId>opensaml-profile-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.nimbusds</groupId>
+            <artifactId>oauth2-oidc-sdk</artifactId>
+        </dependency>
+    </dependencies>
+
+</project>
diff --git a/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/ClientInformationManager.java b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/ClientInformationManager.java
new file mode 100644
index 0000000..d3f002b
--- /dev/null
+++ b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/ClientInformationManager.java
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.oidc.metadata;
+
+import java.time.Instant;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import com.nimbusds.oauth2.sdk.client.ClientInformation;
+import com.nimbusds.oauth2.sdk.id.ClientID;
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformation;
+
+/**
+ * A manager that is capable of managing {@link ClientInformation} instances.
+ */
+public interface ClientInformationManager {
+
+    /**
+     * Store a {@link ClientInformation} object.
+     * 
+     * @param clientInformation The client information to be stored.
+     * @param expiration The expiration for record, or null.
+     * @throws ClientInformationManagerException If the client information cannot be stored.
+     */
+    @Nonnull void storeClientInformation(@Nonnull final OIDCClientInformation clientInformation, 
+            @Nullable final Instant expiration) throws ClientInformationManagerException;
+    
+    /**
+     * Invalidates or otherwise removes a {@link ClientInformation} from persistent storage.
+     * 
+     * @param clientId the unique ID of the client information to destroy.
+     * @throws ClientInformationManagerException If the client information cannot be destroyed.
+     */
+    void destroyClientInformation(@Nonnull final ClientID clientId) throws ClientInformationManagerException;
+    
+}
\ No newline at end of file
diff --git a/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/ClientInformationManagerException.java b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/ClientInformationManagerException.java
new file mode 100644
index 0000000..e5f839a
--- /dev/null
+++ b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/ClientInformationManagerException.java
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.oidc.metadata;
+
+import javax.annotation.Nullable;
+
+/** Exception indicating a problem managing client information. */
+ at SuppressWarnings("serial")
+public class ClientInformationManagerException extends Exception {
+
+    /** Constructor. */
+    public ClientInformationManagerException() {
+        
+    }
+
+    /**
+     * Constructor.
+     * 
+     * @param message exception message
+     */
+    public ClientInformationManagerException(@Nullable final String message) {
+        super(message);
+    }
+
+    /**
+     * Constructor.
+     * 
+     * @param wrappedException exception to be wrapped by this one
+     */
+    public ClientInformationManagerException(@Nullable final Exception wrappedException) {
+        super(wrappedException);
+    }
+
+    /**
+     * Constructor.
+     * 
+     * @param message exception message
+     * @param wrappedException exception to be wrapped by this one
+     */
+    public ClientInformationManagerException(@Nullable final String message, 
+            @Nullable final Exception wrappedException) {
+        super(message, wrappedException);
+    }
+}
diff --git a/oidc-common-saml-api/src/main/java/net/shibboleth/oidc/saml/xmlobject/Constants.java b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/ClientInformationResolver.java
similarity index 51%
copy from oidc-common-saml-api/src/main/java/net/shibboleth/oidc/saml/xmlobject/Constants.java
copy to oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/ClientInformationResolver.java
index 316d364..d3b873f 100644
--- a/oidc-common-saml-api/src/main/java/net/shibboleth/oidc/saml/xmlobject/Constants.java
+++ b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/ClientInformationResolver.java
@@ -15,29 +15,18 @@
  * limitations under the License.
  */
 
-package net.shibboleth.oidc.saml.xmlobject;
+package net.shibboleth.oidc.metadata;
 
-import org.opensaml.saml.common.xml.SAMLConstants;
+import com.nimbusds.oauth2.sdk.client.ClientInformation;
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformation;
+
+import net.shibboleth.utilities.java.support.component.IdentifiedComponent;
+import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
+import net.shibboleth.utilities.java.support.resolver.Resolver;
 
 /**
- * Constants related to SAML 2.0 Metadata extension OAuthRPRoleDescriptorType.
+ * A resolver that is capable of resolving {@link ClientInformation} instances which meet certain supplied criteria.
  */
-public final class Constants {
-
-    /** SAML 2.0 Metadata extension for OAuth/OIDC descriptors prefi. */
-    public static final String SAML20MDOIDCMD_PREFIX = "oidcmd";
-
-    /** SAML 2.0 Metadata extension for OAuth/OIDC descriptors XML namespace. */
-    public static final String SAML20MDOIDCMD_NS = "urn:mace:shibboleth:metadata:oidc:1.0";
+public interface ClientInformationResolver extends Resolver<OIDCClientInformation, CriteriaSet>, IdentifiedComponent {
 
-    /** SAML 2.0 Metadata extension for OAuth/OIDC descriptors. */
-    public static final String SAML20MDOIDCMD_SCHEMA_LOCATION =
-            SAMLConstants.SCHEMA_DIR + "saml-metadata-ext-oidcmd.xsd";
-    
-    /**
-     * Constructor.
-     */
-    private Constants() {
-        // no op
-    }
 }
diff --git a/oidc-common-saml-api/src/main/java/net/shibboleth/oidc/saml/xmlobject/Constants.java b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/ClientSecretValueResolver.java
similarity index 51%
copy from oidc-common-saml-api/src/main/java/net/shibboleth/oidc/saml/xmlobject/Constants.java
copy to oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/ClientSecretValueResolver.java
index 316d364..3edc44f 100644
--- a/oidc-common-saml-api/src/main/java/net/shibboleth/oidc/saml/xmlobject/Constants.java
+++ b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/ClientSecretValueResolver.java
@@ -15,29 +15,16 @@
  * limitations under the License.
  */
 
-package net.shibboleth.oidc.saml.xmlobject;
+package net.shibboleth.oidc.metadata;
 
-import org.opensaml.saml.common.xml.SAMLConstants;
+import net.shibboleth.utilities.java.support.component.IdentifiedComponent;
+import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
+import net.shibboleth.utilities.java.support.resolver.Resolver;
 
 /**
- * Constants related to SAML 2.0 Metadata extension OAuthRPRoleDescriptorType.
+ * A resolver that is capable of resolving client secret values (as {@link String}s) which meet certain supplied
+ * criteria.
  */
-public final class Constants {
+public interface ClientSecretValueResolver extends Resolver<String, CriteriaSet>, IdentifiedComponent {
 
-    /** SAML 2.0 Metadata extension for OAuth/OIDC descriptors prefi. */
-    public static final String SAML20MDOIDCMD_PREFIX = "oidcmd";
-
-    /** SAML 2.0 Metadata extension for OAuth/OIDC descriptors XML namespace. */
-    public static final String SAML20MDOIDCMD_NS = "urn:mace:shibboleth:metadata:oidc:1.0";
-
-    /** SAML 2.0 Metadata extension for OAuth/OIDC descriptors. */
-    public static final String SAML20MDOIDCMD_SCHEMA_LOCATION =
-            SAMLConstants.SCHEMA_DIR + "saml-metadata-ext-oidcmd.xsd";
-    
-    /**
-     * Constructor.
-     */
-    private Constants() {
-        // no op
-    }
 }
diff --git a/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/RefreshableClientInformationResolver.java b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/RefreshableClientInformationResolver.java
new file mode 100644
index 0000000..6065a9c
--- /dev/null
+++ b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/RefreshableClientInformationResolver.java
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.oidc.metadata;
+
+import java.time.Instant;
+
+import javax.annotation.Nullable;
+
+import net.shibboleth.utilities.java.support.resolver.ResolverException;
+
+/**
+ * Specialization of {@link ClientInformationResolver} that supports on-demand refresh.
+ */
+public interface RefreshableClientInformationResolver extends ClientInformationResolver {
+
+    /**
+     * Refresh the data exposed by the resolver.
+     * 
+     * <p>
+     * An implementation of this method should typically be either <code>synchronized</code>
+     * or make use other locking mechanisms to protect against concurrent access.
+     * </p>
+     * 
+     * @throws ResolverException if the refresh operation was unsuccessful
+     */
+    void refresh() throws ResolverException;
+
+    /**
+     * Gets the time the last refresh cycle occurred.
+     * 
+     * @return time the last refresh cycle occurred
+     */
+    @Nullable Instant getLastRefresh();
+
+    /**
+     * Get the time that the currently available client information was last updated. Note, this may be different than
+     * the time retrieved by {@link #getLastRefresh()} is the client information was known not to have changed during
+     * the last refresh cycle.
+     * 
+     * @return time when the currently client information was last updated, null if it has never successfully been read
+     * in
+     */
+    @Nullable Instant getLastUpdate();
+
+}
\ No newline at end of file
diff --git a/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/RelyingPartyClientInformationProvider.java b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/RelyingPartyClientInformationProvider.java
new file mode 100644
index 0000000..228f898
--- /dev/null
+++ b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/RelyingPartyClientInformationProvider.java
@@ -0,0 +1,203 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.oidc.metadata;
+
+import java.time.Instant;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.base.Objects;
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformation;
+
+import net.shibboleth.ext.spring.service.AbstractServiceableComponent;
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+import net.shibboleth.utilities.java.support.component.ComponentSupport;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
+import net.shibboleth.utilities.java.support.resolver.ResolverException;
+
+/**
+ * This class exists primarily to allow the parsing of relying-party.xml to create a serviceable implementation of
+ * {@link ClientInformationResolver}. Based on net.shibboleth.idp.saml.metadata.RelyingPartyMetadataProvider.
+ */
+
+public class RelyingPartyClientInformationProvider extends AbstractServiceableComponent<ClientInformationResolver>
+        implements RefreshableClientInformationResolver, Comparable<RelyingPartyClientInformationProvider> {
+
+    /** If we autogenerate a sort key it comes from this count. */
+    private static int sortKeyValue;
+
+    /** Class logger. */
+    private final Logger log = LoggerFactory.getLogger(RelyingPartyClientInformationProvider.class);
+
+    /** The embedded resolver. */
+    @NonnullAfterInit
+    private ClientInformationResolver resolver;
+
+    /** The key by which we sort the provider. */
+    @NonnullAfterInit
+    private Integer sortKey;
+
+    /** Constructor. */
+    public RelyingPartyClientInformationProvider() {
+    }
+
+    /**
+     * Set the sort key.
+     * 
+     * @param key what to set
+     */
+    public void setSortKey(final int key) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        sortKey = Integer.valueOf(key);
+    }
+
+    /**
+     * Set the {@link ClientInformationResolver} to embed.
+     * 
+     * @param theResolver The {@link ClientInformationResolver} to embed.
+     */
+    @Nonnull
+    public void setEmbeddedResolver(@Nonnull final ClientInformationResolver theResolver) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        resolver = Constraint.isNotNull(theResolver, "ClientInformationResolver cannot be null");
+    }
+
+    /**
+     * Return what we are build around. Used for testing.
+     * 
+     * @return the parameter we got as a constructor
+     */
+    @Nonnull
+    public ClientInformationResolver getEmbeddedResolver() {
+        return resolver;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    @Nonnull
+    public Iterable<OIDCClientInformation> resolve(@Nullable final CriteriaSet criteria) throws ResolverException {
+
+        return resolver.resolve(criteria);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    @Nullable
+    public OIDCClientInformation resolveSingle(@Nullable final CriteriaSet criteria) throws ResolverException {
+
+        return resolver.resolveSingle(criteria);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        setId(resolver.getId());
+        super.doInitialize();
+        if (null == resolver) {
+            throw new ComponentInitializationException("ClientInformationResolver cannot be null");
+        }
+
+        if (null == sortKey) {
+            synchronized (this) {
+                sortKeyValue++;
+                sortKey = Integer.valueOf(sortKeyValue);
+            }
+            log.info("Top level ClientInformation Provider '{}' did not have a sort key; giving it value '{}'", getId(),
+                    sortKey);
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    @Nonnull
+    public ClientInformationResolver getComponent() {
+        return this;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void refresh() throws ResolverException {
+        if (resolver instanceof RefreshableClientInformationResolver) {
+            ((RefreshableClientInformationResolver) resolver).refresh();
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Instant getLastRefresh() {
+        if (resolver instanceof RefreshableClientInformationResolver) {
+            return ((RefreshableClientInformationResolver) resolver).getLastRefresh();
+        } else {
+            return null;
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Instant getLastUpdate() {
+        if (resolver instanceof RefreshableClientInformationResolver) {
+            return ((RefreshableClientInformationResolver) resolver).getLastUpdate();
+        } else {
+            return null;
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public int compareTo(final RelyingPartyClientInformationProvider other) {
+        ComponentSupport.ifNotInitializedThrowUninitializedComponentException(this);
+        final int result = sortKey.compareTo(other.sortKey);
+        if (result != 0) {
+            return result;
+        }
+        if (equals(other)) {
+            return 0;
+        }
+        return getId().compareTo(other.getId());
+    }
+
+    /**
+     * {@inheritDoc}. We are within a spring context and so equality can be determined by ID, however we also test by
+     * sortKey just in case.
+     */
+    @Override
+    public boolean equals(final Object other) {
+        if (null == other) {
+            return false;
+        }
+        if (!(other instanceof RelyingPartyClientInformationProvider)) {
+            return false;
+        }
+        final RelyingPartyClientInformationProvider otherRp = (RelyingPartyClientInformationProvider) other;
+
+        return Objects.equal(otherRp.sortKey, sortKey) && Objects.equal(getId(), otherRp.getId());
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(sortKey, getId());
+    }
+
+}
\ No newline at end of file
diff --git a/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/criterion/ClientIDCriterion.java b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/criterion/ClientIDCriterion.java
new file mode 100644
index 0000000..b08e4fe
--- /dev/null
+++ b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/criterion/ClientIDCriterion.java
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.oidc.metadata.criterion;
+
+import javax.annotation.Nonnull;
+
+import com.nimbusds.oauth2.sdk.id.ClientID;
+
+import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+import net.shibboleth.utilities.java.support.resolver.Criterion;
+
+/**
+ * A {@link Criterion} representing an OIDC client ID.
+ */
+public class ClientIDCriterion implements Criterion {
+
+    /** The client ID. */
+    @Nonnull @NotEmpty private final ClientID id;
+
+    /**
+     * Constructor.
+     * 
+     * @param clientId the client ID, can not be null or empty.
+     */
+    public ClientIDCriterion(@Nonnull @NotEmpty final ClientID clientId) {
+        id = Constraint.isNotNull(clientId, "Client ID cannot be null or empty");
+    }
+
+    /**
+     * Gets the client ID.
+     * 
+     * @return the client ID, never null or empty.
+     */
+    @Nonnull @NotEmpty public ClientID getClientID() {
+        return id;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("ClientIDCriterion [id=");
+        builder.append(id);
+        builder.append("]");
+        return builder.toString();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public int hashCode() {
+        return id.hashCode();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean equals(final Object obj) {
+        if (this == obj) {
+            return true;
+        }
+
+        if (obj == null) {
+            return false;
+        }
+
+        if (obj instanceof ClientIDCriterion) {
+            return id.equals(((ClientIDCriterion) obj).getClientID());
+        }
+
+        return false;
+    }
+}
diff --git a/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/criterion/ClientSecretReferenceCriterion.java b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/criterion/ClientSecretReferenceCriterion.java
new file mode 100644
index 0000000..3075a01
--- /dev/null
+++ b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/criterion/ClientSecretReferenceCriterion.java
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.oidc.metadata.criterion;
+
+import javax.annotation.Nonnull;
+
+import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+import net.shibboleth.utilities.java.support.resolver.Criterion;
+
+/**
+ * A {@link Criterion} representing a reference to a client secret.
+ */
+public class ClientSecretReferenceCriterion implements Criterion {
+    
+    /** The client secret reference. */
+    @Nonnull @NotEmpty private final String secretReference;
+    
+    /**
+     * Constructor.
+     *
+     * @param reference The client secret reference.
+     */
+    public ClientSecretReferenceCriterion(@Nonnull @NotEmpty final String reference) {
+        secretReference = Constraint.isNotEmpty(reference, "The client secret reference cannot be empty");
+    }
+    
+    /**
+     * Get the client secret reference.
+     * 
+     * @return The client secret reference.
+     */
+    @Nonnull @NotEmpty public String getSecretReference() {
+        return secretReference;
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("ClientSecretReferenceCriterion [secretReference=");
+        builder.append(secretReference);
+        builder.append("]");
+        return builder.toString();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public int hashCode() {
+        return secretReference.hashCode();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean equals(final Object obj) {
+        if (this == obj) {
+            return true;
+        }
+
+        if (obj == null) {
+            return false;
+        }
+
+        if (obj instanceof String) {
+            return secretReference.equals((String) obj);
+        }
+
+        return false;
+    }
+
+
+}
diff --git a/oidc-common-saml-api/src/main/java/net/shibboleth/oidc/saml/xmlobject/Constants.java b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/criterion/package-info.java
similarity index 51%
copy from oidc-common-saml-api/src/main/java/net/shibboleth/oidc/saml/xmlobject/Constants.java
copy to oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/criterion/package-info.java
index 316d364..9b0230a 100644
--- a/oidc-common-saml-api/src/main/java/net/shibboleth/oidc/saml/xmlobject/Constants.java
+++ b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/criterion/package-info.java
@@ -15,29 +15,7 @@
  * limitations under the License.
  */
 
-package net.shibboleth.oidc.saml.xmlobject;
-
-import org.opensaml.saml.common.xml.SAMLConstants;
-
 /**
- * Constants related to SAML 2.0 Metadata extension OAuthRPRoleDescriptorType.
+ * Criteria related to OIDC entity resolution.
  */
-public final class Constants {
-
-    /** SAML 2.0 Metadata extension for OAuth/OIDC descriptors prefi. */
-    public static final String SAML20MDOIDCMD_PREFIX = "oidcmd";
-
-    /** SAML 2.0 Metadata extension for OAuth/OIDC descriptors XML namespace. */
-    public static final String SAML20MDOIDCMD_NS = "urn:mace:shibboleth:metadata:oidc:1.0";
-
-    /** SAML 2.0 Metadata extension for OAuth/OIDC descriptors. */
-    public static final String SAML20MDOIDCMD_SCHEMA_LOCATION =
-            SAMLConstants.SCHEMA_DIR + "saml-metadata-ext-oidcmd.xsd";
-    
-    /**
-     * Constructor.
-     */
-    private Constants() {
-        // no op
-    }
-}
+package net.shibboleth.oidc.metadata.criterion;
\ No newline at end of file
diff --git a/oidc-common-saml-api/src/main/java/net/shibboleth/oidc/saml/xmlobject/Constants.java b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/package-info.java
similarity index 51%
copy from oidc-common-saml-api/src/main/java/net/shibboleth/oidc/saml/xmlobject/Constants.java
copy to oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/package-info.java
index 316d364..4f37977 100644
--- a/oidc-common-saml-api/src/main/java/net/shibboleth/oidc/saml/xmlobject/Constants.java
+++ b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/package-info.java
@@ -15,29 +15,7 @@
  * limitations under the License.
  */
 
-package net.shibboleth.oidc.saml.xmlobject;
-
-import org.opensaml.saml.common.xml.SAMLConstants;
-
 /**
- * Constants related to SAML 2.0 Metadata extension OAuthRPRoleDescriptorType.
+ * Interfaces and common classes related to resolution of OIDC metadata (or client information).
  */
-public final class Constants {
-
-    /** SAML 2.0 Metadata extension for OAuth/OIDC descriptors prefi. */
-    public static final String SAML20MDOIDCMD_PREFIX = "oidcmd";
-
-    /** SAML 2.0 Metadata extension for OAuth/OIDC descriptors XML namespace. */
-    public static final String SAML20MDOIDCMD_NS = "urn:mace:shibboleth:metadata:oidc:1.0";
-
-    /** SAML 2.0 Metadata extension for OAuth/OIDC descriptors. */
-    public static final String SAML20MDOIDCMD_SCHEMA_LOCATION =
-            SAMLConstants.SCHEMA_DIR + "saml-metadata-ext-oidcmd.xsd";
-    
-    /**
-     * Constructor.
-     */
-    private Constants() {
-        // no op
-    }
-}
+package net.shibboleth.oidc.metadata;
\ No newline at end of file
diff --git a/oidc-common-metadata-impl/pom.xml b/oidc-common-metadata-impl/pom.xml
new file mode 100644
index 0000000..3cd5071
--- /dev/null
+++ b/oidc-common-metadata-impl/pom.xml
@@ -0,0 +1,111 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>net.shibboleth.oidc</groupId>
+        <artifactId>oidc-common-parent</artifactId>
+        <version>0.0.1-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>oidc-common-metadata-impl</artifactId>
+    <packaging>jar</packaging>    
+    <name>Shibboleth OIDC Java Common Library - OIDC RP metadata implementation</name>
+    <description>Implementation for common OIDC RP metadata functions/features.</description>
+    
+    <properties>
+        <checkstyle.configLocation>${project.basedir}/../checkstyle.xml</checkstyle.configLocation>
+        <automatic.module.name>net.shibboleth.oidc.metadata.impl</automatic.module.name>
+    </properties>
+    
+    <dependencies>
+        <dependency>
+            <groupId>net.shibboleth.oidc</groupId>
+            <artifactId>oidc-common-crypto-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>net.shibboleth.oidc</groupId>
+            <artifactId>oidc-common-crypto-impl</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>net.shibboleth.oidc</groupId>
+            <artifactId>oidc-common-metadata-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>net.shibboleth.oidc</groupId>
+            <artifactId>oidc-common-saml-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>net.shibboleth.idp</groupId>
+            <artifactId>idp-attribute-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>net.shibboleth.idp</groupId>
+            <artifactId>idp-attribute-resolver-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.opensaml</groupId>
+            <artifactId>opensaml-storage-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.opensaml</groupId>
+            <artifactId>opensaml-saml-impl</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.opensaml</groupId>
+            <artifactId>opensaml-xmlsec-impl</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>net.shibboleth.oidc</groupId>
+            <artifactId>oidc-common-saml-impl</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>net.shibboleth.idp</groupId>
+            <artifactId>idp-saml-api</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.hsqldb</groupId>
+            <artifactId>hsqldb</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>${opensaml.groupId}</groupId>
+            <artifactId>opensaml-storage-impl</artifactId>
+            <version>${opensaml.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>net.shibboleth.idp</groupId>
+            <artifactId>idp-attribute-resolver-spring</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <!-- Needed for Nimbus use in tests. -->
+        <dependency>
+            <groupId>com.sun.mail</groupId>
+            <artifactId>jakarta.mail</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>${opensaml.groupId}</groupId>
+            <artifactId>opensaml-core</artifactId>
+            <version>${opensaml.version}</version>
+            <type>test-jar</type>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>net.shibboleth.idp</groupId>
+            <artifactId>idp-core</artifactId>
+            <type>test-jar</type>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>net.shibboleth.idp</groupId>
+            <artifactId>idp-attribute-resolver-spring</artifactId>
+            <type>test-jar</type>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+</project>
diff --git a/oidc-common-saml-api/src/main/java/net/shibboleth/oidc/saml/xmlobject/Constants.java b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/AbstractClientSecretValueResolver.java
similarity index 51%
copy from oidc-common-saml-api/src/main/java/net/shibboleth/oidc/saml/xmlobject/Constants.java
copy to oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/AbstractClientSecretValueResolver.java
index 316d364..069e165 100644
--- a/oidc-common-saml-api/src/main/java/net/shibboleth/oidc/saml/xmlobject/Constants.java
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/AbstractClientSecretValueResolver.java
@@ -15,29 +15,15 @@
  * limitations under the License.
  */
 
-package net.shibboleth.oidc.saml.xmlobject;
+package net.shibboleth.oidc.metadata.impl;
 
-import org.opensaml.saml.common.xml.SAMLConstants;
+import net.shibboleth.oidc.metadata.ClientSecretValueResolver;
+import net.shibboleth.utilities.java.support.component.AbstractIdentifiableInitializableComponent;
 
 /**
- * Constants related to SAML 2.0 Metadata extension OAuthRPRoleDescriptorType.
+ * Base class for {@link ClientSecretValueResolver}s.
  */
-public final class Constants {
+public abstract class AbstractClientSecretValueResolver extends AbstractIdentifiableInitializableComponent 
+    implements ClientSecretValueResolver {
 
-    /** SAML 2.0 Metadata extension for OAuth/OIDC descriptors prefi. */
-    public static final String SAML20MDOIDCMD_PREFIX = "oidcmd";
-
-    /** SAML 2.0 Metadata extension for OAuth/OIDC descriptors XML namespace. */
-    public static final String SAML20MDOIDCMD_NS = "urn:mace:shibboleth:metadata:oidc:1.0";
-
-    /** SAML 2.0 Metadata extension for OAuth/OIDC descriptors. */
-    public static final String SAML20MDOIDCMD_SCHEMA_LOCATION =
-            SAMLConstants.SCHEMA_DIR + "saml-metadata-ext-oidcmd.xsd";
-    
-    /**
-     * Constructor.
-     */
-    private Constants() {
-        // no op
-    }
 }
diff --git a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/AbstractFileOIDCEntityResolver.java b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/AbstractFileOIDCEntityResolver.java
new file mode 100644
index 0000000..7d8892a
--- /dev/null
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/AbstractFileOIDCEntityResolver.java
@@ -0,0 +1,130 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.oidc.metadata.impl;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.time.Instant;
+import java.util.Timer;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.core.io.Resource;
+
+import com.nimbusds.oauth2.sdk.id.Identifier;
+
+import net.shibboleth.utilities.java.support.component.ComponentSupport;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+import net.shibboleth.utilities.java.support.resolver.ResolverException;
+
+/**
+ * Based on {@link org.opensaml.saml.metadata.resolver.impl.FilesystemMetadataResolver}.
+ * 
+ * @param <Key> The identifier type in the backing store
+ * @param <Value> The entity type in the backing store
+ */
+public abstract class AbstractFileOIDCEntityResolver<Key extends Identifier, Value> 
+    extends AbstractReloadingOIDCEntityResolver<Key, Value> {
+
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(AbstractFileOIDCEntityResolver.class);
+
+    /** The metadata file. */
+    @Nonnull private File metadataFile;
+
+    /**
+     * Constructor.
+     * 
+     * @param metadata the metadata file
+     * 
+     * @throws IOException If the metedata cannot be loaded.
+     */
+    public AbstractFileOIDCEntityResolver(@Nonnull final Resource metadata) throws IOException {
+        setMetadataFile(metadata.getFile());
+    }
+
+    /**
+     * Constructor.
+     * 
+     * @param metadata the metadata file
+     * @param backgroundTaskTimer timer used to refresh metadata in the background
+     * 
+     * @throws IOException If the metedata cannot be loaded.
+     */
+    public AbstractFileOIDCEntityResolver(@Nullable final Timer backgroundTaskTimer, @Nonnull final Resource metadata)
+            throws IOException {
+        super(backgroundTaskTimer);
+        setMetadataFile(metadata.getFile());
+    }
+
+    /**
+     * Sets the file from which metadata is read.
+     * 
+     * @param file path to the metadata file
+     */
+    protected void setMetadataFile(@Nonnull final File file) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+
+        metadataFile = Constraint.isNotNull(file, "Metadata file cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doDestroy() {
+        metadataFile = null;
+          
+        super.doDestroy();
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    protected String getMetadataIdentifier() {
+        return metadataFile.getAbsolutePath();
+    }
+    
+    /**
+     * Get the time for the last update/modification of the metadata file.
+     * @return The last update time.
+     */
+    protected Instant getMetadataUpdateTime() {
+        return Instant.ofEpochMilli(metadataFile.lastModified());
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected byte[] fetchMetadata() throws ResolverException {
+        try {
+            ResolverHelper.validateMetadataFile(metadataFile);
+            final Instant metadataUpdateTime = getMetadataUpdateTime();
+            if (getLastRefresh() == null || getLastUpdate() == null || metadataUpdateTime.isAfter(getLastRefresh())) {
+                log.debug("Returning the contents of {} as byte array", metadataFile.toPath());
+                return ResolverHelper.inputstreamToByteArray(new FileInputStream(metadataFile));
+            }
+            return null;
+        } catch (final IOException e) {
+            final String errMsg = "Unable to read metadata file " + metadataFile.getAbsolutePath();
+            log.error(errMsg, e);
+            throw new ResolverException(errMsg, e);
+        }
+    }
+}
diff --git a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/AbstractOIDCEntityResolver.java b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/AbstractOIDCEntityResolver.java
new file mode 100644
index 0000000..0507a7b
--- /dev/null
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/AbstractOIDCEntityResolver.java
@@ -0,0 +1,280 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.oidc.metadata.impl;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import javax.annotation.Nonnull;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.base.Strings;
+import com.nimbusds.oauth2.sdk.id.Identifier;
+
+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.Resolver;
+import net.shibboleth.utilities.java.support.resolver.ResolverException;
+
+/**
+ * A base class for {@link Resolver}s used for resolving entities containing identifiers based on {@link Identifier}. 
+ *
+ * @param <Key> The identifier type in the backing store
+ * @param <Value> The entity type in the backing store
+ */
+public abstract class AbstractOIDCEntityResolver<Key extends Identifier, Value> 
+    extends AbstractIdentifiableInitializableComponent {
+
+    /** Class logger. */
+    private final Logger log = LoggerFactory.getLogger(AbstractOIDCEntityResolver.class);
+    
+    /** Backing store for runtime JSON data. */
+    private JsonBackingStore jsonBackingStore;
+    
+    /**
+     * 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.
+     */
+    public AbstractOIDCEntityResolver() {
+        failFastInitialization = true;
+    }
+
+    /**
+     * 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 void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        try {
+            initOIDCResolver();
+        } catch (final ComponentInitializationException e) {
+            if (failFastInitialization) {
+                log.error("OIDC metadata provider failed to properly initialize, fail-fast=true, halting", e);
+                throw e;
+            } else {
+                log.error("OIDC metadata provider failed to properly initialize, fail-fast=false, "
+                        + "continuing on in a degraded state", e);
+            }
+        }
+    }
+    
+    /**
+     * Initializes this resolver by creating a new backing store.
+     * @throws ComponentInitializationException If the initialization fails.
+     */
+    protected void initOIDCResolver() throws ComponentInitializationException {
+        jsonBackingStore = createNewBackingStore();
+    }
+
+    /**
+     * Get list of information matching a given identifier.
+     * 
+     * @param identifier identifier to lookup
+     * @return a list of information
+     * @throws ResolverException if an error occurs
+     */
+    @Nonnull @NonnullElements protected List<Value> lookupIdentifier(
+            @Nonnull @NotEmpty final Key identifier)
+            throws ResolverException {
+        if (!isInitialized()) {
+            throw new ResolverException("Metadata resolver has not been initialized");
+        }
+
+        if (identifier == null || Strings.isNullOrEmpty(identifier.getValue())) {
+            log.debug("Identifier was null or empty, skipping search for it");
+            return Collections.emptyList();
+        }
+
+        final List<Value> allInformation = lookupIndexedIdentifier(identifier);
+        if (allInformation.isEmpty()) {
+            log.debug("Backing store does not contain any information with the ID: {}", identifier);
+            return allInformation;
+        }
+        return allInformation;
+    }
+
+    /**
+     * Lookup the specified identifier from the index. The returned list will be a copy of what is stored in the 
+     * backing index, and is safe to be manipulated by callers.
+     * 
+     * @param identifier the identifier to lookup
+     * 
+     * @return list copy of indexed identifiers, may be empty, will never be null
+     */
+    @Nonnull @NonnullElements protected List<Value> lookupIndexedIdentifier(
+            @Nonnull @NotEmpty final Key identifier) {
+        final List<Value> allInformation = getBackingStore().getIndexedInformation().get(identifier);
+        if (allInformation != null) {
+            return new ArrayList<>(allInformation);
+        } else {
+            return Collections.emptyList();
+        }
+    }
+
+    /**
+     * Pre-process the specified entity descriptor, updating the specified entity backing store instance as necessary.
+     * 
+     * @param entityDescriptor the target entity descriptor to process
+     * @param key key to entity
+     * @param backingStore the backing store instance to update
+     */
+    protected void preProcessEntityDescriptor(@Nonnull final Value entityDescriptor, @Nonnull final Key key,
+            @Nonnull final JsonBackingStore backingStore) {
+
+        backingStore.getOrderedInformation().add(entityDescriptor);
+        indexEntityDescriptor(entityDescriptor, key, backingStore);
+    }
+    
+    /**
+     * Remove from the backing store all metadata for the entity with the given identifier.
+     * 
+     * @param identifier the identifier of the metadata to remove
+     * @param backingStore the backing store instance to update
+     */
+    protected void removeByIdentifier(@Nonnull final Key identifier, @Nonnull final JsonBackingStore backingStore) {
+        final Map<Key, List<Value>> indexedDescriptors = backingStore.getIndexedInformation();
+        final List<Value> descriptors = indexedDescriptors.get(identifier);
+        if (descriptors != null) {
+            backingStore.getOrderedInformation().removeAll(descriptors);
+        }
+        indexedDescriptors.remove(identifier);
+    }
+
+    /**
+     * Index the specified entity descriptor, updating the specified entity backing store instance as necessary.
+     * 
+     * @param entityDescriptor the target entity descriptor to process
+     * @param key key to entity
+     * @param backingStore the backing store instance to update
+     */
+    protected void indexEntityDescriptor(@Nonnull final Value entityDescriptor, @Nonnull final Key key,
+            @Nonnull final JsonBackingStore backingStore) {
+
+        List<Value> entities = backingStore.getIndexedInformation().get(key);
+        if (entities == null) {
+            entities = new ArrayList<>();
+            backingStore.getIndexedInformation().put(key, entities);
+        } else if (!entities.isEmpty()) {
+            log.warn("Detected duplicate object for key: {}", key);
+        }
+        entities.add(entityDescriptor);
+    }
+
+    
+    /**
+     * Create a new backing store instance for entity data. Subclasses may override to return a more
+     * specialized subclass type. Note this method does not make the returned backing store the effective one in use.
+     * The caller is responsible for calling {@link #setBackingStore(AbstractOIDCEntityResolver.JsonBackingStore)}
+     * to make it the effective instance in use.
+     * 
+     * @return the new backing store instance
+     */
+    @Nonnull protected JsonBackingStore createNewBackingStore() {
+        return new JsonBackingStore();
+    }
+
+    /**
+     * Get the entity backing store currently in use by the metadata resolver.
+     * 
+     * @return the current effective entity backing store
+     */
+    @Nonnull protected JsonBackingStore getBackingStore() {
+        return jsonBackingStore;
+    }
+
+    /**
+     * Set the entity backing store currently in use by the metadata resolver.
+     * 
+     * @param newBackingStore the new entity backing store
+     */
+    protected void setBackingStore(@Nonnull final JsonBackingStore newBackingStore) {
+        jsonBackingStore = Constraint.isNotNull(newBackingStore, "JsonBackingStore may not be null");
+    }
+
+    
+    /**
+     * The collection of data which provides the backing store for the processed metadata.
+     */
+    protected class JsonBackingStore {
+
+        /** Index of identifiers to their entity information. */
+        private Map<Key, List<Value>> indexedEntities;
+
+        /** Ordered list of entity information. */
+        private List<Value> orderedEntitiess;
+
+        /** Constructor. */
+        protected JsonBackingStore() {
+            indexedEntities = new ConcurrentHashMap<>();
+            orderedEntitiess = new ArrayList<>();
+        }
+
+        /**
+         * Get the entity information index.
+         * 
+         * @return the entity information index.
+         */
+        @Nonnull public Map<Key, List<Value>> getIndexedInformation() {
+            return indexedEntities;
+        }
+
+        /**
+         * Get the ordered entity information.
+         * 
+         * @return the entity information.
+         */
+        @Nonnull public List<Value> getOrderedInformation() {
+            return orderedEntitiess;
+        }
+
+    }
+}
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
new file mode 100644
index 0000000..5bcd8bd
--- /dev/null
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/AbstractReloadingOIDCEntityResolver.java
@@ -0,0 +1,270 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.oidc.metadata.impl;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Timer;
+import java.util.TimerTask;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.nimbusds.oauth2.sdk.ParseException;
+import com.nimbusds.oauth2.sdk.id.Identifier;
+
+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;
+
+/**
+ * Based on {@link org.opensaml.saml.metadata.resolver.impl.AbstractReloadingMetadataResolver}.
+ * 
+ * @param <Key> The identifier type in the backing store
+ * @param <Value> The entity type in the backing store
+ */
+public abstract class AbstractReloadingOIDCEntityResolver<Key extends Identifier, Value> 
+    extends AbstractOIDCEntityResolver<Key, Value> {
+
+    /** Class logger. */
+    private final Logger log = LoggerFactory.getLogger(AbstractReloadingOIDCEntityResolver.class);
+    
+    /** Timer used to schedule background metadata update tasks. */
+    private Timer taskTimer;
+    
+    /** Whether we created our own task timer during object construction. */
+    private boolean createdOwnTaskTimer;
+        
+    /** Current task to refresh metadata. */
+    private RefreshMetadataTask refreshMetadataTask;
+    
+    /**
+     * Refresh interval used when metadata does not contain any validUntil or cacheDuration information. Default value:
+     * 4 hours
+     */
+    @Nonnull @Positive private Duration maxRefreshDelay = Duration.ofHours(4);
+
+    /** Floor, in milliseconds, for the refresh interval. Default value: 5 minutes */
+    @Nonnull @Positive private Duration minRefreshDelay = Duration.ofMinutes(5);
+
+    /** Last time the metadata was updated. */
+    @Nullable private Instant lastUpdate;
+
+    /** Last time a refresh cycle occurred. */
+    @Nullable private Instant lastRefresh;
+
+    /** Next time a refresh cycle will occur. */
+    @Nullable private Instant nextRefresh;
+
+    /** Constructor. */
+    protected AbstractReloadingOIDCEntityResolver() {
+        this(null);
+    }
+
+    /**
+     * Constructor.
+     * 
+     * @param backgroundTaskTimer time used to schedule background refresh tasks
+     */
+    protected AbstractReloadingOIDCEntityResolver(@Nullable final Timer backgroundTaskTimer) {
+        if (backgroundTaskTimer == null) {
+            taskTimer = new Timer(true);
+            createdOwnTaskTimer = true;
+        } else {
+            taskTimer = backgroundTaskTimer;
+        }
+    }
+
+    protected void initOIDCResolver() throws ComponentInitializationException {
+        super.initOIDCResolver();
+        try {
+            refresh();
+        } catch (final ResolverException e) {
+            log.error("Could not refresh the entity information", e);
+            throw new ComponentInitializationException("Could not refresh the entity information", e);
+        }
+    }
+
+    /**
+     * Get last update of resolver.
+     * 
+     * @return last update
+     */
+    @Nullable public Instant getLastUpdate() {
+        return lastUpdate;
+    }
+
+    /**
+     * Get last refresh of resolver.
+     * 
+     * @return last refresh
+     */
+    @Nullable public Instant getLastRefresh() {
+        return lastRefresh;
+    }
+    
+    /**
+     * Sets the minimum amount of time between refreshes.
+     * 
+     * @param delay minimum amount of time between refreshes
+     */
+    public void setMinRefreshDelay(@Positive @Nonnull final Duration delay) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+
+        Constraint.isFalse(delay == null || delay.isNegative(), "Minimum refresh delay must be greater than 0");
+        minRefreshDelay = delay;
+    }
+    
+    /**
+     * Sets the maximum amount of time between refresh intervals.
+     * 
+     * @param delay maximum amount of time, in milliseconds, between refresh intervals
+     */
+    public void setMaxRefreshDelay(@Positive @Nonnull final Duration delay) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+        
+        Constraint.isFalse(delay == null || delay.isNegative(), "Maximum refresh delay must be greater than 0");
+        maxRefreshDelay = delay;
+    }
+    
+    /**
+     * Refreshes the metadata from its source.
+     * 
+     * @throws ResolverException thrown is there is a problem retrieving and processing the metadata
+     */
+    public synchronized void refresh() throws ResolverException {
+        final Instant now = Instant.now();
+        final String mdId = getMetadataIdentifier();
+
+        Duration refreshDelay = null;
+        
+        log.debug("Beginning refresh of metadata from '{}'", mdId);
+        try {
+            final byte[] mdBytes = fetchMetadata();
+            if (mdBytes == null) {
+                log.debug("Metadata from '{}' has not changed since last refresh", mdId);
+            } else {
+                log.debug("Processing new metadata from '{}'", mdId);
+                final List<Value> resolvedInformation = parse(mdBytes);
+                final JsonBackingStore newBackingStore = new JsonBackingStore();
+                for (final Value information : resolvedInformation) {
+                    final Key id = getKey(information);
+                    log.info("Parsed entity information for {}", id);
+                    newBackingStore.getIndexedInformation().put(id, Arrays.asList(information));
+                    newBackingStore.getOrderedInformation().add(information);                    
+                }
+                setBackingStore(newBackingStore);
+                lastUpdate = now;
+            }
+        } catch (final Throwable t) {
+            log.error("Error occurred while attempting to refresh metadata from '" + mdId + "'", t);
+            refreshDelay = minRefreshDelay;
+            if (t instanceof Exception) {
+                throw new ResolverException((Exception) t);
+            } else {
+                throw new ResolverException(String.format("Saw an error of type '%s' with message '%s'", 
+                        t.getClass().getName(), t.getMessage()));
+            }
+        } finally {
+            scheduleNextRefresh(refreshDelay);
+            lastRefresh = now;
+        }
+    }
+    
+    /**
+     * Schedules the next refresh. If the given delay is 0 or null, then {@link #maxRefreshDelay} is used.
+     * @param delay The delay before the next refresh.
+     */
+    protected void scheduleNextRefresh(@Nullable final Duration delay) {
+        refreshMetadataTask = new RefreshMetadataTask();
+        Duration refreshDelay = delay;
+        if (delay == null || delay.isZero()) {
+            refreshDelay = maxRefreshDelay;
+        }
+        nextRefresh = Instant.now().plus(refreshDelay);
+        final long nextRefreshDelay = nextRefresh.toEpochMilli() - System.currentTimeMillis();
+
+        taskTimer.schedule(refreshMetadataTask, nextRefreshDelay);
+        log.info("Next refresh cycle for metadata provider '{}' will occur on '{}' ('{}' local time)",
+                new Object[] {getMetadataIdentifier(), nextRefresh, nextRefresh.atZone(ZoneId.systemDefault()),});
+    }
+    
+    /**
+     * Parses an entity from the byte array.
+     * 
+     * @param bytes The encoded entity
+     * @return The parsed entity
+     * 
+     * @throws ParseException if parse fails
+     */
+    protected abstract List<Value> parse(final byte[] bytes) throws ParseException;
+    
+    /**
+     * Gets the identifier for the given entity.
+     * 
+     * @param value The entity whose identifier will be returned.
+     * @return The identifier for the given entity.
+     */
+    protected abstract Key getKey(final Value value);
+    
+    /**
+     * Gets an identifier which may be used to distinguish this metadata in logging statements.
+     * 
+     * @return identifier which may be used to distinguish this metadata in logging statements
+     */
+    protected abstract String getMetadataIdentifier();
+
+    /**
+     * Fetches metadata from a source.
+     * 
+     * @return the fetched metadata, or null if the metadata is known not to have changed since the last retrieval
+     * 
+     * @throws ResolverException thrown if there is a problem fetching the metadata
+     */
+    protected abstract byte[] fetchMetadata() throws ResolverException;
+    
+    /** Background task that refreshes metadata. */
+    private class RefreshMetadataTask extends TimerTask {
+
+        /** {@inheritDoc} */
+        @Override
+        public void run() {
+            try {
+                if (!isInitialized()) {
+                    // just in case the metadata provider was destroyed before this task runs
+                    return;
+                }
+                
+                refresh();
+            } catch (final ResolverException e) {
+                // nothing to do, error message already logged by refreshMetadata()
+                return;
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/BaseStorageServiceClientInformationComponent.java b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/BaseStorageServiceClientInformationComponent.java
new file mode 100644
index 0000000..9bac215
--- /dev/null
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/BaseStorageServiceClientInformationComponent.java
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.oidc.metadata.impl;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.storage.StorageService;
+
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
+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;
+
+/**
+ * A base class for {@link ClientInformationManager} and {@link ClientInformationResolver} implementations
+ * exploiting {@link StorageService} for storing the OIDC client information.
+ */
+public abstract class BaseStorageServiceClientInformationComponent extends AbstractIdentifiableInitializableComponent {
+    
+    /** The context name in the {@link StorageService}. */
+    @Nonnull @NotEmpty public static final String CONTEXT_NAME = "oidcClientInformation";
+
+    /** The {@link StorageService} back-end to use. */
+    @NonnullAfterInit private StorageService storageService;
+    
+    /**
+     * This method checks to ensure that the {@link StorageService} back-end is not null.
+     * 
+     * {@inheritDoc}
+     */
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        
+        if (getStorageService() == null) {
+            throw new ComponentInitializationException("StorageService cannot be null");
+        }
+    }
+
+    /**
+     * Get the {@link StorageService} back-end to use.
+     * 
+     * @return the back-end to use
+     */
+    @NonnullAfterInit public StorageService getStorageService() {
+        return storageService;
+    }
+
+    /**
+     * Set the {@link StorageService} back-end to use.
+     * 
+     * @param storage the back-end to use
+     */
+    public void setStorageService(@Nonnull final StorageService storage) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+        storageService = Constraint.isNotNull(storage, "StorageService cannot be null");
+    }
+
+}
\ No newline at end of file
diff --git a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/ChainingClientInformationResolver.java b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/ChainingClientInformationResolver.java
new file mode 100644
index 0000000..8589d1e
--- /dev/null
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/ChainingClientInformationResolver.java
@@ -0,0 +1,220 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.oidc.metadata.impl;
+
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformation;
+
+import net.shibboleth.oidc.metadata.ClientInformationResolver;
+import net.shibboleth.oidc.metadata.RefreshableClientInformationResolver;
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
+import net.shibboleth.utilities.java.support.annotation.constraint.NotLive;
+import net.shibboleth.utilities.java.support.annotation.constraint.Unmodifiable;
+import net.shibboleth.utilities.java.support.component.AbstractIdentifiableInitializableComponent;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+import net.shibboleth.utilities.java.support.component.ComponentSupport;
+import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
+import net.shibboleth.utilities.java.support.resolver.ResolverException;
+
+/**
+ * A client information provider that uses registered resolvers, in turn, to answer queries.
+ * 
+ * The Iterable of client informations returned is the first non-null and non-empty Iterable found while iterating over
+ * the registered resolvers in resolver list order.
+ */
+public class ChainingClientInformationResolver extends AbstractIdentifiableInitializableComponent implements 
+    ClientInformationResolver, RefreshableClientInformationResolver {
+
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(ChainingClientInformationResolver.class);
+    
+    /** Registered resolvers. */
+    @Nonnull @NonnullElements private List<ClientInformationResolver> resolvers;
+
+    /** Constructor. */
+    public ChainingClientInformationResolver() {
+        resolvers = Collections.emptyList();
+    }
+    
+    /**
+     * Get an immutable the list of currently registered resolvers.
+     * 
+     * @return list of currently registered resolvers
+     */
+    @Nonnull @NonnullElements @Unmodifiable @NotLive public List<ClientInformationResolver> getResolvers() {
+        return resolvers;
+    }
+
+    /**
+     * Set the registered client information resolvers.
+     * 
+     * @param newResolvers the client information resolvers to use
+     * 
+     * @throws ResolverException thrown if there is a problem adding the client information resolvers
+     */
+    public void setResolvers(@Nullable @NonnullElements final List<? extends ClientInformationResolver> newResolvers)
+            throws ResolverException {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+
+        if (newResolvers == null || newResolvers.isEmpty()) {
+            resolvers = Collections.emptyList();
+        } else {
+            resolvers = List.copyOf(newResolvers);
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    @Nullable public OIDCClientInformation resolveSingle(@Nullable final CriteriaSet criteria) 
+            throws ResolverException {
+        ComponentSupport.ifNotInitializedThrowUninitializedComponentException(this);
+
+        final Iterable<OIDCClientInformation> iterable = resolve(criteria);
+        if (iterable != null) {
+            final Iterator<OIDCClientInformation> iterator = iterable.iterator();
+            if (iterator != null && iterator.hasNext()) {
+                return iterator.next();
+            }
+        }
+        return null;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    @Nonnull public Iterable<OIDCClientInformation> resolve(@Nullable final CriteriaSet criteria) 
+            throws ResolverException {
+        ComponentSupport.ifNotInitializedThrowUninitializedComponentException(this);
+
+        for (final ClientInformationResolver resolver : resolvers) {
+            try {
+                final Iterable<OIDCClientInformation> clientInformations = resolver.resolve(criteria);
+                if (clientInformations != null && clientInformations.iterator().hasNext()) {
+                    return clientInformations;
+                }
+            } catch (final ResolverException e) {
+                log.warn("Error retrieving client information from resolver of type {}, proceeding to next resolver",
+                        resolver.getClass().getName(), e);
+                continue;
+            }
+        }
+
+        return Collections.emptyList();
+    }
+
+    /** {@inheritDoc} */
+    @Override public void refresh() throws ResolverException {
+        for (final ClientInformationResolver resolver : resolvers) {
+            if (resolver instanceof RefreshableClientInformationResolver) {
+                ((RefreshableClientInformationResolver) resolver).refresh();
+            }
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    @Nullable public Instant getLastUpdate() {
+        Instant ret = null;
+        for (final ClientInformationResolver resolver : resolvers) {
+            if (resolver instanceof RefreshableClientInformationResolver) {
+                final Instant lastUpdate = ((RefreshableClientInformationResolver) resolver).getLastUpdate();
+                if (ret == null || ret.isBefore(lastUpdate)) {
+                    ret = lastUpdate;
+                }
+            }
+        }
+        
+        return ret;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    @Nullable public Instant getLastRefresh() {
+        Instant ret = null;
+        for (final ClientInformationResolver resolver : resolvers) {
+            if (resolver instanceof RefreshableClientInformationResolver) {
+                final Instant lastRefresh = ((RefreshableClientInformationResolver) resolver).getLastRefresh();
+                if (ret == null || ret.isBefore(lastRefresh)) {
+                    ret = lastRefresh;
+                }
+            }
+        }
+        
+        return ret;
+    }
+    
+    /** {@inheritDoc} */
+    @Override protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        if (resolvers == null) {
+            log.warn("ChainingClientInformationResolver was not configured with any member " + 
+                    "ClientInformationResolvers");
+            resolvers = Collections.emptyList();
+        } else {
+            final List<String> resolverDetails = new ArrayList<>();
+            for (final ClientInformationResolver resolver : resolvers) {
+                resolverDetails.add(resolver.getId() + ": " + countClients(resolver) + " clients");
+            }
+            log.info("ChainingClientInformationResolver was configured with the following resolvers: {}",
+                    resolverDetails);
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void doDestroy() {
+        super.doDestroy();
+        resolvers = Collections.emptyList();
+    }
+    
+    /**
+     * Counts the clients found from the given resolver.
+     * 
+     * @param resolver The resolver whose clients are counted.
+     * @return The amount of resolvable clients.
+     */
+    protected int countClients(final ClientInformationResolver resolver) {
+        int count = 0;
+        final Iterable<OIDCClientInformation> iterable;
+        try {
+            iterable = resolver.resolve(new CriteriaSet());
+        } catch (final ResolverException e) {
+            log.warn("ChainingClientInformationResolver could not count clients for {}", resolver.getId());
+            return 0;
+        }
+        if (iterable != null) {
+            final Iterator<OIDCClientInformation> iterator = iterable.iterator();
+            while (iterator.hasNext()) {
+                iterator.next();
+                count++;
+            }
+        }
+        return count;
+    }
+
+}
\ No newline at end of file
diff --git a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/ClientInformationNodeProcessor.java b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/ClientInformationNodeProcessor.java
new file mode 100644
index 0000000..1ae17dc
--- /dev/null
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/ClientInformationNodeProcessor.java
@@ -0,0 +1,587 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.oidc.metadata.impl;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.xml.namespace.QName;
+
+import org.opensaml.core.xml.XMLObject;
+import org.opensaml.saml.criterion.RoleDescriptorCriterion;
+import org.opensaml.saml.metadata.resolver.RoleDescriptorResolver;
+import org.opensaml.saml.metadata.resolver.filter.FilterException;
+import org.opensaml.saml.metadata.resolver.filter.MetadataNodeProcessor;
+import org.opensaml.saml.saml2.metadata.AssertionConsumerService;
+import org.opensaml.saml.saml2.metadata.EntityDescriptor;
+import org.opensaml.saml.saml2.metadata.Extensions;
+import org.opensaml.saml.saml2.metadata.NameIDFormat;
+import org.opensaml.saml.saml2.metadata.RoleDescriptor;
+import org.opensaml.saml.saml2.metadata.SPSSODescriptor;
+import org.opensaml.saml.security.impl.MetadataCredentialResolver;
+import org.opensaml.security.credential.Credential;
+import org.opensaml.xmlsec.keyinfo.KeyInfoCredentialResolver;
+import org.opensaml.xmlsec.keyinfo.impl.BasicProviderKeyInfoCredentialResolver;
+import org.opensaml.xmlsec.keyinfo.impl.KeyInfoProvider;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.nimbusds.jose.EncryptionMethod;
+import com.nimbusds.jose.JWEAlgorithm;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.jwk.JWK;
+import com.nimbusds.jose.jwk.JWKSet;
+import com.nimbusds.oauth2.sdk.GrantType;
+import com.nimbusds.oauth2.sdk.ResponseType;
+import com.nimbusds.oauth2.sdk.Scope;
+import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod;
+import com.nimbusds.oauth2.sdk.auth.Secret;
+import com.nimbusds.oauth2.sdk.id.ClientID;
+import com.nimbusds.oauth2.sdk.id.SoftwareID;
+import com.nimbusds.oauth2.sdk.id.SoftwareVersion;
+import com.nimbusds.openid.connect.sdk.SubjectType;
+import com.nimbusds.openid.connect.sdk.claims.ACR;
+import com.nimbusds.openid.connect.sdk.rp.ApplicationType;
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformation;
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientMetadata;
+
+import net.shibboleth.oidc.saml.xmlobject.Constants;
+import net.shibboleth.oidc.saml.xmlobject.DefaultAcrValue;
+import net.shibboleth.oidc.saml.xmlobject.MetadataValueSAMLObject;
+import net.shibboleth.oidc.saml.xmlobject.OAuthRPExtensions;
+import net.shibboleth.oidc.security.credential.NimbusSecretCredential;
+import net.shibboleth.oidc.security.impl.CredentialConversionUtil;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+import net.shibboleth.utilities.java.support.primitive.StringSupport;
+import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
+import net.shibboleth.utilities.java.support.resolver.ResolverException;
+
+/**
+ * An implementation of {@link MetadataNodeProcessor} which supports adding an instance of
+ * {@link OIDCClientInformation} to the object metadata of {@link SPSSODescriptor}. The data
+ * is fetched mainly via {@link OAuthRPExtensions}.
+ */
+public class ClientInformationNodeProcessor implements MetadataNodeProcessor {
+
+    /** The ACS binding identifier matching to the redirect_uri. */
+    public static final String BINDING_ID_REDIRECT_URI = "https://tools.ietf.org/html/rfc6749#section-3.1.2";
+    
+    /** Class logger. */
+    private final Logger log = LoggerFactory.getLogger(ClientInformationNodeProcessor.class);
+    
+    /** The {@link KeyInfoCredentialResolver} to be used for the resolution. */
+    private final @Nonnull KeyInfoCredentialResolver keyInfoCredentialResolver;
+    
+    /**
+     * Constructor.
+     * 
+     * @param keyInfoProviders The list of key info providers.
+     */
+    public ClientInformationNodeProcessor(@Nonnull final List<KeyInfoProvider> keyInfoProviders) {
+        keyInfoCredentialResolver = new BasicProviderKeyInfoCredentialResolver(keyInfoProviders);
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    public void process(final @Nullable XMLObject metadataNode) throws FilterException {
+        if (metadataNode instanceof SPSSODescriptor) {
+            final SPSSODescriptor roleDescriptor = (SPSSODescriptor) metadataNode;
+            if (roleDescriptor.isSupportedProtocol(Constants.OIDC_PROTOCOL_URI)) {
+                final ClientID clientId = parseClientID(roleDescriptor);
+                if (clientId == null || StringSupport.trimOrNull(clientId.getValue()) == null) {
+                    log.error("Could not find a value for client_id, nothing to do");
+                    return;
+                }
+                final Iterable<Credential> credentials = resolveCredentials(roleDescriptor);
+                final OIDCClientMetadata metadata = populateMetadata(roleDescriptor, credentials, clientId.getValue());
+                final Secret clientSecret = parseClientSecret(credentials);
+                final OIDCClientInformation clientInformation =
+                        new OIDCClientInformation(clientId, null, metadata, clientSecret);
+                metadataNode.getObjectMetadata().put(clientInformation);
+            }
+        } else {
+            log.trace("Ignoring object {}", metadataNode);
+        }
+    }
+
+    /**
+     * Converts the entityID of the given {@link SPSSODescriptor} into a {@link ClientID}. The value is fetched from
+     * the {@link EntityDescriptor}, expected to be the parent element of the given role decriptor.
+     * 
+     * @param roleDescriptor The {@link SPSSODescriptor} to be used as a source.
+     * @return The entityID value as {@link ClientID}.
+     */
+    protected @Nullable ClientID parseClientID(final @Nonnull SPSSODescriptor roleDescriptor) {
+        if (!roleDescriptor.hasParent() || !(roleDescriptor.getParent() instanceof EntityDescriptor)) {
+            log.warn("Unexpected structure, EntityDescriptor not as a parent for OAuthRPRoleDescriptor");
+            return null;
+        }
+        final EntityDescriptor entityDescriptor = (EntityDescriptor) roleDescriptor.getParent();
+        return new ClientID(entityDescriptor.getEntityID());
+    }
+    
+    /**
+     * Fetches the client secret from given the set of {@link Credential}s. The first credential matching the type
+     * {@link NimbusSecretCredential} is used as the source.
+     * 
+     * @param credentials The source set of {@link Credential}s.
+     * @return The client secret as {@link Secret}.
+     */
+    protected @Nullable Secret parseClientSecret(final @Nonnull Iterable<Credential> credentials) {
+        for (final Credential credential : credentials) {
+            log.trace("Processing credential type {}", credential.getCredentialType());
+            if (NimbusSecretCredential.class.isAssignableFrom(credential.getCredentialType())) {
+                log.debug("Found client secret from the credentials");
+                return ((NimbusSecretCredential) credential).getSecret();
+            }
+        }
+        log.trace("No client secret found from the credentials");
+        return null;
+    }
+
+    /**
+     * Populates the {@link OIDCClientMetadata} using the values found from the given {@link SPSSODescriptor}, the set
+     * of {@link Credential}s and the client ID.
+     * 
+     * @param roleDescriptor The {@link SPSSODescriptor} to be used as a source.
+     * @param credentials The source set of {@link Credential}s to be used for client secret and remote/local JWKS.
+     * @param clientId The client ID.
+     * @return The {@link OIDCClientMetadata} parsed from the given parameters.
+     */
+    protected @Nonnull OIDCClientMetadata populateMetadata(final @Nonnull SPSSODescriptor roleDescriptor,
+            final @Nonnull Iterable<Credential> credentials, final @Nonnull String clientId) {
+        final OIDCClientMetadata metadata = new OIDCClientMetadata();
+        final OAuthRPExtensions extensions = getOAuthRPExtensions(roleDescriptor);
+        if (extensions != null) {
+            metadata.setApplicationType(parseApplicationType(extensions));
+            metadata.setURI(getSingleURIValue(extensions.getClientUri()));
+            metadata.setDefaultACRs(parseDefaultAcrValues(extensions));
+            metadata.setGrantTypes(parseGrantTypes(extensions));
+            metadata.setIDTokenJWEAlg(parseJweAlgorithm(extensions.getIdTokenEncryptedResponseAlg()));
+            metadata.setIDTokenJWEEnc(parseEncryptionMethod(extensions.getIdTokenEncryptedResponseEnc()));
+            metadata.setIDTokenJWSAlg(parseJwsAlgorithm(extensions.getIdTokenSignedResponseAlg()));
+            metadata.setInitiateLoginURI(getSingleURIValue(extensions.getInitiateLoginUri()));
+            metadata.setJWKSetURI(getSingleURIValue(extensions.getJwksUri()));
+            metadata.setPostLogoutRedirectionURIs(parseUris(extensions.getPostLogoutRedirectUris()));
+            metadata.setRedirectionURIs(parseRedirectUris(roleDescriptor));
+            metadata.setRequestObjectJWEAlg(parseJweAlgorithm(extensions.getRequestObjectEncryptionAlg()));
+            metadata.setRequestObjectJWEEnc(parseEncryptionMethod(extensions.getRequestObjectEncryptionEnc()));
+            metadata.setRequestObjectJWSAlg(parseJwsAlgorithm(extensions.getRequestObjectSigningAlg()));
+            metadata.setRequestObjectURIs(parseUris(extensions.getRequestUris()));
+            metadata.setResponseTypes(parseResponseTypes(extensions));
+            metadata.setScope(parseScope(extensions));
+            metadata.setSectorIDURI(getSingleURIValue(extensions.getSectorIdentifierUri()));
+            final String softwareId = getSingleValue(extensions.getSoftwareId());
+            if (softwareId != null) {
+                metadata.setSoftwareID(new SoftwareID(softwareId));
+            }
+            final String softwareVersion = getSingleValue(extensions.getSoftwareVersion());
+            if (softwareVersion != null) {
+                metadata.setSoftwareVersion(new SoftwareVersion(softwareVersion));
+            }
+            metadata.setSubjectType(parseSubjectType(roleDescriptor));
+            metadata.setTokenEndpointAuthMethod(parseClientAuthenticationMethod(extensions));
+            metadata.setTokenEndpointAuthJWSAlg(parseJwsAlgorithm(extensions.getTokenEndpointAuthSigningAlg()));
+            metadata.setUserInfoJWEAlg(parseJweAlgorithm(extensions.getUserInfoEncryptedResponseAlg()));
+            metadata.setUserInfoJWEEnc(parseEncryptionMethod(extensions.getUserInfoEncryptedResponseEnc()));
+            metadata.setUserInfoJWSAlg(parseJwsAlgorithm(extensions.getUserInfoSignedResponseAlg()));
+            metadata.setJWKSet(parseJwkSet(credentials, clientId));
+           
+        } else {
+            log.debug("No OAuthRPExtensions found to be processed");
+        }
+        return metadata;
+    }
+    
+    /**
+     * Get the {@link OAuthRPExtensions} from the given {@link SPSSODescriptor}, it it was found from its extensions.
+     * 
+     * @param roleDescriptor The role descriptor to get the extensions from.
+     * @return The extensions, if they were found from the role descriptor. <code>null</code> otherwise.
+     */
+    protected @Nullable OAuthRPExtensions getOAuthRPExtensions(final SPSSODescriptor roleDescriptor) {
+        final Extensions extensions = roleDescriptor.getExtensions();
+        if (extensions == null) {
+            log.debug("No extensions found from the given SPSSODescriptor");
+            return null;
+        }
+        final List<XMLObject> rpExtensions = extensions.getUnknownXMLObjects();
+        if (rpExtensions == null || rpExtensions.isEmpty()) {
+            log.debug("The extension element on SPSSODescriptor didn't have any child elements");
+            return null;
+        }
+        if (rpExtensions.size() > 1) {
+            log.warn("More than one OAuthRPExtensions defined, using only one of them");
+        }
+        if (rpExtensions.get(0) instanceof OAuthRPExtensions) {
+            log.debug("Successfully parsed OAuthRPExtensions");
+            return (OAuthRPExtensions) rpExtensions.get(0);
+        }
+        log.warn("Could not parse OAuthRPExtensions from the element");
+        return null;
+    }
+    
+    /**
+     * Get all the credentials attached to the given {@link SPSSODescriptor}. They are resolved using the
+     * {@link #keyInfoCredentialResolver}.
+     * 
+     * @param roleDescriptor The role descriptor to parse the credentials from.
+     * @return All the resolved credentials. Or empty set if none was found.
+     */
+    protected @Nonnull Iterable<Credential> resolveCredentials(final SPSSODescriptor roleDescriptor) {
+        final MetadataCredentialResolver credentialResolver = new MetadataCredentialResolver();
+        credentialResolver.setKeyInfoCredentialResolver(keyInfoCredentialResolver);
+        credentialResolver.setRoleDescriptorResolver(new SkeletonEchoingRoleDescriptorResolver() {
+
+            /** {@inheritDoc} */
+            @Override public RoleDescriptor resolveSingle(final CriteriaSet criteria) throws ResolverException {
+                return roleDescriptor;
+            }
+
+        });
+
+        final RoleDescriptorCriterion criterion = new RoleDescriptorCriterion(roleDescriptor);
+        final CriteriaSet criteriaSet = new CriteriaSet();
+        criteriaSet.add(criterion);
+        try {
+            credentialResolver.initialize();
+        } catch (final ComponentInitializationException e) {
+            log.error("Could not initialize the SAML metadata credential resolver, cannot resolve JWKSet", e);
+        }
+        try {
+            return credentialResolver.resolve(criteriaSet);
+        } catch (final ResolverException e) {
+            log.warn("Could not resolve credentials", e);
+        }
+        return Collections.emptySet();
+    }
+    
+    /**
+     * Convert the given credentials into the Nimbus {@link JWKSet}.
+     * 
+     * @param credentials The set to be converted.
+     * @param clientId The client ID related to the credentials.
+     * @return The given credentials converted into a JWKSet.
+     */
+    protected @Nullable JWKSet parseJwkSet(final @Nonnull Iterable<Credential> credentials, 
+            final @Nonnull String clientId) {
+        final List<JWK> jwks = new ArrayList<>();
+        for (final Credential credential : credentials) {
+            final JWK jwk = CredentialConversionUtil.credentialToKey(credential);
+            if (jwk == null) {
+                log.debug("Could not parse credential of {} to a JWK", clientId);
+            } else {
+                log.trace("Successfully parsed a JWK to client {}: {}", clientId, jwk.toJSONString());
+                jwks.add(jwk);
+            }
+        }
+        return jwks.isEmpty() ? null : new JWKSet(jwks);
+    }
+    
+    /**
+     * Get all objects of the given type from the given extensions.
+     * 
+     * @param extension The extensions to get the object from.
+     * @param type The type of the objects to get.
+     * @return The list of objects of the given type.
+     */
+    protected @Nonnull List<MetadataValueSAMLObject> getFromExtensions(final @Nonnull Extensions extension, 
+            final @Nonnull QName type) {
+        final List<XMLObject> list = extension.getUnknownXMLObjects(type);
+        final List<MetadataValueSAMLObject> result = new ArrayList<>();
+        for (final XMLObject object : list) {
+            if (object instanceof MetadataValueSAMLObject) {
+                result.add((MetadataValueSAMLObject) object);
+            }
+        }
+        return result;
+    }
+    
+    /**
+     * Parse the {@link ClientAuthenticationMethod} from the given extensions.
+     * 
+     * @param extensions The extensions to parse from.
+     * @return The client authentication method, or <code>null</code> it was not found.
+     */
+    protected @Nullable ClientAuthenticationMethod parseClientAuthenticationMethod(
+            final @Nonnull OAuthRPExtensions extensions) {
+        final String metadataValue = getSingleValue(extensions.getTokenEndpointAuthMethod());
+        if (metadataValue == null) {
+            return null;
+        }
+        return ClientAuthenticationMethod.parse(metadataValue);
+    }
+    
+    /**
+     * Parse the {@link ApplicationType} from the given extensions.
+     * 
+     * @param extensions The extensions to parse from.
+     * @return {@link ApplicationType#NATIVE} if it was defined in the extensions, {@link ApplicationType#WEB}
+     * otherwise.
+     */
+    protected @Nonnull ApplicationType parseApplicationType(final @Nonnull OAuthRPExtensions extensions) {
+        final String metadataValue = getSingleValue(extensions.getApplicationType());
+        if (ApplicationType.NATIVE.toString().equalsIgnoreCase(metadataValue)) {
+            return ApplicationType.NATIVE;
+        }
+        return ApplicationType.WEB;
+    }
+    
+    /**
+     * Parse the {@link SubjectType} from the given role descriptor's name ID formats.
+     * 
+     * @param roleDescriptor The role descriptor to parse from. Only the first nameID definition is taken into
+     * consideration.
+     * @return {@link SubjectType#PAIRWISE} if <code>pairwise</code> was defined as the name ID format.
+     * {@link SubjectType#PUBLIC} otherwise.
+     */
+    protected @Nonnull SubjectType parseSubjectType(final @Nonnull SPSSODescriptor roleDescriptor) {
+        final List<NameIDFormat> nameIdFormats = roleDescriptor.getNameIDFormats();
+        if (nameIdFormats == null || nameIdFormats.isEmpty()) {
+            log.warn("No NameIDFormat defined, using public");
+            return SubjectType.PUBLIC;
+        }
+        if (nameIdFormats.size() > 1) {
+            log.warn("Multiple NameIDFormats defined, using first one");
+        }
+        final String metadataValue = nameIdFormats.get(0).getURI();
+        if (SubjectType.PAIRWISE.toString().equalsIgnoreCase(metadataValue)) {
+            return SubjectType.PAIRWISE;
+        }
+        return SubjectType.PUBLIC;
+    }
+    
+    /**
+     * Parse the default {@link ACR} values from the given extensions.
+     *
+     * @param extensions The extensions to parse from.
+     * @return The list of ACR values that were found.
+     */
+    protected @Nonnull List<ACR> parseDefaultAcrValues(final @Nonnull OAuthRPExtensions extensions) {
+        final List<ACR> acrs = new ArrayList<>();
+        for (final DefaultAcrValue acr : extensions.getDefaultAcrValues()) {
+            final String value = getSingleValue(acr);
+            if (value != null) {
+                acrs.add(new ACR(value));
+            }
+        }
+        return acrs;
+    }
+    
+    /**
+     * Parse the {@link GrantType}s from the given extensions.
+     * 
+     * @param extensions The extensions to parse from.
+     * @return The set of grant types that were found.
+     */
+    protected @Nonnull Set<GrantType> parseGrantTypes(final @Nonnull OAuthRPExtensions extensions) {
+        final Set<GrantType> grantTypes = new HashSet<>();
+        for (final net.shibboleth.oidc.saml.xmlobject.GrantType grantType : extensions.getGrantTypes()) {
+            final String value = getSingleValue(grantType);
+            if (value != null) {
+                grantTypes.add(new GrantType(value));
+            }
+        }
+        return grantTypes;
+    }
+    
+    /**
+     * Parse the {@link ResponseType}s from the given extensions.
+     * 
+     * @param extensions The extensions to parse from.
+     * @return The set of response types that were found.
+     */
+    protected @Nonnull Set<ResponseType> parseResponseTypes(final @Nonnull OAuthRPExtensions extensions) {
+        final Set<ResponseType> responseTypes = new HashSet<>();
+        for (final net.shibboleth.oidc.saml.xmlobject.ResponseType responseType : extensions.getResponseTypes()) {
+            final String value = getSingleValue(responseType);
+            if (value != null) {
+                responseTypes.add(new ResponseType(value));
+            }
+        }
+        return responseTypes;
+    }
+    
+    /**
+     * Parse the {@link Scope} from the given extensions.
+     * 
+     * @param extensions The extensions to parse from.
+     * @return The scope that was found.
+     */
+    protected @Nonnull Scope parseScope(final @Nonnull OAuthRPExtensions extensions) {
+        final Scope scope = new Scope();
+        for (final net.shibboleth.oidc.saml.xmlobject.Scope singleScope : extensions.getScopes()) {
+            final String value = getSingleValue(singleScope);
+            if (value != null) {
+                scope.add(value);
+            }
+        }
+        return scope;
+    }
+    
+    /**
+     * Parse the {@link JWEAlgorithm} from the given metadata value.
+     * 
+     * @param metadataValue The metadata value to parse from.
+     * @return The JWE algorithm, or <code>null</code> if no value was found.
+     */
+    protected @Nullable JWEAlgorithm parseJweAlgorithm(final @Nullable MetadataValueSAMLObject metadataValue) {
+        final String value = getSingleValue(metadataValue);
+        if (value != null) {
+            return new JWEAlgorithm(value);
+        }
+        return null;
+    }
+
+    /**
+     * Parse the {@link JWSAlgorithm} from the given metadata value.
+     * 
+     * @param metadataValue The metadata value to parse from.
+     * @return The JWS algorithm, or <code>null</code> if no value was found.
+     */
+    protected @Nullable JWSAlgorithm parseJwsAlgorithm(final @Nullable MetadataValueSAMLObject metadataValue) {
+        final String value = getSingleValue(metadataValue);
+        if (value != null) {
+            return new JWSAlgorithm(value);
+        }
+        return null;
+    }
+
+    /**
+     * Parse the {@link EncryptionMethod} from the given metadata value.
+     * 
+     * @param metadataValue The metadata value to parse from.
+     * @return The encryption method, or <code>null</code> if no value was found.
+     */
+    protected @Nullable EncryptionMethod parseEncryptionMethod(final @Nullable MetadataValueSAMLObject metadataValue) {
+        final String value = getSingleValue(metadataValue);
+        if (value != null) {
+            return new EncryptionMethod(value);
+        }
+        return null;
+    }
+    
+    /**
+     * Parse the redirection URIs from the given role descriptor. Only the assertion consumer service URLs whose
+     * binding matches to {@link #BINDING_ID_REDIRECT_URI} are taken into consideration.
+     * 
+     * @param roleDescriptor The role descriptor to parse from.
+     * @return The set of redirection URIs that were successfully parsed.
+     */
+    protected @Nonnull Set<URI> parseRedirectUris(final @Nonnull SPSSODescriptor roleDescriptor) {
+        final Set<URI> uris = new HashSet<>();
+        for (final AssertionConsumerService acs : roleDescriptor.getAssertionConsumerServices()) {
+            if (BINDING_ID_REDIRECT_URI.equals(acs.getBinding())) {
+                final URI uri = getSingleURIValue(acs.getLocation());
+                if (uri != null) {
+                    uris.add(uri);
+                }
+            }
+        }
+        return uris;
+    }
+    
+    /**
+     * Parse the URIs from the given list of metadata values.
+     * 
+     * @param listOfValues The list to parse from.
+     * @return Set of URIs that were successfully parsed from the list.
+     */
+    protected @Nonnull Set<URI> parseUris(final @Nonnull List<? extends MetadataValueSAMLObject> listOfValues) {
+        final Set<URI> uris = new HashSet<>();
+        for (final MetadataValueSAMLObject value : listOfValues) {
+            final URI uri = getSingleURIValue(value);
+            if (uri != null) {
+                uris.add(uri);
+            }
+        }
+        return uris;
+    }
+
+    /**
+     * Gets the trimmed {@link String} value from the given metadata value object.
+     * 
+     * @param metadataValue The object to get the value from.
+     * @return The value as trimmed value, or <code>null</code> if no value was found.
+     */
+    protected @Nullable String getSingleValue(final @Nullable MetadataValueSAMLObject metadataValue) {
+        if (metadataValue != null) {
+            return StringSupport.trimOrNull(metadataValue.getValue());
+        }
+        return null;
+    }
+    
+    /**
+     * Converts the metadata value object value into a {@link URI}.
+     * 
+     * @param metadataValue The metadata object value to convert from.
+     * @return The value as URI if it was successfully parsed, <code>null</code> otherwise.
+     */
+    protected @Nullable URI getSingleURIValue(final @Nonnull MetadataValueSAMLObject metadataValue) {
+        return getSingleURIValue(getSingleValue(metadataValue));
+    }
+    
+    /**
+     * Converts the given {@link String} into a {@link URI}.
+     * 
+     * @param value The raw string value.
+     * @return The value as URI if it was successfully parsed, <code>null</code> otherwise.
+     */
+    protected @Nullable URI getSingleURIValue(final @Nullable String value) {
+        if (value != null) {
+            try {
+                return new URI(value);
+            } catch (final URISyntaxException e) {
+                log.warn("Could not parse {} into an URI", value, e);
+            }
+        }
+        return null;
+    }
+    
+    protected abstract class SkeletonEchoingRoleDescriptorResolver implements RoleDescriptorResolver {
+
+        /** {@inheritDoc} */
+        @Override  public Iterable<RoleDescriptor> resolve(final CriteriaSet criteria) throws ResolverException {
+            return Arrays.asList(resolveSingle(criteria));
+        }
+
+        /** {@inheritDoc} */
+        @Override  public String getId() {
+            return "EmbeddedLocalRoleDescriptorResolver";
+        }
+
+        /** {@inheritDoc} */
+        @Override  public boolean isRequireValidMetadata() {
+            return false;
+        }
+
+        /** {@inheritDoc} */
+        @Override public void setRequireValidMetadata(final boolean requireValidMetadata) {
+            // no op
+        }         
+    }
+    
+}
\ No newline at end of file
diff --git a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/FilesystemClientInformationResolver.java b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/FilesystemClientInformationResolver.java
new file mode 100644
index 0000000..6f7949d
--- /dev/null
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/FilesystemClientInformationResolver.java
@@ -0,0 +1,206 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.oidc.metadata.impl;
+
+import java.io.IOException;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Timer;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.core.io.Resource;
+
+import com.nimbusds.oauth2.sdk.ParseException;
+import com.nimbusds.oauth2.sdk.id.ClientID;
+import com.nimbusds.oauth2.sdk.util.JSONArrayUtils;
+import com.nimbusds.oauth2.sdk.util.JSONObjectUtils;
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformation;
+
+import net.minidev.json.JSONArray;
+import net.minidev.json.JSONObject;
+import net.shibboleth.oidc.jwk.RemoteJwkSetCache;
+import net.shibboleth.oidc.metadata.ClientInformationResolver;
+import net.shibboleth.oidc.metadata.RefreshableClientInformationResolver;
+import net.shibboleth.oidc.metadata.criterion.ClientIDCriterion;
+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.CriteriaSet;
+import net.shibboleth.utilities.java.support.resolver.ResolverException;
+
+/**
+ * Based on {@link org.opensaml.saml.metadata.resolver.impl.FilesystemMetadataResolver}.
+ */
+public class FilesystemClientInformationResolver extends AbstractFileOIDCEntityResolver<ClientID, OIDCClientInformation>
+        implements ClientInformationResolver, RefreshableClientInformationResolver {
+
+    /** Class logger. */
+    private final Logger log = LoggerFactory.getLogger(FilesystemClientInformationResolver.class);
+
+    /** The cache for remote JWK key sets. */
+    private RemoteJwkSetCache remoteJwkSetCache;
+
+    /** The remote key refresh interval. Default value: 30 minutes. */
+    @Positive
+    private Duration keyFetchInterval = Duration.ofMinutes(30);
+
+    /**
+     * Constructor.
+     * 
+     * @param metadata the metadata file
+     * 
+     * @throws IOException If the metedata cannot be loaded.
+     */
+    public FilesystemClientInformationResolver(@Nonnull final Resource metadata) throws IOException {
+        super(metadata);
+    }
+
+    /**
+     * Constructor.
+     * 
+     * @param metadata the metadata file
+     * @param backgroundTaskTimer timer used to refresh metadata in the background
+     * 
+     * @throws IOException If the metedata cannot be loaded.
+     */
+    public FilesystemClientInformationResolver(@Nullable final Timer backgroundTaskTimer,
+            @Nonnull final Resource metadata) throws IOException {
+        super(backgroundTaskTimer, metadata);
+    }
+    
+    /** {@inheritDoc} */
+    @Override protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        if (remoteJwkSetCache == null) {
+            log.warn("The RemoteJwkSetCache is not defined, the remote keys are not fetched automatically");
+        }
+    }
+
+    /**
+     * Set the cache for remote JWK key sets.
+     * 
+     * @param jwkSetCache What to set.
+     */
+    public void setRemoteJwkSetCache(final RemoteJwkSetCache jwkSetCache) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+        remoteJwkSetCache = Constraint.isNotNull(jwkSetCache, "The remote JWK set cache cannot be null");
+    }
+
+    /**
+     * Set the remote key refresh interval.
+     * 
+     * @param interval What to set.
+     */
+    public void setKeyFetchInterval(@Positive final Duration interval) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+
+        Constraint.isFalse(interval == null || interval.isNegative(), "Remote key refresh must be greater than 0");
+        keyFetchInterval = interval;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Iterable<OIDCClientInformation> resolve(final CriteriaSet criteria) throws ResolverException {
+        ComponentSupport.ifNotInitializedThrowUninitializedComponentException(this);
+        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+
+        final ClientIDCriterion clientIdCriterion = criteria.get(ClientIDCriterion.class);
+        if (clientIdCriterion == null || clientIdCriterion.getClientID() == null) {
+            log.trace("No client ID criteria found, returning all");
+            return updateKeys(getBackingStore().getOrderedInformation());
+        }
+        // TODO: support other criterion
+        return updateKeys(lookupIdentifier(clientIdCriterion.getClientID()));
+    }
+
+    /**
+     * Updates the key set in the given list of OIDC client informations. The configured remote JWK set cache is
+     * exploited.
+     * 
+     * @param clientInformations The OIDC client informations whose keys are going to be updated.
+     * 
+     * @return The OIDC client informations, containing contents of getJWKSetURI() in getJWKSet().
+     */
+    protected List<OIDCClientInformation> updateKeys(final List<OIDCClientInformation> clientInformations) {
+        final List<OIDCClientInformation> result = new ArrayList<>();
+        for (final OIDCClientInformation clientInformation : clientInformations) {
+            if (clientInformation.getOIDCMetadata().getJWKSetURI() != null && remoteJwkSetCache != null) {
+                clientInformation.getOIDCMetadata().setJWKSet(
+                        remoteJwkSetCache.fetch(clientInformation.getOIDCMetadata().getJWKSetURI(),
+                                Instant.now().plus(keyFetchInterval)));
+            }
+            result.add(clientInformation);
+        }
+        return result;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public OIDCClientInformation resolveSingle(@Nullable final CriteriaSet criteria) throws ResolverException {
+        final Iterable<OIDCClientInformation> iterable = resolve(criteria);
+        if (iterable != null) {
+            final Iterator<OIDCClientInformation> iterator = iterable.iterator();
+            if (iterator != null && iterator.hasNext()) {
+                return iterator.next();
+            }
+        }
+        log.warn("Could not find any clients with the given criteria");
+        return null;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected List<OIDCClientInformation> parse(@Nonnull final byte[] bytes) throws ParseException {
+        final String rawString = new String(bytes);
+        try {
+            final OIDCClientInformation single = OIDCClientInformation.parse(JSONObjectUtils.parse(rawString));
+            log.debug("Found single client information from the file");
+            return Arrays.asList(single);
+        } catch (final ParseException e) {
+            log.debug("Could not parse single client information from the file, checking for array");
+        }
+        try {
+            final JSONArray parsedArray = JSONArrayUtils.parse(rawString);
+            final List<OIDCClientInformation> result = new ArrayList<OIDCClientInformation>();
+            for (final Object object : parsedArray) {
+                final OIDCClientInformation client = OIDCClientInformation.parse((JSONObject) object);
+                result.add(client);
+            }
+            return result;
+        } catch (final ParseException e) {
+            throw new ParseException("Could not parse a single or an array of OIDC client information object(s).");
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected ClientID getKey(@Nonnull final OIDCClientInformation value) {
+        return value.getID();
+    }
+}
diff --git a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/PropertiesClientSecretValueResolver.java b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/PropertiesClientSecretValueResolver.java
new file mode 100644
index 0000000..338c4f0
--- /dev/null
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/PropertiesClientSecretValueResolver.java
@@ -0,0 +1,125 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.oidc.metadata.impl;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Properties;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.core.io.Resource;
+
+import net.shibboleth.oidc.metadata.criterion.ClientSecretReferenceCriterion;
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
+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.logic.ConstraintViolationException;
+import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
+import net.shibboleth.utilities.java.support.resolver.ResolverException;
+
+/**
+ * A client secret value resolver that fetches the values from the given Properties file.
+ */
+public class PropertiesClientSecretValueResolver extends AbstractClientSecretValueResolver {
+
+    /** Class logger. */
+    private final Logger log = LoggerFactory.getLogger(PropertiesClientSecretValueResolver.class);
+
+    /** The properties resource containing client secret values. */
+    @NonnullAfterInit private Resource propertiesResource;
+    
+    /**
+     * Set the properties resource containing client secret values.
+     * 
+     * @param resource The properties resource containing client secret values.
+     */
+    public void setResource(@Nonnull final Resource resource) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        propertiesResource = Constraint.isNotNull(resource, "Properties resource cannot be null");
+        final Properties properties = new Properties();
+        try {
+            properties.load(propertiesResource.getInputStream());
+        } catch (final IOException e) {
+            log.error("Could not read the properties from the given resource", e);
+            throw new ConstraintViolationException("The properties resource must be readable");
+        }
+    }
+    
+    /**
+     * Get the properties resource containing client secret values.
+     * 
+     * @return The properties resource containing client secret values.
+     */
+    public @NonnullAfterInit Resource getResource() {
+        return propertiesResource;
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    public @Nonnull Iterable<String> resolve(@Nonnull final CriteriaSet criteria) throws ResolverException {
+        final String value = resolveSingle(criteria);
+        if (value != null) {
+            final LazySet<String> result = new LazySet<>();
+            result.add(value);
+            return result;
+        }
+        return Collections.emptySet();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public @Nullable String resolveSingle(@Nonnull final CriteriaSet criteria) throws ResolverException {
+        ComponentSupport.ifNotInitializedThrowUninitializedComponentException(this);
+        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+
+        final ClientSecretReferenceCriterion criterion = criteria.get(ClientSecretReferenceCriterion.class);
+        final Properties properties = new Properties();
+        try {
+            properties.load(propertiesResource.getInputStream());
+        } catch (final IOException e) {
+            log.error("Could not read the properties from the resource", e);
+            throw new ResolverException(e);
+        }
+        return properties.getProperty(criterion.getSecretReference());
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        
+        if (getResource() == null) {
+            throw new ComponentInitializationException("Properties resource cannot be null");
+        }
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    protected void doDestroy() {
+        propertiesResource = null;
+          
+        super.doDestroy();
+    }
+
+}
diff --git a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/ReloadingRelyingPartyClientInformationProvider.java b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/ReloadingRelyingPartyClientInformationProvider.java
new file mode 100644
index 0000000..7134c03
--- /dev/null
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/ReloadingRelyingPartyClientInformationProvider.java
@@ -0,0 +1,111 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.oidc.metadata.impl;
+
+import java.util.Collections;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformation;
+
+import net.shibboleth.oidc.metadata.ClientInformationResolver;
+import net.shibboleth.utilities.java.support.component.AbstractIdentifiableInitializableComponent;
+import net.shibboleth.utilities.java.support.component.ComponentSupport;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
+import net.shibboleth.utilities.java.support.resolver.ResolverException;
+import net.shibboleth.utilities.java.support.service.ReloadableService;
+import net.shibboleth.utilities.java.support.service.ServiceableComponent;
+
+/**
+ * This class uses the service interface to implement {@link ClientInformationResolver}.
+ * Based on net.shibboleth.idp.saml.metadata.impl.ReloadingRelyingPartyMetadataProvider.
+ */
+public class ReloadingRelyingPartyClientInformationProvider extends AbstractIdentifiableInitializableComponent 
+    implements ClientInformationResolver {
+
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(ReloadingRelyingPartyClientInformationProvider.class);
+
+    /** The service which managed the reloading. */
+    private final ReloadableService<ClientInformationResolver> service;
+
+    /**
+     * Constructor.
+     * 
+     * @param resolverService the service which will manage the loading.
+     */
+    public ReloadingRelyingPartyClientInformationProvider(
+            @Nonnull final ReloadableService<ClientInformationResolver> resolverService) {
+        service = Constraint.isNotNull(resolverService, "ClientInformationResolver Service cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    @Override @Nonnull public Iterable<OIDCClientInformation> resolve(final CriteriaSet criteria) 
+            throws ResolverException {
+
+        ComponentSupport.ifNotInitializedThrowUninitializedComponentException(this);
+        ServiceableComponent<ClientInformationResolver> component = null;
+        try {
+            component = service.getServiceableComponent();
+            if (null == component) {
+                log.error("RelyingPartyClientInformationProvider '{}': Error accessing underlying source: "
+                        + "Invalid configuration.", getId());
+            } else {
+                final ClientInformationResolver resolver = component.getComponent();
+                return resolver.resolve(criteria);
+            }
+        } catch (final ResolverException e) {
+            log.error("RelyingPartyClientInformationProvider '{}': Error during resolution", getId(), e);
+        } finally {
+            if (null != component) {
+                component.unpinComponent();
+            }
+        }
+        return Collections.emptySet();
+    }
+
+    /** {@inheritDoc} */
+    @Override @Nullable public OIDCClientInformation resolveSingle(final CriteriaSet criteria) 
+            throws ResolverException {
+
+        ComponentSupport.ifNotInitializedThrowUninitializedComponentException(this);
+        ServiceableComponent<ClientInformationResolver> component = null;
+        try {
+            component = service.getServiceableComponent();
+            if (null == component) {
+                log.error("RelyingPartyClientInformationProvider '{}': Error accessing underlying source: "
+                        + "Invalid configuration.", getId());
+            } else {
+                final ClientInformationResolver resolver = component.getComponent();
+                return resolver.resolveSingle(criteria);
+            }
+        } catch (final ResolverException e) {
+            log.error("RelyingPartyResolver '{}': Error during resolution", getId(), e);
+        } finally {
+            if (null != component) {
+                component.unpinComponent();
+            }
+        }
+        return null;
+    }
+}
diff --git a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/ResolverHelper.java b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/ResolverHelper.java
new file mode 100644
index 0000000..a4db2b5
--- /dev/null
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/ResolverHelper.java
@@ -0,0 +1,90 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.oidc.metadata.impl;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+
+import javax.annotation.Nonnull;
+
+import net.shibboleth.utilities.java.support.resolver.ResolverException;
+
+/**
+ * Helper methods for (OIDC) metadata resolution classes.
+ * 
+ * Based on {@link org.opensaml.saml.metadata.resolver.impl.FilesystemMetadataResolver} and its parent classes.
+ */
+public final class ResolverHelper {
+    
+    /**
+     * Constructor.
+     */
+    private ResolverHelper() {
+        // no op
+    }
+    
+    /**
+     * Converts an InputStream into a byte array.
+     * 
+     * @param ins input stream to convert
+     * 
+     * @return resultant byte array
+     * 
+     * @throws ResolverException thrown if there is a problem reading the resultant byte array
+     */
+    public static byte[] inputstreamToByteArray(@Nonnull final InputStream ins) throws ResolverException {
+        try {
+            // 1 MB read buffer
+            final byte[] buffer = new byte[1024 * 1024];
+            final ByteArrayOutputStream output = new ByteArrayOutputStream();
+
+            int n = 0;
+            while (-1 != (n = ins.read(buffer))) {
+                output.write(buffer, 0, n);
+            }
+
+            ins.close();
+            return output.toByteArray();
+        } catch (final IOException e) {
+            throw new ResolverException(e);
+        }
+    }
+    
+    /**
+     * Validate the basic properties of the specified metadata file, for example that it exists; 
+     * that it is a file; and that it is readable.
+     *
+     * @param file the file to evaluate
+     * @throws ResolverException if file does not pass basic properties required of a metadata file
+     */
+    public static void validateMetadataFile(@Nonnull final File file) throws ResolverException {
+        if (!file.exists()) {
+            throw new ResolverException("Metadata file '" + file.getAbsolutePath() + "' does not exist");
+        }
+
+        if (!file.isFile()) {
+            throw new ResolverException("Metadata file '" + file.getAbsolutePath() + "' is not a file");
+        }
+
+        if (!file.canRead()) {
+            throw new ResolverException("Metadata file '" + file.getAbsolutePath() + "' is not readable");
+        }
+    }
+}
diff --git a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/ResolverServiceClientSecretValueResolver.java b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/ResolverServiceClientSecretValueResolver.java
new file mode 100644
index 0000000..649f95b
--- /dev/null
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/ResolverServiceClientSecretValueResolver.java
@@ -0,0 +1,161 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.oidc.metadata.impl;
+
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.core.criterion.EntityIdCriterion;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import net.shibboleth.idp.attribute.IdPAttribute;
+import net.shibboleth.idp.attribute.resolver.AttributeResolver;
+import net.shibboleth.idp.attribute.resolver.context.AttributeResolutionContext;
+import net.shibboleth.oidc.metadata.criterion.ClientSecretReferenceCriterion;
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
+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.resolver.CriteriaSet;
+import net.shibboleth.utilities.java.support.resolver.ResolverException;
+import net.shibboleth.utilities.java.support.service.ReloadableService;
+
+/**
+ * A client secret value resolver that fetches the values from the given {@link AttributeResolver} service.
+ * 
+ * This class builds a new {@link AttributeResolutionContext} and sets the client secret reference key value to
+ * {@link AttributeResolutionContext#setPrincipal(String)} and its related entity ID to
+ * {@link AttributeResolutionContext#setAttributeRecipientID(String)}. The resolution context does not have any
+ * parent contexts.
+ */
+public class ResolverServiceClientSecretValueResolver extends AbstractClientSecretValueResolver {
+    
+    /** Class logger. */
+    private final Logger log = LoggerFactory.getLogger(ResolverServiceClientSecretValueResolver.class);
+   
+    /** The attribute resolver service used for the client secret value resolution. */
+    @NonnullAfterInit private ReloadableService<AttributeResolver> service;
+    
+    /** The list of attribute IDs that may contain the resolved client secret values. */
+    @Nonnull private List<String> attributeIds;
+    
+    /**
+     * Constructor.
+     */
+    public ResolverServiceClientSecretValueResolver() {
+        attributeIds = Collections.emptyList();
+    }
+    
+    /**
+     * Set the attribute resolver service used for the client secret value resolution.
+     * 
+     * @param resolver The attribute resolver service used for the client secret value resolution.
+     */
+    public void setAttributeResolver(@Nonnull final ReloadableService<AttributeResolver> resolver) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        service = Constraint.isNotNull(resolver, "Attribute resolver service can not be null");
+    }
+    
+    /**
+     * Get the attribute resolver service used for the client secret value resolution.
+     * 
+     * @return The attribute resolver service used for the client secret value resolution.
+     */
+    public @NonnullAfterInit ReloadableService<AttributeResolver> getAttributeResolver() {
+        return service;
+    }
+    
+    /**
+     * Set the list of attribute IDs that may contain the resolved client secret values.
+     * 
+     * @param ids The list of attribute IDs that may contain the resolved client secret values.
+     */
+    public void setAttributeIds(@Nonnull final List<String> ids) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        attributeIds = (List<String>) Constraint.isNotEmpty(ids, "The list of attribute ids cannot be empty");
+    }
+    
+    /**
+     * Get the list of attribute IDs that may contain the resolved client secret values.
+     * 
+     * @return The list of attribute IDs that may contain the resolved client secret values.
+     */
+    public @Nonnull List<String> getAttributeIds() {
+        return attributeIds;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public @Nonnull Iterable<String> resolve(@Nonnull final CriteriaSet criteria) throws ResolverException {
+        ComponentSupport.ifNotInitializedThrowUninitializedComponentException(this);
+        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+
+        final ClientSecretReferenceCriterion referenceCriterion = criteria.get(ClientSecretReferenceCriterion.class);
+        Constraint.isNotNull(referenceCriterion,
+                "The client secret reference criterion must be included in the criteria.");
+
+        final AttributeResolutionContext resolutionContext = new AttributeResolutionContext();
+        resolutionContext.setPrincipal(referenceCriterion.getSecretReference());
+        if (criteria.contains(EntityIdCriterion.class)) {
+            resolutionContext.setAttributeRecipientID(criteria.get(EntityIdCriterion.class).getEntityId());
+        }
+        resolutionContext.resolveAttributes(service);
+        final Map<String, IdPAttribute> resolvedAttributes = resolutionContext.getResolvedIdPAttributes();
+        final LazySet<String> result = new LazySet<>();
+        for (final String attributeId : attributeIds) {
+            if (resolvedAttributes.containsKey(attributeId)) {
+                log.debug("Found a value for reference '{}' via attribute ID {}",
+                        referenceCriterion.getSecretReference(), attributeId);
+                result.add((String) resolvedAttributes.get(attributeId).getValues().get(0).getNativeValue());
+            }
+        }
+        return result;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public @Nullable String resolveSingle(@Nonnull final CriteriaSet criteria) throws ResolverException {
+        final Iterator<String> iterator = resolve(criteria).iterator();
+        return iterator.hasNext() ? iterator.next() : null;
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    protected void doDestroy() {
+        service = null;
+        attributeIds = null;
+        super.doDestroy();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        
+        if (getAttributeResolver() == null) {
+            throw new ComponentInitializationException("Attribute resolver service can not be null");
+        }
+    }
+}
diff --git a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/StorageServiceClientInformationManager.java b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/StorageServiceClientInformationManager.java
new file mode 100644
index 0000000..0a6028d
--- /dev/null
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/StorageServiceClientInformationManager.java
@@ -0,0 +1,77 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.oidc.metadata.impl;
+
+import java.io.IOException;
+import java.time.Instant;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.storage.StorageService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.nimbusds.oauth2.sdk.id.ClientID;
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformation;
+
+import net.shibboleth.oidc.metadata.ClientInformationManager;
+import net.shibboleth.oidc.metadata.ClientInformationManagerException;
+
+/**
+ * A {@link ClientInformationManager} exploiting {@link StorageService} for storing the data.
+ */
+public class StorageServiceClientInformationManager extends BaseStorageServiceClientInformationComponent 
+    implements ClientInformationManager {
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(StorageServiceClientInformationResolver.class);
+    
+    /** {@inheritDoc} */
+    @Override
+    public void storeClientInformation(final OIDCClientInformation clientInformation,
+            @Nullable final Instant expiration) throws ClientInformationManagerException {
+        log.debug("Attempting to store client information");
+        final String clientId = clientInformation.getID().getValue();
+        //TODO: configurable serialization
+        final String serialized = clientInformation.toJSONObject().toJSONString();
+        try {
+            getStorageService().create(CONTEXT_NAME, clientId, serialized,
+                    expiration != null ? expiration.toEpochMilli() : null);
+        } catch (final IOException e) {
+            log.error("Could not store the client information", e);
+            throw new ClientInformationManagerException("Could not store the client information", e);
+        }
+        log.info("Successfully stored the client information for id {}", clientId);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void destroyClientInformation(final ClientID clientId) {
+        if (clientId == null) {
+            log.warn("The null clientId cannot be destroyed, nothing to do");
+            return;
+        }
+        try {
+            getStorageService().delete(CONTEXT_NAME, clientId.getValue());
+        } catch (final IOException e) {
+            log.error("Could not delete the client ID {}", clientId.getValue(), e);
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/StorageServiceClientInformationResolver.java b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/StorageServiceClientInformationResolver.java
new file mode 100644
index 0000000..a9d4ad9
--- /dev/null
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/StorageServiceClientInformationResolver.java
@@ -0,0 +1,151 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.oidc.metadata.impl;
+
+import java.io.IOException;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.storage.StorageRecord;
+import org.opensaml.storage.StorageService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.nimbusds.oauth2.sdk.ParseException;
+import com.nimbusds.oauth2.sdk.util.JSONObjectUtils;
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformation;
+
+import net.shibboleth.oidc.jwk.RemoteJwkSetCache;
+import net.shibboleth.oidc.metadata.ClientInformationResolver;
+import net.shibboleth.oidc.metadata.criterion.ClientIDCriterion;
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
+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.CriteriaSet;
+import net.shibboleth.utilities.java.support.resolver.ResolverException;
+
+/**
+ * A {@link ClientInformationResolver} exploiting {@link StorageService} for fetching the stored data.
+ */
+public class StorageServiceClientInformationResolver extends BaseStorageServiceClientInformationComponent
+        implements ClientInformationResolver {
+
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(StorageServiceClientInformationResolver.class);
+
+    /** The cache for remote JWK key sets. */
+    @NonnullAfterInit private RemoteJwkSetCache remoteJwkSetCache;
+
+    /** The remote key refresh interval in milliseconds. Default value: 30 minutes. */
+    @Positive private Duration keyFetchInterval = Duration.ofMinutes(30);
+
+    /** {@inheritDoc} */
+    @Override protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        
+        if (remoteJwkSetCache == null) {
+            log.warn("The RemoteJwkSetCache is not defined, the remote keys are not fetched automatically");
+        }
+    }
+
+    /**
+     * Set the cache for remote JWK key sets.
+     * 
+     * @param jwkSetCache What to set.
+     */
+    public void setRemoteJwkSetCache(@Nonnull final RemoteJwkSetCache jwkSetCache) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+        
+        remoteJwkSetCache = Constraint.isNotNull(jwkSetCache, "The remote JWK set cache cannot be null");
+    }
+
+    /**
+     * Set the remote key refresh interval.
+     * 
+     * @param interval What to set.
+     */
+    public void setKeyFetchInterval(@Positive final Duration interval) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+
+        Constraint.isFalse(interval == null || interval.isNegative(), "Remote key refresh must be greater than 0");
+        
+        keyFetchInterval = interval;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    @Nullable public Iterable<OIDCClientInformation> resolve(@Nullable final CriteriaSet criteria)
+            throws ResolverException {
+        ComponentSupport.ifNotInitializedThrowUninitializedComponentException(this);
+        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+
+        final ClientIDCriterion clientIdCriterion = criteria.get(ClientIDCriterion.class);
+        if (clientIdCriterion == null || clientIdCriterion.getClientID() == null) {
+            log.warn("No client ID criteria found, returning empty set.");
+            return Collections.emptyList();
+        }
+        // TODO: support other criterion
+        final String clientId = clientIdCriterion.getClientID().getValue();
+        final List<OIDCClientInformation> result = new ArrayList<>();
+        try {
+            final StorageRecord<?> record = getStorageService().read(CONTEXT_NAME, clientId);
+            if (record == null) {
+                log.debug("Could not find any records with clientId {}", clientId);
+            } else {
+                final OIDCClientInformation clientInformation =
+                        OIDCClientInformation.parse(JSONObjectUtils.parse(record.getValue()));
+                log.debug("Found a record with clientId {}", clientId);
+                if (clientInformation.getOIDCMetadata().getJWKSetURI() != null && remoteJwkSetCache != null) {
+                    clientInformation.getOIDCMetadata().setJWKSet(remoteJwkSetCache
+                            .fetch(clientInformation.getOIDCMetadata().getJWKSetURI(),
+                                    Instant.now().plus(keyFetchInterval)));
+                }
+                result.add(clientInformation);
+            }
+        } catch (final IOException | ParseException e) {
+            log.error("Could not read the storage data", e);
+        }
+        return result;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    @Nullable public OIDCClientInformation resolveSingle(@Nullable final CriteriaSet criteria)
+            throws ResolverException {
+        final Iterable<OIDCClientInformation> iterable = resolve(criteria);
+        if (iterable != null) {
+            final Iterator<OIDCClientInformation> iterator = iterable.iterator();
+            if (iterator != null && iterator.hasNext()) {
+                return iterator.next();
+            }
+        }
+        log.warn("Could not find any clients with the given criteria");
+        return null;
+    }
+}
diff --git a/oidc-common-saml-api/src/main/java/net/shibboleth/oidc/saml/xmlobject/Constants.java b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/package-info.java
similarity index 51%
copy from oidc-common-saml-api/src/main/java/net/shibboleth/oidc/saml/xmlobject/Constants.java
copy to oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/package-info.java
index 316d364..b9f786a 100644
--- a/oidc-common-saml-api/src/main/java/net/shibboleth/oidc/saml/xmlobject/Constants.java
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/package-info.java
@@ -15,29 +15,7 @@
  * limitations under the License.
  */
 
-package net.shibboleth.oidc.saml.xmlobject;
-
-import org.opensaml.saml.common.xml.SAMLConstants;
-
 /**
- * Constants related to SAML 2.0 Metadata extension OAuthRPRoleDescriptorType.
+ * Implementation classes related to resolution of OIDC metadata (or client information).
  */
-public final class Constants {
-
-    /** SAML 2.0 Metadata extension for OAuth/OIDC descriptors prefi. */
-    public static final String SAML20MDOIDCMD_PREFIX = "oidcmd";
-
-    /** SAML 2.0 Metadata extension for OAuth/OIDC descriptors XML namespace. */
-    public static final String SAML20MDOIDCMD_NS = "urn:mace:shibboleth:metadata:oidc:1.0";
-
-    /** SAML 2.0 Metadata extension for OAuth/OIDC descriptors. */
-    public static final String SAML20MDOIDCMD_SCHEMA_LOCATION =
-            SAMLConstants.SCHEMA_DIR + "saml-metadata-ext-oidcmd.xsd";
-    
-    /**
-     * Constructor.
-     */
-    private Constants() {
-        // no op
-    }
-}
+package net.shibboleth.oidc.metadata.impl;
\ No newline at end of file
diff --git a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/keyinfo/ext/impl/provider/ClientSecretProvider.java b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/keyinfo/ext/impl/provider/ClientSecretProvider.java
new file mode 100644
index 0000000..47d750c
--- /dev/null
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/keyinfo/ext/impl/provider/ClientSecretProvider.java
@@ -0,0 +1,86 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.oidc.metadata.keyinfo.ext.impl.provider;
+
+import java.util.Collection;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.core.xml.XMLObject;
+import org.opensaml.security.credential.Credential;
+import org.opensaml.xmlsec.keyinfo.KeyInfoCredentialResolver;
+import org.opensaml.xmlsec.keyinfo.impl.KeyInfoResolutionContext;
+import org.opensaml.xmlsec.keyinfo.impl.provider.AbstractKeyInfoProvider;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.nimbusds.oauth2.sdk.auth.Secret;
+
+import net.shibboleth.oidc.saml.xmlobject.ClientSecret;
+import net.shibboleth.oidc.security.credential.BasicNimbusSecretCredential;
+import net.shibboleth.oidc.security.credential.NimbusSecretCredential;
+import net.shibboleth.utilities.java.support.collection.LazySet;
+import net.shibboleth.utilities.java.support.primitive.StringSupport;
+import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
+
+/**
+ * Implementation of {@link org.opensaml.xmlsec.keyinfo.impl.KeyInfoProvider} which provides basic support for
+ * extracting a {@link NimbusSecretCredential} child of KeyInfo.
+ */
+public class ClientSecretProvider extends AbstractKeyInfoProvider {
+
+    /** Class logger. */
+    private final Logger log = LoggerFactory.getLogger(ClientSecretProvider.class);
+
+    /** {@inheritDoc} */
+    @Override @Nullable
+    public Collection<Credential> process(@Nonnull final KeyInfoCredentialResolver resolver,
+            @Nonnull final XMLObject keyInfoChild, @Nullable final CriteriaSet criteriaSet,
+            @Nonnull final KeyInfoResolutionContext kiContext) throws SecurityException {
+        final ClientSecret clientSecret = getClientSecret(keyInfoChild);
+        if (clientSecret == null || StringSupport.trimOrNull(clientSecret.getValue()) == null) {
+            log.debug("No value found for the ClientSecret element");
+            return null;
+        }
+        final LazySet<Credential> credentials = new LazySet<>();
+        credentials.add(new BasicNimbusSecretCredential(new Secret(clientSecret.getValue())));
+        return credentials;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean handles(@Nonnull final XMLObject keyInfoChild) {
+        return getClientSecret(keyInfoChild) != null;
+    }
+
+    /**
+     * Get the ClientSecret from the passed XML object.
+     * 
+     * @param xmlObject an XML object, presumably a {@link ClientSecret}
+     * @return the ClientSecret which was found, or null if none
+     */
+    @Nullable protected ClientSecret getClientSecret(@Nonnull final XMLObject xmlObject) {
+
+        if (xmlObject instanceof ClientSecret) {
+            return (ClientSecret) xmlObject;
+        } else {
+            return null;
+        }
+    }
+}
diff --git a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/keyinfo/ext/impl/provider/ClientSecretReferenceProvider.java b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/keyinfo/ext/impl/provider/ClientSecretReferenceProvider.java
new file mode 100644
index 0000000..fffcaf0
--- /dev/null
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/keyinfo/ext/impl/provider/ClientSecretReferenceProvider.java
@@ -0,0 +1,119 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.oidc.metadata.keyinfo.ext.impl.provider;
+
+import java.util.Collection;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.core.xml.XMLObject;
+import org.opensaml.security.credential.Credential;
+import org.opensaml.xmlsec.keyinfo.KeyInfoCredentialResolver;
+import org.opensaml.xmlsec.keyinfo.impl.KeyInfoResolutionContext;
+import org.opensaml.xmlsec.keyinfo.impl.provider.AbstractKeyInfoProvider;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.nimbusds.oauth2.sdk.auth.Secret;
+
+import net.shibboleth.oidc.metadata.ClientSecretValueResolver;
+import net.shibboleth.oidc.metadata.criterion.ClientSecretReferenceCriterion;
+import net.shibboleth.oidc.saml.xmlobject.ClientSecretKeyReference;
+import net.shibboleth.oidc.security.credential.BasicNimbusSecretCredential;
+import net.shibboleth.oidc.security.credential.NimbusSecretCredential;
+import net.shibboleth.utilities.java.support.collection.LazySet;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+import net.shibboleth.utilities.java.support.primitive.StringSupport;
+import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
+import net.shibboleth.utilities.java.support.resolver.ResolverException;
+
+/**
+ * Implementation of {@link org.opensaml.xmlsec.keyinfo.impl.KeyInfoProvider} which provides basic support for
+ * extracting a {@link NimbusSecretCredential} child of KeyInfo. The values are resolved using the given
+ * collection of {@link ClientSecretValueResolver}s.
+ */
+public class ClientSecretReferenceProvider  extends AbstractKeyInfoProvider {
+
+    /** Class logger. */
+    private final Logger log = LoggerFactory.getLogger(ClientSecretReferenceProvider.class);
+    
+    /** The list of resolvers for resolving the values for client secrets. */
+    private Collection<ClientSecretValueResolver> clientSecretValueResolvers;
+
+    /**
+     * Constructor.
+     *
+     * @param valueResolvers The list of resolvers for resolving the values for client secrets.
+     */
+    public ClientSecretReferenceProvider(final Collection<ClientSecretValueResolver> valueResolvers) {
+        clientSecretValueResolvers = 
+                Constraint.isNotEmpty(valueResolvers, "The client secret value resolvers cannot be empty");    
+    }
+    
+    /** {@inheritDoc} */
+    @Override @Nullable
+    public Collection<Credential> process(@Nonnull final KeyInfoCredentialResolver resolver,
+            @Nonnull final XMLObject keyInfoChild, @Nullable final CriteriaSet criteriaSet,
+            @Nonnull final KeyInfoResolutionContext kiContext) throws SecurityException {
+        final ClientSecretKeyReference clientSecretReference = getClientSecret(keyInfoChild);
+        if (clientSecretReference == null || StringSupport.trimOrNull(clientSecretReference.getValue()) == null) {
+            log.debug("No value found for the ClientSecretReferenceKey element");
+            return null;
+        }
+        final String secretReference = clientSecretReference.getValue();
+        for (final ClientSecretValueResolver valueResolver : clientSecretValueResolvers) {
+            log.trace("Attempting to resolve value for {} with {}", secretReference, valueResolver.getId());
+            final CriteriaSet criteria =  new CriteriaSet(new ClientSecretReferenceCriterion(secretReference));
+            try {
+                final String value = valueResolver.resolveSingle(criteria);
+                if (value != null) {
+                    log.debug("Found value for {} with {}", secretReference, valueResolver.getId());
+                    final LazySet<Credential> credentials = new LazySet<>();
+                    credentials.add(new BasicNimbusSecretCredential(new Secret(value)));
+                    return credentials;
+                }
+            } catch (final ResolverException e) {
+                log.warn("Client secret value resolution failed", e);
+            }
+        }
+        log.warn("No values could be resolved for the client secret reference {}", clientSecretReference.getValue());
+        return null;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean handles(final @Nonnull XMLObject keyInfoChild) {
+        return getClientSecret(keyInfoChild) != null;
+    }
+
+    /**
+     * Get the ClientSecretReferenceKey from the passed XML object.
+     * 
+     * @param xmlObject an XML object, presumably a {@link ClientSecretKeyReference}
+     * @return the ClientSecret which was found, or null if none
+     */
+    @Nullable protected ClientSecretKeyReference getClientSecret(@Nonnull final XMLObject xmlObject) {
+
+        if (xmlObject instanceof ClientSecretKeyReference) {
+            return (ClientSecretKeyReference) xmlObject;
+        } else {
+            return null;
+        }
+    }
+}
\ No newline at end of file
diff --git a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/keyinfo/ext/impl/provider/InlineJwksProvider.java b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/keyinfo/ext/impl/provider/InlineJwksProvider.java
new file mode 100644
index 0000000..832c769
--- /dev/null
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/keyinfo/ext/impl/provider/InlineJwksProvider.java
@@ -0,0 +1,170 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.oidc.metadata.keyinfo.ext.impl.provider;
+
+import java.text.ParseException;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.core.xml.XMLObject;
+import org.opensaml.security.SecurityException;
+import org.opensaml.security.credential.Credential;
+import org.opensaml.security.credential.CredentialContext;
+import org.opensaml.xmlsec.keyinfo.KeyInfoCredentialResolver;
+import org.opensaml.xmlsec.keyinfo.impl.KeyInfoResolutionContext;
+import org.opensaml.xmlsec.keyinfo.impl.provider.AbstractKeyInfoProvider;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.jwk.AsymmetricJWK;
+import com.nimbusds.jose.jwk.JWK;
+import com.nimbusds.jose.jwk.JWKSet;
+import com.nimbusds.jose.jwk.KeyType;
+
+import net.shibboleth.oidc.saml.xmlobject.JwksData;
+import net.shibboleth.oidc.security.credential.BasicJWKCredential;
+import net.shibboleth.oidc.security.credential.JWKCredential;
+import net.shibboleth.oidc.security.impl.CredentialConversionUtil;
+import net.shibboleth.utilities.java.support.codec.Base64Support;
+import net.shibboleth.utilities.java.support.codec.DecodingException;
+import net.shibboleth.utilities.java.support.collection.LazySet;
+import net.shibboleth.utilities.java.support.primitive.StringSupport;
+import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
+
+/**
+ * Implementation of {@link org.opensaml.xmlsec.keyinfo.impl.KeyInfoProvider} which provides basic support for
+ * extracting a {@link JWKCredential} child of KeyInfo. The value is expected to be either a Base64-encoded value of
+ * either a single JWK credential or a JSON array of JWK credentials.
+ */
+public class InlineJwksProvider extends AbstractKeyInfoProvider {
+
+    /** Class logger. */
+    private final Logger log = LoggerFactory.getLogger(InlineJwksProvider.class);
+
+    /** {@inheritDoc} */
+    @Override @Nullable
+    public Collection<Credential> process(@Nonnull final KeyInfoCredentialResolver resolver,
+            @Nonnull final XMLObject keyInfoChild, @Nullable final CriteriaSet criteriaSet,
+            @Nonnull final KeyInfoResolutionContext kiContext) throws SecurityException {
+        final JwksData jwksData = getJwksData(keyInfoChild);
+        if (jwksData == null || StringSupport.trimOrNull(jwksData.getValue()) == null) {
+            return null;
+        }
+        final String data;
+        try {
+            data = new String(Base64Support.decode(jwksData.getValue()));
+        } catch (final DecodingException e) {
+            log.error("Could not decode the JWKS data", e);
+            return null;
+        }
+        final List<JWK> jwks = parseJWKs(data);
+        if (jwks == null) {
+            return null;
+        }
+        final LazySet<Credential> credentials = new LazySet<>();
+        for (final JWK jwk : jwks) {
+            credentials.add(buildCredential(jwk, kiContext));
+        }
+        return credentials;
+    }
+    
+    @Nonnull protected BasicJWKCredential buildCredential(@Nonnull final JWK jwk,
+            @Nonnull final KeyInfoResolutionContext kiContext) {
+        final BasicJWKCredential credential = new BasicJWKCredential();
+        if (jwk.getKeyType() == KeyType.EC || jwk.getKeyType() == KeyType.RSA) {
+            try {
+                credential.setPublicKey(((AsymmetricJWK) jwk).toPublicKey());
+            } catch (final JOSEException e) {
+                log.warn("Could not parse public key from JWK", e);
+            }
+        } else {
+            log.warn("Unsupported key type {} found from JWK", jwk.getKeyType());
+        }
+        if (jwk.getKeyID() != null) {
+            credential.getKeyNames().add(jwk.getKeyID());
+            credential.setKid(jwk.getKeyID());
+        } else {
+            credential.getKeyNames().addAll(kiContext.getKeyNames());
+        }
+
+        if (jwk.getKeyUse() != null) {
+            credential.setUsageType(CredentialConversionUtil.getUsageType(jwk));
+        } else {
+            final CredentialContext credContext = buildCredentialContext(kiContext);
+            if (credContext != null) {
+                credential.getCredentialContextSet().add(credContext);
+            }
+        }
+        return credential;
+    }
+    
+    @Nullable protected List<JWK> parseJWKs(@Nullable final String input) {
+        final JWKSet jwkSet = parseJWKSet(input);
+        if (jwkSet != null) {
+            return jwkSet.getKeys();
+        }
+        final JWK jwk = parseJWK(input);
+        if (jwk != null) {
+            return Arrays.asList(jwk);
+        }
+        return null;
+    }
+    
+    @Nullable protected JWKSet parseJWKSet(@Nullable final String input) {
+        try {
+            return JWKSet.parse(input);
+        } catch (final ParseException e) {
+            log.debug("Could not parse JWKSet from the given input", e);
+        }
+        return null;
+    }
+    
+    @Nullable protected JWK parseJWK(@Nullable final String input) {
+        try {
+            return JWK.parse(input);
+        } catch (final ParseException e) {
+            log.debug("Could not parse JWK from the given input", e);
+        }
+        return null;
+    }
+
+    @Override
+    public boolean handles(@Nonnull final XMLObject keyInfoChild) {
+        return getJwksData(keyInfoChild) != null;
+    }
+
+    /**
+     * Get the JwksData from the passed XML object.
+     * 
+     * @param xmlObject an XML object, presumably a {@link JwksData}
+     * @return the JwksData which was found, or null if none
+     */
+    @Nullable protected JwksData getJwksData(@Nonnull final XMLObject xmlObject) {
+
+        if (xmlObject instanceof JwksData) {
+            return (JwksData) xmlObject;
+        } else {
+            return null;
+        }
+    }
+}
diff --git a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/keyinfo/ext/impl/provider/JWKSReferenceProvider.java b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/keyinfo/ext/impl/provider/JWKSReferenceProvider.java
new file mode 100644
index 0000000..4fb9cf1
--- /dev/null
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/keyinfo/ext/impl/provider/JWKSReferenceProvider.java
@@ -0,0 +1,94 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.oidc.metadata.keyinfo.ext.impl.provider;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Collection;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.core.xml.XMLObject;
+import org.opensaml.security.SecurityException;
+import org.opensaml.security.credential.Credential;
+import org.opensaml.xmlsec.keyinfo.KeyInfoCredentialResolver;
+import org.opensaml.xmlsec.keyinfo.impl.KeyInfoResolutionContext;
+import org.opensaml.xmlsec.keyinfo.impl.provider.AbstractKeyInfoProvider;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import net.shibboleth.oidc.saml.xmlobject.JwksUri;
+import net.shibboleth.oidc.security.credential.BasicJWKReferenceCredential;
+import net.shibboleth.oidc.security.credential.JWKReferenceCredential;
+import net.shibboleth.utilities.java.support.collection.LazySet;
+import net.shibboleth.utilities.java.support.primitive.StringSupport;
+import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
+
+/**
+ * Implementation of {@link org.opensaml.xmlsec.keyinfo.impl.KeyInfoProvider} which provides basic support for
+ * extracting a {@link JWKReferenceCredential} child of KeyInfo.
+ */
+public class JWKSReferenceProvider extends AbstractKeyInfoProvider {
+
+    /** Class logger. */
+    private final Logger log = LoggerFactory.getLogger(JWKSReferenceProvider.class);
+
+    /** {@inheritDoc} */
+    @Override
+    public Collection<Credential> process(@Nonnull final KeyInfoCredentialResolver resolver,
+            @Nonnull final XMLObject keyInfoChild, @Nullable final CriteriaSet criteriaSet,
+            @Nonnull final KeyInfoResolutionContext kiContext) throws SecurityException {
+        final JwksUri jwksUri = getJwksUri(keyInfoChild);
+        if (jwksUri == null || StringSupport.trimOrNull(jwksUri.getValue()) == null) {
+            return null;
+        }
+        final URI uri;
+        try {
+            uri = new URI(jwksUri.getValue());
+        } catch (final URISyntaxException e) {
+            log.warn("Could not build URI from the given value {}", jwksUri.getValue(), e);
+            return null;
+        }
+        final LazySet<Credential> credentials = new LazySet<>();
+        credentials.add(new BasicJWKReferenceCredential(uri));
+        return credentials;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean handles(@Nonnull final XMLObject keyInfoChild) {
+        return getJwksUri(keyInfoChild) != null;
+    }
+
+    /**
+     * Get the JwksUri from the passed XML object.
+     * 
+     * @param xmlObject an XML object, presumably a {@link JwksUri}
+     * @return the JwksUri which was found, or null if none
+     */
+    @Nullable protected JwksUri getJwksUri(@Nonnull final XMLObject xmlObject) {
+
+        if (xmlObject instanceof JwksUri) {
+            return (JwksUri) xmlObject;
+        } else {
+            return null;
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/oidc-common-saml-api/src/main/java/net/shibboleth/oidc/saml/xmlobject/Constants.java b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/keyinfo/ext/impl/provider/package-info.java
similarity index 51%
copy from oidc-common-saml-api/src/main/java/net/shibboleth/oidc/saml/xmlobject/Constants.java
copy to oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/keyinfo/ext/impl/provider/package-info.java
index 316d364..f78af07 100644
--- a/oidc-common-saml-api/src/main/java/net/shibboleth/oidc/saml/xmlobject/Constants.java
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/keyinfo/ext/impl/provider/package-info.java
@@ -15,29 +15,7 @@
  * limitations under the License.
  */
 
-package net.shibboleth.oidc.saml.xmlobject;
-
-import org.opensaml.saml.common.xml.SAMLConstants;
-
 /**
- * Constants related to SAML 2.0 Metadata extension OAuthRPRoleDescriptorType.
+ * Implementation classes related to KeyInfo extensions.
  */
-public final class Constants {
-
-    /** SAML 2.0 Metadata extension for OAuth/OIDC descriptors prefi. */
-    public static final String SAML20MDOIDCMD_PREFIX = "oidcmd";
-
-    /** SAML 2.0 Metadata extension for OAuth/OIDC descriptors XML namespace. */
-    public static final String SAML20MDOIDCMD_NS = "urn:mace:shibboleth:metadata:oidc:1.0";
-
-    /** SAML 2.0 Metadata extension for OAuth/OIDC descriptors. */
-    public static final String SAML20MDOIDCMD_SCHEMA_LOCATION =
-            SAMLConstants.SCHEMA_DIR + "saml-metadata-ext-oidcmd.xsd";
-    
-    /**
-     * Constructor.
-     */
-    private Constants() {
-        // no op
-    }
-}
+package net.shibboleth.oidc.metadata.keyinfo.ext.impl.provider;
\ No newline at end of file
diff --git a/oidc-common-metadata-impl/src/test/java/net/shibboleth/oidc/metadata/impl/BaseClientSecretValueResolverTest.java b/oidc-common-metadata-impl/src/test/java/net/shibboleth/oidc/metadata/impl/BaseClientSecretValueResolverTest.java
new file mode 100644
index 0000000..7c265ff
--- /dev/null
+++ b/oidc-common-metadata-impl/src/test/java/net/shibboleth/oidc/metadata/impl/BaseClientSecretValueResolverTest.java
@@ -0,0 +1,75 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.oidc.metadata.impl;
+
+import org.testng.annotations.Test;
+
+import net.shibboleth.oidc.metadata.criterion.ClientSecretReferenceCriterion;
+import net.shibboleth.oidc.metadata.impl.AbstractClientSecretValueResolver;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+import net.shibboleth.utilities.java.support.component.DestroyedComponentException;
+import net.shibboleth.utilities.java.support.component.UninitializedComponentException;
+import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
+import net.shibboleth.utilities.java.support.resolver.ResolverException;
+
+/**
+ * Base unit tests for all implementations of {@link ClientSecretValueResolver}.
+ *
+ * @param <T> The implementation to be tested.
+ */
+public abstract class BaseClientSecretValueResolverTest<T extends AbstractClientSecretValueResolver> {
+    
+    T resolver;
+    
+    @Test(expectedExceptions = UninitializedComponentException.class)
+    public void resolveSingle_shouldThrowIfCalledBeforeInit() throws ResolverException,
+        ComponentInitializationException {
+        resolver = buildResolver(false);
+        resolver.resolveSingle(buildCriteriaSet("ignored"));
+    }
+
+    @Test(expectedExceptions = DestroyedComponentException.class)
+    public void resolveSingle_shouldThrowIfCalledAfterDestroy() throws ResolverException,
+        ComponentInitializationException {
+        resolver = buildResolver(true);
+        resolver.destroy();
+        resolver.resolveSingle(buildCriteriaSet("ignored"));
+    }
+    
+    @Test(expectedExceptions = UninitializedComponentException.class)
+    public void resolve_shouldThrowIfCalledBeforeInit() throws ResolverException,
+        ComponentInitializationException {
+        resolver = buildResolver(false);
+        resolver.resolve(buildCriteriaSet("ignored"));
+    }
+    
+    @Test(expectedExceptions = DestroyedComponentException.class)
+    public void resolve_shouldThrowIfCalledAfterDestroy() throws ResolverException,
+        ComponentInitializationException {
+        resolver = buildResolver(true);
+        resolver.destroy();
+        resolver.resolve(buildCriteriaSet("ignored"));
+    }
+
+    protected CriteriaSet buildCriteriaSet(final String secretReference) {
+        final ClientSecretReferenceCriterion criterion = new ClientSecretReferenceCriterion(secretReference);
+        return new CriteriaSet(criterion);
+    }
+
+    protected abstract T buildResolver(boolean init) throws ComponentInitializationException;
+}
diff --git a/oidc-common-metadata-impl/src/test/java/net/shibboleth/oidc/metadata/impl/ClientInformationNodeProcessorTest.java b/oidc-common-metadata-impl/src/test/java/net/shibboleth/oidc/metadata/impl/ClientInformationNodeProcessorTest.java
new file mode 100644
index 0000000..35c6c5b
--- /dev/null
+++ b/oidc-common-metadata-impl/src/test/java/net/shibboleth/oidc/metadata/impl/ClientInformationNodeProcessorTest.java
@@ -0,0 +1,214 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.oidc.metadata.impl;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+
+import org.opensaml.core.criterion.EntityIdCriterion;
+import org.opensaml.core.testing.XMLObjectBaseTestCase;
+import org.opensaml.saml.common.xml.SAMLSchemaBuilder;
+import org.opensaml.saml.criterion.EntityRoleCriterion;
+import org.opensaml.saml.criterion.ProtocolCriterion;
+import org.opensaml.saml.metadata.resolver.filter.MetadataFilterChain;
+import org.opensaml.saml.metadata.resolver.filter.MetadataNodeProcessor;
+import org.opensaml.saml.metadata.resolver.filter.impl.NodeProcessingMetadataFilter;
+import org.opensaml.saml.metadata.resolver.filter.impl.SchemaValidationFilter;
+import org.opensaml.saml.metadata.resolver.impl.FilesystemMetadataResolver;
+import org.opensaml.saml.metadata.resolver.impl.PredicateRoleDescriptorResolver;
+import org.opensaml.saml.saml2.metadata.RoleDescriptor;
+import org.opensaml.saml.saml2.metadata.SPSSODescriptor;
+import org.opensaml.xmlsec.keyinfo.impl.KeyInfoProvider;
+import org.opensaml.xmlsec.keyinfo.impl.provider.DSAKeyValueProvider;
+import org.opensaml.xmlsec.keyinfo.impl.provider.InlineX509DataProvider;
+import org.opensaml.xmlsec.keyinfo.impl.provider.RSAKeyValueProvider;
+import org.springframework.core.io.ClassPathResource;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.nimbusds.jose.EncryptionMethod;
+import com.nimbusds.jose.JWEAlgorithm;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.jwk.JWKSet;
+import com.nimbusds.oauth2.sdk.GrantType;
+import com.nimbusds.oauth2.sdk.ParseException;
+import com.nimbusds.oauth2.sdk.ResponseType;
+import com.nimbusds.oauth2.sdk.Scope;
+import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod;
+import com.nimbusds.openid.connect.sdk.SubjectType;
+import com.nimbusds.openid.connect.sdk.claims.ACR;
+import com.nimbusds.openid.connect.sdk.rp.ApplicationType;
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformation;
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientMetadata;
+
+import net.shibboleth.ext.spring.resource.ResourceHelper;
+import net.shibboleth.oidc.metadata.impl.ClientInformationNodeProcessor;
+import net.shibboleth.oidc.metadata.keyinfo.ext.impl.provider.ClientSecretProvider;
+import net.shibboleth.oidc.metadata.keyinfo.ext.impl.provider.InlineJwksProvider;
+import net.shibboleth.oidc.metadata.keyinfo.ext.impl.provider.JWKSReferenceProvider;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
+import net.shibboleth.utilities.java.support.resolver.ResolverException;
+import net.shibboleth.utilities.java.support.resource.Resource;
+import net.shibboleth.utilities.java.support.xml.SchemaBuilder;
+
+/**
+ * Unit tests for {@link ClientInformationNodeProcessor}.
+ */
+public class ClientInformationNodeProcessorTest extends XMLObjectBaseTestCase {
+    
+    protected RoleDescriptor parseRoleDescriptor(final String mdFileName)
+            throws ResolverException, ComponentInitializationException, URISyntaxException, IOException {
+        final URL mdURL = ClientInformationNodeProcessorTest.class.getResource(mdFileName);
+        final File mdFile = new File(mdURL.toURI());
+        
+        final FilesystemMetadataResolver mdProvider = new FilesystemMetadataResolver(mdFile);
+        mdProvider.setParserPool(parserPool);
+        mdProvider.setId("test");
+        final MetadataFilterChain filterChain = new MetadataFilterChain();
+        final NodeProcessingMetadataFilter filter = new NodeProcessingMetadataFilter();
+        final List<KeyInfoProvider> providers = new ArrayList<>();
+        providers.add(new DSAKeyValueProvider());
+        providers.add(new RSAKeyValueProvider());
+        providers.add(new InlineX509DataProvider());
+        providers.add(new InlineJwksProvider());
+        providers.add(new JWKSReferenceProvider());
+        providers.add(new ClientSecretProvider());
+
+        final List<MetadataNodeProcessor> processors = new ArrayList<>();
+        processors.add(new ClientInformationNodeProcessor(providers));
+        filter.setNodeProcessors(processors);
+        filter.initialize();
+        filterChain.getFilters().add(buildSchemaValidationFilter());
+        filterChain.getFilters().add(filter);
+        mdProvider.setMetadataFilter(filterChain);
+        mdProvider.initialize();
+        
+        final PredicateRoleDescriptorResolver roleResolver = new PredicateRoleDescriptorResolver(mdProvider);
+        roleResolver.initialize();
+        return roleResolver.resolveSingle(new CriteriaSet(
+                new EntityIdCriterion("mockSamlClientId"),
+                new EntityRoleCriterion(SPSSODescriptor.DEFAULT_ELEMENT_NAME),
+                new ProtocolCriterion("http://openid.net/specs/openid-connect-core-1_0.html")));
+        
+    }
+    
+    protected SchemaValidationFilter buildSchemaValidationFilter() {
+        final SAMLSchemaBuilder samlSchemaBuilder = new SAMLSchemaBuilder(SAMLSchemaBuilder.SAML1Version.SAML_11);
+        final SchemaBuilder schemaBuilder = new SchemaBuilder();
+        schemaBuilder.setResourceResolver(new net.shibboleth.utilities.java.support.xml.ClasspathResolver());
+        final Collection<Resource> schemaResources = new ArrayList<>();
+        schemaResources.add(ResourceHelper.of(
+                new ClassPathResource(org.opensaml.saml.common.xml.SAMLConstants.XMLSIG_SCHEMA_LOCATION)));
+        schemaResources.add(ResourceHelper.of(
+                new ClassPathResource(net.shibboleth.idp.saml.xmlobject.ExtensionsConstants.
+                        SHIB_MDEXT10_SCHEMA_LOCATION)));
+        schemaResources.add(ResourceHelper.of(new ClassPathResource("/schema/saml-metadata-ext-oidcmd.xsd")));
+        schemaBuilder.setSchemaResources(schemaResources);
+        samlSchemaBuilder.setSchemaBuilder(schemaBuilder);
+        return new SchemaValidationFilter(samlSchemaBuilder);        
+    }
+    
+    @Test
+    public void testParsedXMLMetadata() throws ResolverException, ComponentInitializationException,
+            URISyntaxException, ParseException, IOException {
+        final RoleDescriptor role = 
+                parseRoleDescriptor("/net/shibboleth/oidc/metadata/impl/EntitiesDescriptor-with-oidcmd.xml");
+        Assert.assertNotNull(role);
+        Assert.assertTrue(role instanceof SPSSODescriptor);
+        final SPSSODescriptor sp = (SPSSODescriptor) role;
+        final List<OIDCClientInformation> clientInformations = sp.getObjectMetadata().get(OIDCClientInformation.class);
+        Assert.assertNotNull(clientInformations);
+        Assert.assertEquals(clientInformations.size(), 1);
+        final OIDCClientInformation clientInformation = clientInformations.get(0);
+        Assert.assertEquals(clientInformation.getID().getValue(), "mockSamlClientId");
+        final OIDCClientMetadata metadata = clientInformation.getOIDCMetadata();
+        
+        Assert.assertEquals(metadata.getApplicationType(), ApplicationType.WEB);
+        Assert.assertEquals(metadata.getURI(), new URI("https://example.org/clientUri"));
+        
+        final List<ACR> defaultAcrs = metadata.getDefaultACRs();
+        Assert.assertEquals(defaultAcrs.size(), 2);
+        Assert.assertTrue(defaultAcrs.contains(new ACR("password")));
+        Assert.assertTrue(defaultAcrs.contains(new ACR("mfa")));
+        
+        final Set<GrantType> grantTypes = metadata.getGrantTypes();
+        Assert.assertEquals(grantTypes.size(), 1);
+        Assert.assertEquals(grantTypes.iterator().next(), GrantType.AUTHORIZATION_CODE);
+        
+        Assert.assertEquals(metadata.getIDTokenJWEAlg(), JWEAlgorithm.A256KW);
+        Assert.assertEquals(metadata.getIDTokenJWEEnc(), EncryptionMethod.A256GCM);
+        Assert.assertEquals(metadata.getIDTokenJWSAlg(), JWSAlgorithm.RS512);
+        
+        Assert.assertEquals(metadata.getInitiateLoginURI(), new URI("https://example.org/initiateLogin"));
+        
+        final Set<URI> postLogoutUris = metadata.getPostLogoutRedirectionURIs();
+        Assert.assertEquals(postLogoutUris.size(), 1);
+        Assert.assertEquals(postLogoutUris.iterator().next(), new URI("https://example.org/postLogout"));
+        
+        Assert.assertEquals(metadata.getRequestObjectJWEAlg(), JWEAlgorithm.A128KW);
+        Assert.assertEquals(metadata.getRequestObjectJWEEnc(), EncryptionMethod.A128GCM);
+        Assert.assertEquals(metadata.getRequestObjectJWSAlg(), JWSAlgorithm.RS256);
+        
+        final Set<URI> requestUris = metadata.getRequestObjectURIs();
+        Assert.assertEquals(requestUris.size(), 1);
+        Assert.assertEquals(requestUris.iterator().next(), new URI("https://example.org/request"));
+        
+        final Set<ResponseType> responseTypes = metadata.getResponseTypes();
+        Assert.assertEquals(responseTypes.size(), 2);
+        Assert.assertTrue(responseTypes.contains(ResponseType.parse("code")));
+        Assert.assertTrue(responseTypes.contains(ResponseType.parse("id_token")));
+        
+        final Scope scope = metadata.getScope();
+        Assert.assertEquals(scope.size(), 2);
+        Assert.assertTrue(scope.contains("openid"));
+        Assert.assertTrue(scope.contains("profile"));
+        
+        Assert.assertEquals(metadata.getSectorIDURI(), new URI("https://example.org/sectorIdentifier"));
+        Assert.assertEquals(metadata.getSoftwareID().getValue(), "mockSoftwareId");
+        Assert.assertEquals(metadata.getSoftwareVersion().getValue(), "mockSoftwareVersion");
+        Assert.assertEquals(metadata.getTokenEndpointAuthMethod(), ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
+        Assert.assertEquals(metadata.getTokenEndpointAuthJWSAlg(), JWSAlgorithm.RS512);
+        
+        Assert.assertEquals(metadata.getUserInfoJWEAlg(), JWEAlgorithm.A192KW);
+        Assert.assertEquals(metadata.getUserInfoJWEEnc(), EncryptionMethod.A192GCM);
+        Assert.assertEquals(metadata.getUserInfoJWSAlg(), JWSAlgorithm.RS384);
+        
+        final JWKSet jwkSet = metadata.getJWKSet();
+        Assert.assertEquals(jwkSet.getKeys().size(), 4);
+        Assert.assertNotNull(jwkSet.getKeyByKeyId("mock"));
+        Assert.assertNotNull(jwkSet.getKeyByKeyId("mockRSA"));
+        Assert.assertNotNull(jwkSet.getKeyByKeyId("mockX509RSA"));
+        Assert.assertNotNull(jwkSet.getKeyByKeyId("mockX509EC"));
+        
+        final Set<URI> redirectUris = metadata.getRedirectionURIs();
+        Assert.assertEquals(redirectUris.size(), 2);
+        Assert.assertTrue(redirectUris.contains(new URI("https://example.org/cb")));
+        Assert.assertTrue(redirectUris.contains(new URI("https://example.org/cb2")));
+        
+        Assert.assertEquals(metadata.getSubjectType(), SubjectType.PAIRWISE);
+    }
+    
+}
diff --git a/oidc-common-metadata-impl/src/test/java/net/shibboleth/oidc/metadata/impl/FilesystemClientInformationResolverTest.java b/oidc-common-metadata-impl/src/test/java/net/shibboleth/oidc/metadata/impl/FilesystemClientInformationResolverTest.java
new file mode 100644
index 0000000..f8c99fd
--- /dev/null
+++ b/oidc-common-metadata-impl/src/test/java/net/shibboleth/oidc/metadata/impl/FilesystemClientInformationResolverTest.java
@@ -0,0 +1,130 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.oidc.metadata.impl;
+
+import java.net.URI;
+import java.util.Set;
+
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.core.io.Resource;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.nimbusds.oauth2.sdk.ResponseType;
+import com.nimbusds.oauth2.sdk.Scope;
+import com.nimbusds.oauth2.sdk.client.ClientInformation;
+import com.nimbusds.oauth2.sdk.id.ClientID;
+import com.nimbusds.openid.connect.sdk.OIDCResponseTypeValue;
+import com.nimbusds.openid.connect.sdk.OIDCScopeValue;
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformation;
+
+import net.shibboleth.oidc.metadata.criterion.ClientIDCriterion;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
+
+/**
+ * Unit tests for {@link FilesystemClientInformationResolver}.
+ */
+public class FilesystemClientInformationResolverTest {
+
+    FilesystemClientInformationResolver resolver;
+    
+    String clientId;
+    String clientId2;
+    URI redirectUri;
+    URI redirectUri2;
+    
+    public void initTest(final String filename) throws Exception {
+        initTest(filename, true);
+    }
+    
+    public void initTest(final String filename, boolean failFastInitialization) throws Exception {
+        clientId = "demo_rp";
+        clientId2 = "demo_rp2";
+        final Resource file = new ClassPathResource(filename);
+        resolver = new FilesystemClientInformationResolver(file);
+        resolver.setId("mockId");
+        resolver.setFailFastInitialization(failFastInitialization);
+        resolver.initialize();
+        redirectUri = new URI("https://192.168.0.150/static");
+        redirectUri2 = new URI("https://192.168.0.150/static2");
+    }
+    
+    @Test
+    public void testNotFound() throws Exception {
+        initTest("/net/shibboleth/oidc/metadata/impl/oidc-client.json");
+        final ClientIDCriterion criterion = new ClientIDCriterion(new ClientID("not_found"));
+        final ClientInformation clientInfo = resolver.resolveSingle(new CriteriaSet(criterion));
+        Assert.assertNull(clientInfo);
+    }
+
+    @Test(expectedExceptions = ComponentInitializationException.class)
+    public void testInvalidJsonFailFast() throws Exception {
+        initTest("/net/shibboleth/oidc/metadata/impl/client-secret-test.properties");
+    }
+
+    @Test
+    public void testInvalidJsonNoFailFast() throws Exception {
+        initTest("/net/shibboleth/oidc/metadata/impl/client-secret-test.properties", false);
+        final ClientIDCriterion criterion = new ClientIDCriterion(new ClientID(clientId));
+        final ClientInformation clientInfo = resolver.resolveSingle(new CriteriaSet(criterion));
+        Assert.assertNull(clientInfo);
+    }
+
+    @Test
+    public void testSingleSuccess() throws Exception {
+        initTest("/net/shibboleth/oidc/metadata/impl/oidc-client.json");
+        final ClientIDCriterion criterion = new ClientIDCriterion(new ClientID(clientId));
+        final OIDCClientInformation clientInfo = resolver.resolveSingle(new CriteriaSet(criterion));
+        Assert.assertNotNull(clientInfo);
+        Assert.assertEquals(clientInfo.getID().getValue(), clientId);
+        final Set<URI> redirectUris = clientInfo.getOIDCMetadata().getRedirectionURIs();
+        Assert.assertEquals(redirectUris.size(), 1);
+        Assert.assertTrue(redirectUris.contains(redirectUri));
+        testScope(clientInfo.getOIDCMetadata().getScope());
+        final Set<ResponseType> responseTypes = clientInfo.getOIDCMetadata().getResponseTypes();
+        Assert.assertEquals(responseTypes.size(), 2);
+        Assert.assertTrue(responseTypes.contains(new ResponseType(OIDCResponseTypeValue.ID_TOKEN)));
+    }
+
+    @Test
+    public void testArraySuccess() throws Exception {
+        initTest("/net/shibboleth/oidc/metadata/impl/oidc-clients.json");
+        final ClientIDCriterion criterion = new ClientIDCriterion(new ClientID(clientId2));
+        final OIDCClientInformation clientInfo = resolver.resolveSingle(new CriteriaSet(criterion));
+        Assert.assertNotNull(clientInfo);
+        Assert.assertEquals(clientInfo.getID().getValue(), clientId2);
+        final Set<URI> redirectUris = clientInfo.getOIDCMetadata().getRedirectionURIs();
+        Assert.assertEquals(redirectUris.size(), 1);
+        Assert.assertTrue(redirectUris.contains(redirectUri2));
+        testScope(clientInfo.getOIDCMetadata().getScope());
+        final Set<ResponseType> responseTypes = clientInfo.getOIDCMetadata().getResponseTypes();
+        Assert.assertEquals(responseTypes.size(), 2);
+        Assert.assertTrue(responseTypes.contains(new ResponseType(OIDCResponseTypeValue.ID_TOKEN)));
+    }
+
+    protected static void testScope(final Scope scope) {
+        Assert.assertEquals(scope.size(), 6);
+        Assert.assertTrue(scope.contains(OIDCScopeValue.OPENID));
+        Assert.assertTrue(scope.contains(OIDCScopeValue.ADDRESS));
+        Assert.assertTrue(scope.contains(OIDCScopeValue.EMAIL));
+        Assert.assertTrue(scope.contains(OIDCScopeValue.PHONE));
+        Assert.assertTrue(scope.contains(OIDCScopeValue.PROFILE));
+        Assert.assertTrue(scope.contains("info"));
+    }
+}
\ No newline at end of file
diff --git a/oidc-common-metadata-impl/src/test/java/net/shibboleth/oidc/metadata/impl/PropertiesClientSecretValueResolverTest.java b/oidc-common-metadata-impl/src/test/java/net/shibboleth/oidc/metadata/impl/PropertiesClientSecretValueResolverTest.java
new file mode 100644
index 0000000..0507b48
--- /dev/null
+++ b/oidc-common-metadata-impl/src/test/java/net/shibboleth/oidc/metadata/impl/PropertiesClientSecretValueResolverTest.java
@@ -0,0 +1,113 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.oidc.metadata.impl;
+
+import java.util.Iterator;
+
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.core.io.Resource;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+import net.shibboleth.utilities.java.support.component.UnmodifiableComponentException;
+import net.shibboleth.utilities.java.support.logic.ConstraintViolationException;
+import net.shibboleth.utilities.java.support.resolver.ResolverException;
+
+/**
+ * Unit tests for {@link PropertiesClientSecretValueResolver}.
+ */
+public class PropertiesClientSecretValueResolverTest
+    extends BaseClientSecretValueResolverTest<PropertiesClientSecretValueResolver> {
+
+    Resource resource;
+    
+    String clientSecretKeyReference = "mockClientSecretKey";
+    String clientSecretValue = "mockClientSecretmockClientSecretmockClientSecret";
+    
+    @BeforeMethod
+    public void init() {
+        resource = new ClassPathResource("net/shibboleth/oidc/metadata/impl/client-secret-test.properties");
+    }
+    
+    @Test(expectedExceptions = ComponentInitializationException.class)
+    public void initialize_shouldThrowWhenResourceNotSet() throws ComponentInitializationException {
+        resolver = new PropertiesClientSecretValueResolver();
+        resolver.setId("mockId");
+        resolver.initialize();
+    }
+    
+    @Test(expectedExceptions = ConstraintViolationException.class)
+    public void setResource_shouldThrowIfResourceIsNul() {
+        resolver = new PropertiesClientSecretValueResolver();
+        resolver.setResource(null);
+    }
+    
+    @Test(expectedExceptions = UnmodifiableComponentException.class)
+    public void setResource_shouldThrowAfterInit() throws ComponentInitializationException {
+        resolver = buildResolver(true);
+        resolver.setResource(resource);
+    }
+    
+    @Test
+    public void resolveSingle_shouldReturnNullWhenPropertyNotFound() throws ResolverException,
+        ComponentInitializationException {
+        resolver = buildResolver(true);
+        Assert.assertNull(resolver.resolveSingle(buildCriteriaSet("not_found")));
+    }
+
+    @Test
+    public void resolveSingle_shouldReturnPropertyWhenFound() throws ResolverException,
+        ComponentInitializationException {
+        resolver = buildResolver(true);
+        Assert.assertEquals(resolver.resolveSingle(buildCriteriaSet(clientSecretKeyReference)), clientSecretValue);
+    }
+
+    @Test
+    public void resolve_shouldReturnEmptyIteratorWhenPropertyNotFound() throws ResolverException,
+        ComponentInitializationException {
+        resolver = buildResolver(true);
+        final Iterable<String> iterable = resolver.resolve(buildCriteriaSet("not_found"));
+        Assert.assertNotNull(iterable);
+        Assert.assertFalse(iterable.iterator().hasNext());
+    }
+
+    @Test
+    public void resolve_shouldReturnSingleValueIteratorWhenPropertyFound() throws ResolverException,
+        ComponentInitializationException {
+        resolver = buildResolver(true);
+        final Iterable<String> iterable = resolver.resolve(buildCriteriaSet(clientSecretKeyReference));
+        Assert.assertNotNull(iterable);
+        final Iterator<String> iterator = iterable.iterator();
+        Assert.assertTrue(iterator.hasNext());
+        Assert.assertEquals(iterator.next(), clientSecretValue);
+        Assert.assertFalse(iterator.hasNext());
+    }
+
+    protected PropertiesClientSecretValueResolver buildResolver(boolean init) throws ComponentInitializationException {
+        resolver = new PropertiesClientSecretValueResolver();
+        resolver.setId("mockId");
+        resolver.setResource(resource);
+        if (init) {
+            resolver.initialize();
+        }
+        return resolver;
+    }
+
+}
diff --git a/oidc-common-metadata-impl/src/test/java/net/shibboleth/oidc/metadata/impl/ResolverServiceClientSecretValueResolverTest.java b/oidc-common-metadata-impl/src/test/java/net/shibboleth/oidc/metadata/impl/ResolverServiceClientSecretValueResolverTest.java
new file mode 100644
index 0000000..01244ce
--- /dev/null
+++ b/oidc-common-metadata-impl/src/test/java/net/shibboleth/oidc/metadata/impl/ResolverServiceClientSecretValueResolverTest.java
@@ -0,0 +1,284 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.oidc.metadata.impl;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+
+import javax.sql.DataSource;
+
+import org.opensaml.core.criterion.EntityIdCriterion;
+import org.springframework.context.support.ConversionServiceFactoryBean;
+import org.springframework.context.support.GenericApplicationContext;
+import org.testng.Assert;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.AfterTest;
+import org.testng.annotations.BeforeTest;
+import org.testng.annotations.Test;
+
+import com.unboundid.ldap.listener.InMemoryDirectoryServer;
+import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
+import com.unboundid.ldap.listener.InMemoryListenerConfig;
+import com.unboundid.ldap.sdk.LDAPException;
+
+import net.shibboleth.ext.spring.config.IdentifiableBeanPostProcessor;
+import net.shibboleth.ext.spring.config.StringToDurationConverter;
+import net.shibboleth.ext.spring.util.SchemaTypeAwareXMLBeanDefinitionReader;
+import net.shibboleth.idp.attribute.resolver.AttributeResolver;
+import net.shibboleth.idp.testing.DatabaseTestingSupport;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+import net.shibboleth.utilities.java.support.component.UnmodifiableComponentException;
+import net.shibboleth.utilities.java.support.logic.ConstraintViolationException;
+import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
+import net.shibboleth.utilities.java.support.resolver.ResolverException;
+import net.shibboleth.utilities.java.support.service.ReloadableService;
+
+/**
+ * Unit tests for {@link ResolverServiceClientSecretValueResolver}.
+ * 
+ * Based on <pre>idp-attribute-resolver-spring</pre> / 
+ * <pre>net.shibboleth.idp.attribute.resolver.spring.AttributeResolverTest</pre>.
+ */
+public class ResolverServiceClientSecretValueResolverTest
+    extends BaseClientSecretValueResolverTest<ResolverServiceClientSecretValueResolver> {
+    
+    /** LDAP */
+    private InMemoryDirectoryServer directoryServer;
+
+    /** LDAP initialization. */
+    private static final String LDAP_INIT_FILE =
+            "src/test/resources/net/shibboleth/oidc/metadata/impl/ldapDataConnectorTest.ldif";
+
+    /** Database initialization. */
+    private static final String DB_INIT_FILE = "/net/shibboleth/oidc/metadata/impl/RdbmsStore.sql";
+
+    /** Database population. */
+    private static final String DB_DATA_FILE = "/net/shibboleth/oidc/metadata/impl/RdbmsData.sql";
+    
+    /** The resolver service configuration. */
+    private static final String SERVICE_CONF_FILE = "/net/shibboleth/oidc/metadata/impl/service.xml";
+
+    private GenericApplicationContext pendingTeardownContext = null;
+    
+    String entityId = "CLIENT_ID_ONE";
+    String entityId2 = "CLIENT_ID_TWO";
+    
+    String clientSecretKeyReferenceBoth = "keyReferenceOne";
+    String clientSecretKeyReferenceOnlyLdap = "keyReferenceTwo";
+    
+    String clientSecretValueRdbms = "thePlainTextSecretValue1234567890";
+    String clientSecretValueLdap = "thePlainTextSecretValue9876543210";
+
+    
+    @AfterMethod public void tearDownTestContext() {
+        if (null == pendingTeardownContext ) {
+            return;
+        }
+        pendingTeardownContext.close();
+        pendingTeardownContext = null;
+    }
+    
+    protected void setTestContext(final GenericApplicationContext context) {
+        tearDownTestContext();
+        pendingTeardownContext = context;
+    }
+
+    @BeforeTest public void setupDataConnectors() throws LDAPException {
+
+        System.setProperty("org.ldaptive.provider", "org.ldaptive.provider.unboundid.UnboundIDProvider");
+        
+        // LDAP
+        final InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig("dc=shibboleth,dc=net");
+        config.setListenerConfigs(InMemoryListenerConfig.createLDAPConfig("default", 10391));
+        config.addAdditionalBindCredentials("cn=Directory Manager", "password");
+        directoryServer = new InMemoryDirectoryServer(config);
+        directoryServer.importFromLDIF(true, LDAP_INIT_FILE);
+        directoryServer.startListening();
+
+        // RDBMS
+        final DataSource datasource = DatabaseTestingSupport.GetMockDataSource(DB_INIT_FILE, "myTestDB");
+        DatabaseTestingSupport.InitializeDataSourceFromFile(DB_DATA_FILE, datasource);
+
+    }
+
+    /**
+     * Shutdown the in-memory directory server.
+     */
+    @AfterTest public void teardownDataConnectors() {
+        directoryServer.shutDown(true);
+        
+        System.clearProperty("org.ldaptive.provider");
+    }
+    
+    protected ReloadableService<AttributeResolver> getResolver() {
+        final GenericApplicationContext context = new GenericApplicationContext();
+        context.getBeanFactory().addBeanPostProcessor(new IdentifiableBeanPostProcessor());
+        setTestContext(context);
+        context.setDisplayName("ApplicationContext: " + ResolverServiceClientSecretValueResolverTest.class);
+
+        final ConversionServiceFactoryBean service = new ConversionServiceFactoryBean();
+        context.setDisplayName("ApplicationContext: ");
+        service.setConverters(new HashSet<>(Arrays.asList(new StringToDurationConverter())));
+        service.afterPropertiesSet();
+
+        context.getBeanFactory().setConversionService(service.getObject());
+        
+        final SchemaTypeAwareXMLBeanDefinitionReader beanDefinitionReader =
+                new SchemaTypeAwareXMLBeanDefinitionReader(context);
+
+        beanDefinitionReader.loadBeanDefinitions(SERVICE_CONF_FILE);
+        context.refresh();
+
+        return context.getBean(ReloadableService.class);
+    }
+        
+    @Test(expectedExceptions = ComponentInitializationException.class)
+    public void initialize_shouldThrowWhenAttributeResolverNotSet() throws ComponentInitializationException {
+        resolver = new ResolverServiceClientSecretValueResolver();
+        resolver.setId("mockId");
+        resolver.initialize();
+    }
+    
+    @Test(expectedExceptions = ConstraintViolationException.class)
+    public void setAttributeResolver_shouldThrowIfAttributeResolverIsNull() {
+        resolver = new ResolverServiceClientSecretValueResolver();
+        resolver.setAttributeResolver(null);
+    }
+    
+    @Test(expectedExceptions = UnmodifiableComponentException.class)
+    public void setAttributeResolver_shouldThrowAfterInit() throws ComponentInitializationException {
+        resolver = buildResolver(true);
+        resolver.setAttributeResolver(getResolver());
+    }
+
+    @Test(expectedExceptions = ConstraintViolationException.class)
+    public void setAttributeIds_shouldThrowIfAttributeIdsNull() throws ComponentInitializationException {
+        resolver = new ResolverServiceClientSecretValueResolver();
+        resolver.setAttributeIds(null);
+    }
+
+    @Test(expectedExceptions = ConstraintViolationException.class)
+    public void setAttributeIds_shouldThrowIfAttributeIdsEmpty() throws ComponentInitializationException {
+        resolver = new ResolverServiceClientSecretValueResolver();
+        resolver.setAttributeIds(Collections.emptyList());
+    }
+
+    @Test(expectedExceptions = UnmodifiableComponentException.class)
+    public void setAttributeIds_shouldThrowAfterInit() throws ComponentInitializationException {
+        resolver = buildResolver(true);
+        resolver.setAttributeIds(Arrays.asList("myDBClientSecret", "myLDAPClientSecret"));
+    }
+
+    @Test
+    public void resolveSingle_shouldReturnNullWhenValuesNotFound() throws ResolverException,
+        ComponentInitializationException {
+        resolver = buildResolver(true);
+        Assert.assertNull(resolver.resolveSingle(buildCriteriaSet("not_found")));
+    }
+
+    @Test
+    public void resolveSingle_shouldReturnRdbmsValueWhenFound() throws ResolverException,
+        ComponentInitializationException {
+        resolver = buildResolver(true, Arrays.asList("myDBClientSecret"));
+        Assert.assertEquals(resolver.resolveSingle(buildCriteriaSet(clientSecretKeyReferenceBoth, entityId)),
+                clientSecretValueRdbms);
+    }
+
+    @Test
+    public void resolveSingle_shouldReturnLdapValueWhenFound() throws ResolverException,
+        ComponentInitializationException {
+        resolver = buildResolver(true, Arrays.asList("myLDAPClientSecret"));
+        Assert.assertEquals(resolver.resolveSingle(buildCriteriaSet(clientSecretKeyReferenceBoth, entityId)),
+                clientSecretValueLdap);
+    }
+
+    @Test
+    public void resolveSingle_shouldReturnLdapValueWhenOnlyFoundFromLdap() throws ResolverException,
+        ComponentInitializationException {
+        resolver = buildResolver(true, Arrays.asList("myDBClientSecret", "myLDAPClientSecret"));
+        Assert.assertEquals(resolver.resolveSingle(buildCriteriaSet(clientSecretKeyReferenceOnlyLdap, entityId2)),
+                "thePlainTextSecretValue1111111111");
+    }
+
+    @Test
+    public void resolve_shouldReturnEmptyIteratorWhenValuesNotFound() throws ResolverException,
+        ComponentInitializationException {
+        resolver = buildResolver(true);
+        final Iterable<String> iterable = resolver.resolve(buildCriteriaSet("not_found"));
+        Assert.assertNotNull(iterable);
+        Assert.assertFalse(iterable.iterator().hasNext());
+    }
+
+    @Test
+    public void resolve_shouldReturnOneValueIteratorWhenSingleValueFound() throws ResolverException,
+        ComponentInitializationException {
+        resolver = buildResolver(true);
+        final Iterable<String> iterable 
+            = resolver.resolve(buildCriteriaSet(clientSecretKeyReferenceOnlyLdap, entityId2));
+        Assert.assertNotNull(iterable);
+        final Iterator<String> iterator = iterable.iterator();
+        Assert.assertTrue(iterator.hasNext());
+        Assert.assertEquals(iterator.next(), "thePlainTextSecretValue1111111111");
+        Assert.assertFalse(iterator.hasNext());
+    }
+
+    @Test
+    public void resolve_shouldReturnTwoValueIteratorWhenFoundFromBoth() throws ResolverException,
+        ComponentInitializationException {
+        resolver = buildResolver(true);
+        final Iterable<String> iterable = resolver.resolve(buildCriteriaSet(clientSecretKeyReferenceBoth, entityId));
+        Assert.assertNotNull(iterable);
+        final Iterator<String> iterator = iterable.iterator();
+        Assert.assertTrue(iterator.hasNext());
+        final String firstValue = iterator.next();
+        Assert.assertTrue(firstValue.equals(clientSecretValueLdap) || firstValue.equals(clientSecretValueRdbms));
+        Assert.assertTrue(iterator.hasNext());
+        final String secondValue = iterator.next();
+        Assert.assertFalse(firstValue.equals(secondValue));
+        Assert.assertTrue(secondValue.equals(clientSecretValueLdap) || secondValue.equals(clientSecretValueRdbms));        
+        Assert.assertFalse(iterator.hasNext());
+    }
+
+    @Override
+    protected ResolverServiceClientSecretValueResolver buildResolver(boolean init)
+            throws ComponentInitializationException {
+        return buildResolver(init, Arrays.asList("myDBClientSecret", "myLDAPClientSecret"));
+    }
+    
+    protected ResolverServiceClientSecretValueResolver buildResolver(boolean init, List<String> attributeIds)
+            throws ComponentInitializationException {
+        ResolverServiceClientSecretValueResolver resolver = new ResolverServiceClientSecretValueResolver();
+        resolver.setAttributeResolver(getResolver());
+        resolver.setAttributeIds(attributeIds);
+        resolver.setId("resolver");
+        if (init) {
+            resolver.initialize();
+        }
+        return resolver;
+    }
+    
+    protected CriteriaSet buildCriteriaSet(final String secretReference, final String clientId) {
+        final CriteriaSet criteria = super.buildCriteriaSet(secretReference);
+        criteria.add(new EntityIdCriterion(clientId));
+        return criteria;
+    }
+
+}
diff --git a/oidc-common-metadata-impl/src/test/java/net/shibboleth/oidc/metadata/impl/StorageServiceClientInformationManagerTest.java b/oidc-common-metadata-impl/src/test/java/net/shibboleth/oidc/metadata/impl/StorageServiceClientInformationManagerTest.java
new file mode 100644
index 0000000..c121e94
--- /dev/null
+++ b/oidc-common-metadata-impl/src/test/java/net/shibboleth/oidc/metadata/impl/StorageServiceClientInformationManagerTest.java
@@ -0,0 +1,123 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.oidc.metadata.impl;
+
+import java.time.Instant;
+import java.util.Date;
+
+import org.opensaml.storage.impl.MemoryStorageService;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.nimbusds.oauth2.sdk.id.ClientID;
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformation;
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientMetadata;
+
+import net.shibboleth.oidc.metadata.criterion.ClientIDCriterion;
+import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
+
+/**
+ * Unit tests for {@link StorageServiceClientInformationManager}.
+ */
+public class StorageServiceClientInformationManagerTest {
+    
+    StorageServiceClientInformationManager manager;
+    StorageServiceClientInformationResolver resolver;
+    
+    MemoryStorageService storageService;
+    
+    String clientIdValue;
+    
+    @BeforeMethod
+    public void setupTests() throws Exception {
+        storageService = new MemoryStorageService();
+        storageService.setId("mockId");
+        storageService.initialize();
+        
+        manager = new StorageServiceClientInformationManager();
+        manager.setStorageService(storageService);
+        manager.setId("mockId");
+        manager.initialize();
+        
+        resolver = new StorageServiceClientInformationResolver();
+        resolver.setStorageService(storageService);
+        resolver.setId("mockId");
+        resolver.initialize();
+        
+        clientIdValue = "mockClientId";
+    }
+
+    @Test
+    public void testStore() throws Exception {
+        final OIDCClientInformation clientInformation = initializeInformation();
+        manager.storeClientInformation(clientInformation, null);
+        final CriteriaSet criteria = initializeCriteria();
+        final OIDCClientInformation result = resolver.resolveSingle(criteria);
+        Assert.assertNotNull(result);
+        Assert.assertEquals(result.getID().getValue(), clientIdValue);
+    }
+
+    @Test
+    public void testNullDestroy() throws Exception {
+        final OIDCClientInformation clientInformation = initializeInformation();
+        manager.storeClientInformation(clientInformation, null);
+        manager.destroyClientInformation(null);
+        final CriteriaSet criteria = initializeCriteria();
+        final OIDCClientInformation result = resolver.resolveSingle(criteria);
+        Assert.assertNotNull(result);
+        Assert.assertEquals(result.getID().getValue(), clientIdValue);
+    }
+
+    @Test
+    public void testDestroy() throws Exception {
+        final OIDCClientInformation clientInformation = initializeInformation();
+        manager.storeClientInformation(clientInformation, null);
+        manager.destroyClientInformation(new ClientID(clientIdValue));
+        final CriteriaSet criteria = initializeCriteria();
+        final OIDCClientInformation result = resolver.resolveSingle(criteria);
+        Assert.assertNull(result);
+    }
+
+    @Test
+    public void testExpiration() throws Exception {
+        final OIDCClientInformation clientInformation = initializeInformation();
+        manager.storeClientInformation(clientInformation, Instant.now().plusSeconds(2));
+        final CriteriaSet criteria = initializeCriteria();
+        final OIDCClientInformation result = resolver.resolveSingle(criteria);
+        Assert.assertNotNull(result);
+        Assert.assertEquals(result.getID().getValue(), clientIdValue);
+        
+        Thread.sleep(2100);
+        
+        final OIDCClientInformation delayedResult = resolver.resolveSingle(criteria);
+        Assert.assertNull(delayedResult);
+    }
+    
+    protected OIDCClientInformation initializeInformation() {
+        final ClientID clientId = new ClientID(clientIdValue);
+        final OIDCClientMetadata metadata = new OIDCClientMetadata();
+        return new OIDCClientInformation(clientId, new Date(), metadata, null);
+    }
+    
+    protected CriteriaSet initializeCriteria() {
+        final CriteriaSet criteria = new CriteriaSet();
+        criteria.add(new ClientIDCriterion(new ClientID(clientIdValue)));
+        return criteria;
+    }
+}
diff --git a/oidc-common-metadata-impl/src/test/resources/logback-test.xml b/oidc-common-metadata-impl/src/test/resources/logback-test.xml
new file mode 100644
index 0000000..552c933
--- /dev/null
+++ b/oidc-common-metadata-impl/src/test/resources/logback-test.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<configuration>
+    <logger name="net.shibboleth.oidc" level="ALL"/>
+    <logger name="org.springframework" level="DEBUG"/>
+
+    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+            <pattern>%level [%logger:%line] - %msg%n</pattern>
+            <charset>UTF-8</charset>
+        </encoder>
+    </appender>
+
+    <root>
+        <level value="DEBUG" />
+        <appender-ref ref="STDOUT" />
+    </root>
+    
+</configuration>
\ No newline at end of file
diff --git a/oidc-common-metadata-impl/src/test/resources/net/shibboleth/oidc/metadata/impl/EntitiesDescriptor-with-oidcmd.xml b/oidc-common-metadata-impl/src/test/resources/net/shibboleth/oidc/metadata/impl/EntitiesDescriptor-with-oidcmd.xml
new file mode 100644
index 0000000..1e3195d
--- /dev/null
+++ b/oidc-common-metadata-impl/src/test/resources/net/shibboleth/oidc/metadata/impl/EntitiesDescriptor-with-oidcmd.xml
@@ -0,0 +1,143 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<md:EntitiesDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" Name="RP test">
+
+    <md:EntityDescriptor entityID="mockSamlClientId">
+
+        <md:SPSSODescriptor xmlns:oidcmd="urn:mace:shibboleth:metadata:oidc:1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" protocolSupportEnumeration="http://openid.net/specs/openid-connect-core-1_0.html">
+            <md:Extensions>
+                <oidcmd:OAuthRPExtensions>
+                    <oidcmd:ApplicationType>web</oidcmd:ApplicationType>
+                    <oidcmd:ClientUri>https://example.org/clientUri</oidcmd:ClientUri>
+                    <oidcmd:DefaultAcrValue>password</oidcmd:DefaultAcrValue>
+                    <oidcmd:DefaultAcrValue>mfa</oidcmd:DefaultAcrValue>
+                    <oidcmd:GrantType>authorization_code</oidcmd:GrantType>
+                    <oidcmd:IdTokenEncryptedResponseAlg>A256KW</oidcmd:IdTokenEncryptedResponseAlg>
+                    <oidcmd:IdTokenEncryptedResponseEnc>A256GCM</oidcmd:IdTokenEncryptedResponseEnc>
+                    <oidcmd:IdTokenSignedResponseAlg>RS512</oidcmd:IdTokenSignedResponseAlg>
+                    <oidcmd:InitiateLoginUri>https://example.org/initiateLogin</oidcmd:InitiateLoginUri>
+                    <oidcmd:PostLogoutRedirectUri>https://example.org/postLogout</oidcmd:PostLogoutRedirectUri>
+                    <oidcmd:RequestObjectEncryptionAlg>A128KW</oidcmd:RequestObjectEncryptionAlg>
+                    <oidcmd:RequestObjectEncryptionEnc>A128GCM</oidcmd:RequestObjectEncryptionEnc>
+                    <oidcmd:RequestObjectSigningAlg>RS256</oidcmd:RequestObjectSigningAlg>
+                    <oidcmd:RequestUri>https://example.org/request</oidcmd:RequestUri>
+                    <oidcmd:ResponseType>code</oidcmd:ResponseType>
+                    <oidcmd:ResponseType>id_token</oidcmd:ResponseType>
+                    <oidcmd:Scope>openid</oidcmd:Scope>
+                    <oidcmd:Scope>profile</oidcmd:Scope>
+                    <oidcmd:SoftwareId>mockSoftwareId</oidcmd:SoftwareId>
+                    <oidcmd:SoftwareVersion>mockSoftwareVersion</oidcmd:SoftwareVersion>
+                    <oidcmd:TokenEndpointAuthMethod>client_secret_basic</oidcmd:TokenEndpointAuthMethod>
+                    <oidcmd:TokenEndpointAuthSigningAlg>RS512</oidcmd:TokenEndpointAuthSigningAlg>
+                    <oidcmd:UserInfoEncryptedResponseAlg>A192KW</oidcmd:UserInfoEncryptedResponseAlg>
+                    <oidcmd:SectorIdentifierUri>https://example.org/sectorIdentifier</oidcmd:SectorIdentifierUri>
+                    <oidcmd:UserInfoEncryptedResponseEnc>A192GCM</oidcmd:UserInfoEncryptedResponseEnc>
+                    <oidcmd:UserInfoSignedResponseAlg>RS384</oidcmd:UserInfoSignedResponseAlg>
+                </oidcmd:OAuthRPExtensions>
+            </md:Extensions>
+
+            <md:KeyDescriptor use="signing">
+                <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
+                    <ds:KeyName>mockX509RSA</ds:KeyName>
+                    <ds:X509Data>
+                        <ds:X509Certificate>
+                            MIIEQDCCAqigAwIBAgIVAIarXvdvyS47KJR7U40FlTufyD8vMA0GCSqGSIb3DQEB
+                            CwUAMCAxHjAcBgNVBAMMFWxvY2FsaG9zdC5sb2NhbGRvbWFpbjAeFw0xOTA2MTcx
+                            MTI5MTJaFw0zOTA2MTcxMTI5MTJaMCAxHjAcBgNVBAMMFWxvY2FsaG9zdC5sb2Nh
+                            bGRvbWFpbjCCAaIwDQYJKoZIhvcNAQEBBQADggGPADCCAYoCggGBALXysGFnoBFh
+                            oasd5uMecp9OTBjvztntPUVmHfm4R3AcItEMEZEN/pETcX/wgKdo4qCBq4PrZITa
+                            T8Salgl0XL6qF1Wia3JNA7Hh/OaoQEUsbsHgsjLMKt6MJh8vIaE1o8loL7Ay4WmZ
+                            Cr3wc8ZS6CpMsv+qbxkyfl1h7MTydETnQhg/X83bj+BjJSh7QeFU0d0SWK1dN2/D
+                            nFoGOfuTfVqeDRIwMxKlR5G//8N202sLaG28NljaHhLn3jHXeiGpCQ+Q2X90dkFb
+                            EKb6sQ6SlDUAzm9MwLYjglDyOhXpUqOnvD67nggLb4Gn/4k+g5wtdfr7unOJYcHK
+                            w7JGnI8Gd0lJMd6B3SpkhUOWgKv/D6HIBArhqSEmXuTyy8FewyYuo1XkIw/Lu3bB
+                            9qoBojM1tygoGlKi7R7e719J+DSkhyGbMyQ59leoN97iGGgqjUWS5mew8zSNviyz
+                            4uGqvxmLWU9UTH1YhlARsBF1bMiMnwLz7dF74AaAkC4pN3BYzDMyHQIDAQABo3Ew
+                            bzAdBgNVHQ4EFgQUwKUd9D1Qymu2oBEVTscrAhP+sIUwTgYDVR0RBEcwRYIVbG9j
+                            YWxob3N0LmxvY2FsZG9tYWluhixodHRwczovL2xvY2FsaG9zdC5sb2NhbGRvbWFp
+                            bi9pZHAvc2hpYmJvbGV0aDANBgkqhkiG9w0BAQsFAAOCAYEAEYqh54a+j5OuR1UB
+                            /AT9k2xXVwHiqQXAC/2un8O5BWAOeOq9+0gLJO5yaJp5c9GjPXRJmnDfGP9HFF6R
+                            CjngtRCm1gV/fpj97IRQS5oroaeTWPQ9ZD5+ogs5DNt6UZeJ2GqpfA5mOytNg3cM
+                            OP1B5QnA1apOaG4FHTegJR7WOIXkkAjEJUy6R+5Q6At7DdK/SRrP5onVPFv2HgGF
+                            E9v9iX/uQepDizS5F2oi6LZCl1/b38gxA8BFL7VZu53JQguaA7SrnP+dBOErT/yh
+                            Qcx3e9wE2ms8H1qISIdl3e7gvLi5jEyDWC9Agde6EjjvVVJAF7jR0puQ39mBfoxP
+                            moVdHJQmCt3V7Ew9tYZUpG3rjp4YNXOiM+QhtwhHWT94q9uJKUQ6JvbxgLNDs5KM
+                            3PENx2C60TPFne9nRRIMVDavU4wwY7GdCgeo8PiZ5zxI0ZCkxh38ODePtKQrxJ7i
+                            E0J1BE2LIxa1T7KY0XKpsH0iI2dNfZfNpNp4v/HiDb4svYgq</ds:X509Certificate>
+                    </ds:X509Data>
+                </ds:KeyInfo>
+            </md:KeyDescriptor>
+            <md:KeyDescriptor use="signing">
+                <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
+                    <ds:KeyName>mockX509EC</ds:KeyName>
+                    <ds:X509Data>
+                        <ds:X509Certificate>
+                            MIIBKDCBzgIJAOYlspXlaqguMAoGCCqGSM49BAMCMBwxCzAJBgNVBAYTAkZJMQ0w
+                            CwYDVQQDDAR0ZXN0MB4XDTE5MTEwMTA4Mjg0OVoXDTIwMTAzMTA4Mjg0OVowHDEL
+                            MAkGA1UEBhMCRkkxDTALBgNVBAMMBHRlc3QwWTATBgcqhkjOPQIBBggqhkjOPQMB
+                            BwNCAARCUOlFMtRj3MIbdCzXmoGz4giDwjzPoX4AxMehhlXmPOodQhLDdvDqx3KE
+                            hqadzIIsKHRQPDycscpHWpPbaQ2VMAoGCCqGSM49BAMCA0kAMEYCIQCVykSuUjlX
+                            j4lxI6YqgYVuuhL2rG4hIrXw/pCey7eF2gIhAOSSaS025lQWy09W4NlnO28OkHoI
+                            +Hbap7+DQlhbbr2d</ds:X509Certificate>
+                    </ds:X509Data>
+                </ds:KeyInfo>
+            </md:KeyDescriptor>
+            <md:KeyDescriptor use="signing">
+                <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
+                    <ds:KeyName>mockRSA</ds:KeyName>
+                    <ds:KeyValue>
+                        <ds:RSAKeyValue>
+                            <ds:Modulus>
+                                AMP1p7GwPH64UPvBKD4DK0I6SDY7dtFPzL7L5qAIEJwIBBeDmLfVY/f9mLzDuDb19XzQxc6GEcjj
+                                K8qRe7JAD3CE1IXXD0hKSOJ7H+chWS84iv7UNukbHHBO1oaRgfHh7vbX7HnpYMoqKK75rfiQqD9e
+                                XOa2FLiH1QvnhLGKJcN+OKujetTgAhxE7ski9Gtfhhbt1qCEl7XtaUCLLexyrwWxx+NRxFgMU+nt
+                                IZQ+T8ii+JQSWnRh14PGc+K9o1dp+vjse62hFprVQhhcbAKAkWpbup77NvvuTZ2+AtUhOuNHrH2I
+                                X3jHeSWH7EzTGkPLGS6bFnYJQBqWv0POytfSyMM=</ds:Modulus>
+                            <ds:Exponent>AQAB</ds:Exponent>
+                        </ds:RSAKeyValue>
+                    </ds:KeyValue>
+                </ds:KeyInfo>
+            </md:KeyDescriptor>
+            <md:KeyDescriptor use="signing">
+                <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
+                    <ds:KeyName>mockJwkId</ds:KeyName>
+                    <oidcmd:JwksData>
+                        ewogICJrdHkiOiAiUlNBIiwKICAiZSI6ICJBUUFCIiwKICAia2lkIjogIm1vY2siLAogICJhbGci
+                        OiAiUlMyNTYiLAogICJuIjogInBKcHRScnpyRlhEUnBaWkdpRmc1eW9KeVRPMlphUENSNEcwbjEx
+                        aUVSclBTdlVYX202Qmdvak5qVEZISk1pa19pbGhtVzY0Q3JLdGlMdklRTFF6VWV5RXdDZHdYZVB3
+                        UVpNeEV4VDJPV2thQy1DV0ZJNHR4X2VFWGRkUGtja1NMRERhMEVQd3dzWktQUFhoRTNWNTBfZ3pW
+                        VDJZQVRvRE9fMmoyeGpWcHFzU0dFc0xpYjZqLW52dFpVVV9CMHNHeUppR1ZzMkpUTmhCTVNrT2tR
+                        Zks2NkNCcW1sbzBuUE5NYVIxbWl2dG5JUG1aNnJKVHcwUDVZZ0dFS1hmZjBsa25Ib25ZVmRsVktw
+                        c0Q4VW5hY0JzdFlyeUhsM0NQR2Uyc3RmR2ExZ3N6NEdIVGVfRnlWVk04UlNoQ2dYVVo3MTdoenpf
+                        ekdQaVhDQkw0ZktEek5ZUXpIUSIKfQo=</oidcmd:JwksData>
+                </ds:KeyInfo>
+            </md:KeyDescriptor>
+            <md:KeyDescriptor>
+                <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
+                    <ds:KeyName>mockJwksUri</ds:KeyName>
+                    <oidcmd:JwksUri>https://example.org/jwks</oidcmd:JwksUri>
+                </ds:KeyInfo>
+            </md:KeyDescriptor>
+            <md:KeyDescriptor>
+                <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
+                    <ds:KeyName>mockClientSecret</ds:KeyName>
+                    <oidcmd:ClientSecret>mockClientSecretValue</oidcmd:ClientSecret>
+                </ds:KeyInfo>
+            </md:KeyDescriptor>
+            <md:NameIDFormat>pairwise</md:NameIDFormat>
+            <md:AssertionConsumerService
+                    Binding="https://tools.ietf.org/html/rfc6749#section-3.1.2"
+                    Location="https://example.org/cb"
+                    index="1"/>
+            <md:AssertionConsumerService
+                    Binding="https://tools.ietf.org/html/rfc6749#section-3.1.2"
+                    Location="https://example.org/cb2"
+                    index="2"/>
+            <md:AssertionConsumerService
+                    Binding="http://example.org/not/supported/profile/id"
+                    Location="https://example.org/cb3"
+                    index="3"/>
+        </md:SPSSODescriptor>
+
+    </md:EntityDescriptor>
+
+</md:EntitiesDescriptor>
\ No newline at end of file
diff --git a/oidc-common-metadata-impl/src/test/resources/net/shibboleth/oidc/metadata/impl/RdbmsData.sql b/oidc-common-metadata-impl/src/test/resources/net/shibboleth/oidc/metadata/impl/RdbmsData.sql
new file mode 100644
index 0000000..483921b
--- /dev/null
+++ b/oidc-common-metadata-impl/src/test/resources/net/shibboleth/oidc/metadata/impl/RdbmsData.sql
@@ -0,0 +1,7 @@
+INSERT INTO clientSecrets
+        (entityId, clientSecretKeyReference, clientSecretValue)
+         values (
+         'CLIENT_ID_ONE',
+         'keyReferenceOne',
+         'thePlainTextSecretValue1234567890');
+
diff --git a/oidc-common-metadata-impl/src/test/resources/net/shibboleth/oidc/metadata/impl/RdbmsStore.sql b/oidc-common-metadata-impl/src/test/resources/net/shibboleth/oidc/metadata/impl/RdbmsStore.sql
new file mode 100644
index 0000000..b4980e4
--- /dev/null
+++ b/oidc-common-metadata-impl/src/test/resources/net/shibboleth/oidc/metadata/impl/RdbmsStore.sql
@@ -0,0 +1,5 @@
+CREATE TABLE clientSecrets (
+        entityId VARCHAR(250) NOT NULL,
+        clientSecretKeyReference VARCHAR(250) NOT NULL,
+        clientSecretValue VARCHAR(250) NOT NULL);
+
diff --git a/oidc-common-metadata-impl/src/test/resources/net/shibboleth/oidc/metadata/impl/attribute-resolver-clientsecrets.xml b/oidc-common-metadata-impl/src/test/resources/net/shibboleth/oidc/metadata/impl/attribute-resolver-clientsecrets.xml
new file mode 100644
index 0000000..69e60d1
--- /dev/null
+++ b/oidc-common-metadata-impl/src/test/resources/net/shibboleth/oidc/metadata/impl/attribute-resolver-clientsecrets.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<AttributeResolver xmlns="urn:mace:shibboleth:2.0:resolver" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+                   xsi:schemaLocation="urn:mace:shibboleth:2.0:resolver http://shibboleth.net/schema/idp/shibboleth-attribute-resolver.xsd">
+
+    <!-- ========================================== -->
+    <!--      Attribute Definitions                 -->
+    <!-- ========================================== -->
+
+    <AttributeDefinition xsi:type="Simple" id="myLDAPClientSecret">
+        <InputDataConnector ref="myLDAP" attributeNames="mobile"/>
+    </AttributeDefinition>
+
+    
+    <AttributeDefinition xsi:type="Simple" id="myDBClientSecret">
+        <InputDataConnector ref="myDB" attributeNames="CLIENTSECRETVALUE"/>
+    </AttributeDefinition>
+    
+    <!-- ========================================== -->
+    <!--      Data Connectors                       -->
+    <!-- ========================================== -->
+
+    <!-- Example Relational Database Connector -->
+
+    <DataConnector id="myDB" xsi:type="RelationalDatabase">
+        <SimpleManagedConnection
+	        jdbcDriver="org.hsqldb.jdbc.JDBCDriver"
+	        jdbcURL="jdbc:hsqldb:mem:myTestDB"
+	        jdbcUserName="SA"
+	        jdbcPassword="" />
+        <QueryTemplate>
+            <![CDATA[ 
+                SELECT * FROM clientSecrets WHERE clientSecretKeyReference = '$resolutionContext.principal' AND entityId = '$resolutionContext.attributeRecipientID'
+            ]]>
+        </QueryTemplate>
+    </DataConnector>
+     
+
+    <!-- Example LDAP Connector -->
+    <DataConnector id="myLDAP" xsi:type="LDAPDirectory"
+        ldapURL="ldap://localhost:10391" 
+        baseDN="dc=shibboleth,dc=net"
+        connectTimeout="PT6S"
+        responseTimeout="PT6S"
+        principal="cn=Directory Manager"
+        principalCredential="password">
+        <FilterTemplate>
+            <![CDATA[
+                (uid=$resolutionContext.principal)
+            ]]>
+        </FilterTemplate>
+    </DataConnector>
+
+</AttributeResolver>
diff --git a/oidc-common-metadata-impl/src/test/resources/net/shibboleth/oidc/metadata/impl/client-secret-test.properties b/oidc-common-metadata-impl/src/test/resources/net/shibboleth/oidc/metadata/impl/client-secret-test.properties
new file mode 100644
index 0000000..8f1c15c
--- /dev/null
+++ b/oidc-common-metadata-impl/src/test/resources/net/shibboleth/oidc/metadata/impl/client-secret-test.properties
@@ -0,0 +1 @@
+mockClientSecretKey = mockClientSecretmockClientSecretmockClientSecret
\ No newline at end of file
diff --git a/oidc-common-metadata-impl/src/test/resources/net/shibboleth/oidc/metadata/impl/ldapDataConnectorTest.ldif b/oidc-common-metadata-impl/src/test/resources/net/shibboleth/oidc/metadata/impl/ldapDataConnectorTest.ldif
new file mode 100644
index 0000000..30e88e4
--- /dev/null
+++ b/oidc-common-metadata-impl/src/test/resources/net/shibboleth/oidc/metadata/impl/ldapDataConnectorTest.ldif
@@ -0,0 +1,26 @@
+dn: dc=shibboleth,dc=net
+dc: shibboleth
+objectClass: dcObject
+objectClass: organization
+o: Shibboleth, Inc.
+
+dn: ou=clientsecrets,dc=shibboleth,dc=net
+ou: clientsecrets
+description: Client secrets
+objectclass: organizationalunit
+
+dn: cn=CLIENT_ID_ONE,ou=clientsecrets,dc=shibboleth,dc=net
+objectclass: inetOrgPerson
+cn: CLIENT_ID_ONE
+sn: Ignored
+uid: keyReferenceOne
+mobile: thePlainTextSecretValue9876543210
+description: test principal
+
+dn: cn=CLIENT_ID_TWO,ou=clientsecrets,dc=shibboleth,dc=net
+objectclass: inetOrgPerson
+cn: CLIENT_ID_TWO
+sn: Ignored
+uid: keyReferenceTwo
+mobile: thePlainTextSecretValue1111111111
+description: test principal
diff --git a/oidc-common-metadata-impl/src/test/resources/net/shibboleth/oidc/metadata/impl/oidc-client.json b/oidc-common-metadata-impl/src/test/resources/net/shibboleth/oidc/metadata/impl/oidc-client.json
new file mode 100644
index 0000000..bf13f3a
--- /dev/null
+++ b/oidc-common-metadata-impl/src/test/resources/net/shibboleth/oidc/metadata/impl/oidc-client.json
@@ -0,0 +1 @@
+{"scope":"openid info profile email address phone","redirect_uris":["https://192.168.0.150/static"],"client_id":"demo_rp","response_types":["id_token", "id_token token"]}
\ No newline at end of file
diff --git a/oidc-common-metadata-impl/src/test/resources/net/shibboleth/oidc/metadata/impl/oidc-client2.json b/oidc-common-metadata-impl/src/test/resources/net/shibboleth/oidc/metadata/impl/oidc-client2.json
new file mode 100644
index 0000000..11ac850
--- /dev/null
+++ b/oidc-common-metadata-impl/src/test/resources/net/shibboleth/oidc/metadata/impl/oidc-client2.json
@@ -0,0 +1 @@
+{"scope":"openid info profile email address phone","redirect_uris":["https://192.168.0.150/static"],"client_id":"demo_rp2","response_types":["id_token", "id_token token"]}
\ No newline at end of file
diff --git a/oidc-common-metadata-impl/src/test/resources/net/shibboleth/oidc/metadata/impl/oidc-clients.json b/oidc-common-metadata-impl/src/test/resources/net/shibboleth/oidc/metadata/impl/oidc-clients.json
new file mode 100644
index 0000000..e480fc4
--- /dev/null
+++ b/oidc-common-metadata-impl/src/test/resources/net/shibboleth/oidc/metadata/impl/oidc-clients.json
@@ -0,0 +1 @@
+[ {"scope":"openid info profile email address phone","redirect_uris":["https://192.168.0.150/static"],"client_id":"demo_rp","response_types":["id_token", "id_token token"]}, {"scope":"openid info profile email address phone","redirect_uris":["https://192.168.0.150/static2"],"client_id":"demo_rp2","response_types":["id_token", "id_token token"]} ]
\ No newline at end of file
diff --git a/oidc-common-metadata-impl/src/test/resources/net/shibboleth/oidc/metadata/impl/service.xml b/oidc-common-metadata-impl/src/test/resources/net/shibboleth/oidc/metadata/impl/service.xml
new file mode 100644
index 0000000..c25979a
--- /dev/null
+++ b/oidc-common-metadata-impl/src/test/resources/net/shibboleth/oidc/metadata/impl/service.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+        xmlns:util="http://www.springframework.org/schema/util"
+        xmlns:p="http://www.springframework.org/schema/p"
+        xmlns:c="http://www.springframework.org/schema/c"
+        xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
+                            http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"
+        default-init-method="initialize"
+        default-destroy-method="destroy">
+                            
+    <!-- This BeanPostProcessor auto-sets identifiable beans with the bean name (if not already set). -->
+    <bean id="shibboleth.IdentifiableBeanPostProcessor"
+        class="net.shibboleth.ext.spring.config.IdentifiableBeanPostProcessor" />     
+                            
+    <bean id="shibboleth.VelocityEngine" class="net.shibboleth.ext.spring.velocity.VelocityEngineFactoryBean">
+        <property name="velocityProperties">
+            <props>
+                <prop key="resource.loader">classpath, string</prop>
+                <prop key="classpath.resource.loader.class">
+                    org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader
+                </prop>
+                <prop key="string.resource.loader.class">
+                    org.apache.velocity.runtime.resource.loader.StringResourceLoader
+                </prop>
+            </props>
+        </property>
+    </bean>
+    
+    <bean id="shibboleth.ClientSecretValueResolverService" class="net.shibboleth.ext.spring.service.ReloadableSpringService"
+        depends-on="shibboleth.VelocityEngine"
+        p:beanPostProcessors-ref="shibboleth.IdentifiableBeanPostProcessor"
+        p:failFast="false" p:reloadCheckDelay="0">
+        
+        <constructor-arg name="claz"
+            value="net.shibboleth.idp.attribute.resolver.AttributeResolver" />
+        <constructor-arg name="strategy">
+            <bean
+                class="net.shibboleth.idp.attribute.resolver.spring.impl.AttributeResolverServiceStrategy"
+                p:id="Shibboleth.Resolver" />
+        </constructor-arg>
+        <property name="serviceConfigurations">
+            <util:list>
+                <value>net/shibboleth/oidc/metadata/impl/attribute-resolver-clientsecrets.xml</value>
+            </util:list>
+        </property>
+    </bean>
+</beans>
\ No newline at end of file
diff --git a/oidc-common-saml-api/src/main/java/net/shibboleth/oidc/saml/xmlobject/Constants.java b/oidc-common-saml-api/src/main/java/net/shibboleth/oidc/saml/xmlobject/Constants.java
index 316d364..566fae8 100644
--- a/oidc-common-saml-api/src/main/java/net/shibboleth/oidc/saml/xmlobject/Constants.java
+++ b/oidc-common-saml-api/src/main/java/net/shibboleth/oidc/saml/xmlobject/Constants.java
@@ -17,8 +17,12 @@
 
 package net.shibboleth.oidc.saml.xmlobject;
 
+import javax.annotation.Nonnull;
+
 import org.opensaml.saml.common.xml.SAMLConstants;
 
+import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
+
 /**
  * Constants related to SAML 2.0 Metadata extension OAuthRPRoleDescriptorType.
  */
@@ -33,7 +37,11 @@ public final class Constants {
     /** SAML 2.0 Metadata extension for OAuth/OIDC descriptors. */
     public static final String SAML20MDOIDCMD_SCHEMA_LOCATION =
             SAMLConstants.SCHEMA_DIR + "saml-metadata-ext-oidcmd.xsd";
-    
+
+    /** OIDC base protocol URI. */
+    @Nonnull @NotEmpty public static final String OIDC_PROTOCOL_URI =
+            "http://openid.net/specs/openid-connect-core-1_0.html";
+
     /**
      * Constructor.
      */
diff --git a/pom.xml b/pom.xml
index 2fc7141..9ae5051 100644
--- a/pom.xml
+++ b/pom.xml
@@ -93,6 +93,16 @@
                 <artifactId>oidc-common-crypto-impl</artifactId>
                 <version>${project.version}</version>
             </dependency>
+            <dependency>
+                <groupId>net.shibboleth.oidc</groupId>
+                <artifactId>oidc-common-metadata-api</artifactId>
+                <version>${project.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>net.shibboleth.oidc</groupId>
+                <artifactId>oidc-common-metadata-impl</artifactId>
+                <version>${project.version}</version>
+            </dependency>
             <dependency>
                 <groupId>net.shibboleth.oidc</groupId>
                 <artifactId>oidc-common-saml-api</artifactId>
@@ -147,6 +157,8 @@
     <modules>
         <module>oidc-common-crypto-api</module>
         <module>oidc-common-crypto-impl</module>
+        <module>oidc-common-metadata-api</module>
+        <module>oidc-common-metadata-impl</module>
         <module>oidc-common-saml-api</module>
         <module>oidc-common-saml-impl</module>
     </modules>

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


More information about the commits mailing list