[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