[java-idp-oidc] branch dev/JOIDC-222 updated: JOIDC-222 - Support for OpenID Federation
Henri Mikkonen
henri.mikkonen at iki.fi
Fri Nov 15 07:24:34 UTC 2024
This is an automated email from the git hooks/post-receive script.
hjmikkon pushed a commit to branch dev/JOIDC-222
in repository java-idp-oidc.
View the commit online:
http://git.shibboleth.net/view/?p=java-idp-oidc.git;a=commit;h=ed4aacd0e744afd45633867b5772f9aef54aa6a2
The following commit(s) were added to refs/heads/dev/JOIDC-222 by this push:
new ed4aacd0 JOIDC-222 - Support for OpenID Federation
ed4aacd0 is described below
commit ed4aacd0e744afd45633867b5772f9aef54aa6a2
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Fri Nov 15 09:23:16 2024 +0200
JOIDC-222 - Support for OpenID Federation
https://shibboleth.atlassian.net/browse/JOIDC-222
Imported the initial implementation of the caches for entity statements and trust chains
- shibboleth.oidfed.EntityConfigurationMetadataCache
- Fetches and stores entity configurations (self-signed entity statements)
- shibboleth.oidfed.SubordinateEntityStatementMetadataCache
- Fetches and stores subordinate statements
- shibboleth.oidfed.TrustChainMetadataCache
- Builds and stores trust chains (lists of entity statements)
- shibboleth.oidfed.LocalTrustAnchorsMetadataCache
- Batch metadata cache fetching locally trusted anchor keys
Some remarks / TODOs:
- Currently all the related classes are located in net.shibboleth.idp.plugin.oidc.op.oidfed.metadata
- Final destinations will change: some may be be moved to oidc-common
- JOSEObjectCriterion and JOSEObjectCredentialResolver are a bit abused as they are meant for header-parsing
- Some hard-coded configurations (such as cleanup times)
- Local trust anchor file should be a conditional resource
- Lacks unit testing
---
...ityConfigurationEndpointExtractionStrategy.java | 82 ++++++++
.../DefaultEntityStatementCredentialResolver.java | 123 +++++++++++
...faultEntityStatementExpirationTimeStrategy.java | 48 +++++
.../DefaultEntityStatementFetchingStrategy.java | 155 ++++++++++++++
...StatementSignatureValidationFilterStrategy.java | 121 +++++++++++
...atementSubjectIdentifierExtractionStrategy.java | 40 ++++
...IssuerEntityIDCriteriaToIdentifierStrategy.java | 42 ++++
.../DefaultLocalTrustAnchorCredentialResolver.java | 107 ++++++++++
.../DefaultLocalTrustAnchorsValidator.java | 58 ++++++
...ubjectEntityIDCriteriaToIdentifierStrategy.java | 42 ++++
...inateStatementCriteriaToIdentifierStrategy.java | 93 +++++++++
...rdinateStatementEndpointExtractionStrategy.java | 149 ++++++++++++++
...inateStatementIdentifierExtractionStrategy.java | 43 ++++
...StatementSignatureValidationFilterStrategy.java | 116 +++++++++++
...ustAnchorSignatureValidationFilterStrategy.java | 100 +++++++++
.../DefaultTrustChainExpirationTimeStrategy.java | 60 ++++++
.../DefaultTrustChainFetchingStrategy.java | 228 +++++++++++++++++++++
...stChainSubjectIdentifierExtractionStrategy.java | 45 ++++
.../oidfed/metadata/IssuerEntityIDCriterion.java | 77 +++++++
.../metadata/IssuerEntityStatementCriterion.java | 79 +++++++
.../op/oidfed/metadata/JWKSetDeserializer.java | 57 ++++++
.../oidc/op/oidfed/metadata/LocalKeyContainer.java | 53 +++++
.../oidfed/metadata/SubjectEntityIDCriterion.java | 79 +++++++
.../metadata/SubjectEntityStatementCriterion.java | 79 +++++++
.../SubjectLocallyTrustedKeysCriterion.java | 79 +++++++
.../TokenPayloadAsymmetricKeyTrustEngine.java | 78 +++++++
.../META-INF/net.shibboleth.idp/postconfig.xml | 188 +++++++++++++++++
27 files changed, 2421 insertions(+)
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultEntityConfigurationEndpointExtractionStrategy.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultEntityConfigurationEndpointExtractionStrategy.java
new file mode 100644
index 00000000..0ecb3b6c
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultEntityConfigurationEndpointExtractionStrategy.java
@@ -0,0 +1,82 @@
+/*
+ * Licensed 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.idp.plugin.oidc.op.oidfed.metadata;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.slf4j.Logger;
+
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.ThreadSafeAfterInit;
+import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.resolver.CriteriaSet;
+
+/**
+ * Default strategy for extracting entity configuration endpoint URI from the given {@link CriteriaSet}.
+ */
+ at ThreadSafeAfterInit
+public class DefaultEntityConfigurationEndpointExtractionStrategy extends AbstractIdentifiableInitializableComponent
+ implements Function<CriteriaSet, URI> {
+
+ /** Class logger. */
+ @Nonnull private Logger log = LoggerFactory.getLogger(DefaultEntityConfigurationEndpointExtractionStrategy.class);
+
+ /** Strategy for fetching entity ID from the criteria set. */
+ @NonnullAfterInit private Function<CriteriaSet, String> criteriaToEntityIdStrategy;
+
+ /**
+ * Set the strategy for fetching entity ID from the criteria set.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setCriteriaToEntityIdStrategy(@Nonnull final Function<CriteriaSet, String> strategy) {
+ checkSetterPreconditions();
+ criteriaToEntityIdStrategy = Constraint.isNotNull(strategy, "Criteria to entity ID strategy cannot be null");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+ if (criteriaToEntityIdStrategy == null) {
+ throw new ComponentInitializationException("Criteria to entity ID strategy cannot be null");
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override @Nullable
+ public URI apply(@Nullable final CriteriaSet criteria) {
+ checkComponentActive();
+ final String entityId = criteriaToEntityIdStrategy.apply(criteria);
+ if (entityId == null) {
+ return null;
+ }
+ try {
+ return new URI(entityId + (entityId.endsWith("/") ? "" : "/") + ".well-known/openid-federation");
+ } catch (final URISyntaxException e) {
+ log.error("Could not build URI for entity configuration endpoint", e);
+ }
+ return null;
+ }
+
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultEntityStatementCredentialResolver.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultEntityStatementCredentialResolver.java
new file mode 100644
index 00000000..a85cb99a
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultEntityStatementCredentialResolver.java
@@ -0,0 +1,123 @@
+/*
+ * Licensed 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.idp.plugin.oidc.op.oidfed.metadata;
+
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.security.credential.Credential;
+import org.slf4j.Logger;
+
+import com.nimbusds.jose.JOSEObject;
+import com.nimbusds.jose.jwk.JWK;
+import com.nimbusds.jose.jwk.JWKSet;
+import com.nimbusds.jwt.SignedJWT;
+import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
+
+import net.shibboleth.oidc.security.credential.impl.BasicJOSEObjectCredentialResolver;
+import net.shibboleth.oidc.security.jose.criterion.JOSEObjectCriterion;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.resolver.CriteriaSet;
+import net.shibboleth.shared.resolver.ResolverException;
+
+/**
+ * Resolves credentials from the jwks-claim located in the signed JWT (entity statement) payload. The JWT is fetched
+ * via {@link SubjectEntityStatementCriterion} if it exists in the criteria set. If the JWT is not self-signed (i.e.
+ * it's a subordinate statement), the jwks-claim is fetched from the signer's statement fetched via
+ * {@link IssuerEntityStatementCriterion}.
+ *
+ * If the {@link SubjectEntityStatementCriterion} doesn't exist, the JWT is fetched via {@link JOSEObjectCriterion}.
+ */
+public class DefaultEntityStatementCredentialResolver extends BasicJOSEObjectCredentialResolver {
+
+ /** Logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(DefaultEntityStatementCredentialResolver.class);
+
+ /** {@inheritDoc} */
+ @Override
+ protected Iterable<Credential> resolveFromSource(@Nullable final CriteriaSet criteriaSet) throws ResolverException {
+ if (criteriaSet == null) {
+ throw new ResolverException("No criteria set supplied");
+ }
+
+ final JWKSet jwks = parseJwkSet(criteriaSet);
+ final List<Credential> credentials = new ArrayList<>();
+ for (final JWK jwk : jwks.getKeys()) {
+ if (jwk != null) {
+ final Credential cred = buildJWKCredential(jwk, null);
+ if (cred != null) {
+ credentials.add(cred);
+ }
+ }
+ }
+ return credentials;
+ }
+
+ /**
+ * Parses the JWKSet from the given criteria set.
+ *
+ * @param criteriaSet criteria set containing source JWT for the JWKSet
+ * @return the JWKSet parsed from the JWT payload
+ * @throws ResolverException if the JWKSet could not be parsed or found
+ */
+ @Nonnull protected JWKSet parseJwkSet(@Nonnull final CriteriaSet criteriaSet) throws ResolverException {
+ final SubjectEntityStatementCriterion subjectCriterion = criteriaSet.get(SubjectEntityStatementCriterion.class);
+ final JWKSet jwks;
+ if (subjectCriterion == null) {
+ log.debug("No SubjectEntityStatementCriterion criteria supplied, resolving from JOSEObject");
+ final JOSEObjectCriterion joseObjectCriteria = criteriaSet.get(JOSEObjectCriterion.class);
+ if (joseObjectCriteria == null) {
+ log.debug("No JOSEObject criteria supplied, resolver could not process");
+ throw new ResolverException("Credential criteria set did not contain an instance of "
+ + "SubjectEntityStatementCriterion nor JOSEObjectCriterion");
+ }
+ final JOSEObject joseObject = joseObjectCriteria.getJOSEObject();
+ if (joseObject == null) {
+ throw new ResolverException("JOSEObjectCriterion did not contain an instance of JOSEObject");
+ }
+ try {
+ final SignedJWT jwt = SignedJWT.parse(joseObject.serialize());
+ jwks = JWKSet.parse(jwt.getJWTClaimsSet().getJSONObjectClaim("jwks"));
+ } catch (final ParseException e) {
+ throw new ResolverException("Could not parse JWKSet from JOSEObject", e);
+ }
+ } else {
+ final EntityStatement subjectStatement = subjectCriterion.getValue();
+ if (subjectStatement.getEntityID().getValue().equals(
+ subjectStatement.getClaimsSet().getIssuer().getValue())) {
+ jwks = subjectStatement.getClaimsSet().getJWKSet();;
+ } else {
+ final IssuerEntityStatementCriterion issuerCriterion =
+ criteriaSet.get(IssuerEntityStatementCriterion.class);
+ if (issuerCriterion == null) {
+ log.debug("No mandatory IssuerEntityStatementCriterion supplied, resolver could not process");
+ throw new ResolverException(
+ "Credential criteria set did not contain an instance of IssuerEntityStatementCriterion");
+ }
+ final EntityStatement issuerStatement = issuerCriterion.getValue();
+ jwks = issuerStatement.getClaimsSet().getJWKSet();
+ }
+ }
+
+ if (jwks == null || jwks.isEmpty()) {
+ throw new ResolverException("Could not parse mandatory jwks");
+ }
+ return jwks;
+ }
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultEntityStatementExpirationTimeStrategy.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultEntityStatementExpirationTimeStrategy.java
new file mode 100644
index 00000000..7960cfd5
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultEntityStatementExpirationTimeStrategy.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed 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.idp.plugin.oidc.op.oidfed.metadata;
+
+import java.time.Instant;
+import java.util.function.Function;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.ThreadSafe;
+
+import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
+
+import net.shibboleth.oidc.metadata.cache.ExpirationTimeContext;
+
+/**
+ * Default strategy for fetching expiration time for the entity statement.
+ */
+ at ThreadSafe
+public class DefaultEntityStatementExpirationTimeStrategy
+ implements Function<ExpirationTimeContext<EntityStatement>, Instant> {
+
+ /** {@inheritDoc} */
+ @Nullable public Instant apply(@Nullable final ExpirationTimeContext<EntityStatement> context) {
+ if (context == null) {
+ return null;
+ }
+ final Instant contextExpiration = context.getNow().plus(context.getMaxCacheDuration());
+ final EntityStatement statement = context.getMetadata();
+ if (statement == null) {
+ return contextExpiration;
+ }
+ final Instant metadataExpiration = statement.getClaimsSet().getExpirationTime().toInstant();
+ return metadataExpiration.isBefore(contextExpiration) ? metadataExpiration : contextExpiration;
+ }
+
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultEntityStatementFetchingStrategy.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultEntityStatementFetchingStrategy.java
new file mode 100644
index 00000000..f6ec9424
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultEntityStatementFetchingStrategy.java
@@ -0,0 +1,155 @@
+/*
+ * Licensed 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.idp.plugin.oidc.op.oidfed.metadata;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.apache.hc.client5.http.classic.HttpClient;
+import org.apache.hc.client5.http.classic.methods.HttpGet;
+import org.apache.hc.client5.http.protocol.HttpClientContext;
+import org.apache.hc.core5.http.ClassicHttpRequest;
+import org.apache.hc.core5.http.ClassicHttpResponse;
+import org.apache.hc.core5.http.HttpStatus;
+import org.apache.hc.core5.http.io.entity.EntityUtils;
+import org.opensaml.security.httpclient.HttpClientSecurityParameters;
+import org.opensaml.security.httpclient.HttpClientSecuritySupport;
+import org.slf4j.Logger;
+
+import com.nimbusds.oauth2.sdk.ParseException;
+import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
+
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.ThreadSafeAfterInit;
+import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.resolver.CriteriaSet;
+
+/**
+ * Default entity statement fetching strategy via configurable {@link HttpClient} and strategy for looking up the
+ * endpoint URI.
+ */
+ at ThreadSafeAfterInit
+public class DefaultEntityStatementFetchingStrategy extends AbstractIdentifiableInitializableComponent
+ implements Function<CriteriaSet, EntityStatement> {
+
+ /** Class logger. */
+ @Nonnull private Logger log = LoggerFactory.getLogger(DefaultEntityStatementFetchingStrategy.class);
+
+ /** HTTP client to use. */
+ @NonnullAfterInit protected HttpClient httpClient;
+
+ /** HTTP client security parameters. */
+ @Nullable private HttpClientSecurityParameters httpClientSecurityParameters;
+
+ /** Strategy to lookup URI from the criteria set. */
+ @NonnullAfterInit private Function<CriteriaSet, URI> criteriaToEndpointStrategy;
+
+ /**
+ * Set the {@link HttpClient} to use.
+ *
+ * @param client HTTP client to use
+ */
+ public void setHttpClient(@Nonnull final HttpClient client) {
+ checkSetterPreconditions();
+ httpClient = Constraint.isNotNull(client, "HttpClient cannot be null");
+ }
+
+ /**
+ * Set the optional client security parameters.
+ *
+ * @param params the new client security parameters
+ */
+ public void setHttpClientSecurityParameters(@Nullable final HttpClientSecurityParameters params) {
+ checkSetterPreconditions();
+ httpClientSecurityParameters = params;
+ }
+
+ /**
+ * Set the strategy to lookup URI from the criteria set.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setCriteriaToEndpointStrategy(@Nonnull final Function<CriteriaSet, URI> strategy) {
+ checkSetterPreconditions();
+ criteriaToEndpointStrategy = Constraint.isNotNull(strategy, "Criteria to endpoint strategy cannot be null");
+ }
+
+ /**
+ * Build the {@link HttpClientContext} instance to be used by the HttpClient.
+ *
+ * @param request the HTTP client request
+ * @return the client context instance
+ */
+ @Nonnull
+ protected HttpClientContext buildHttpContext(@Nonnull final ClassicHttpRequest request) {
+ final HttpClientContext clientContext = HttpClientContext.create();
+ assert clientContext != null;
+ HttpClientSecuritySupport.marshalSecurityParameters(clientContext, httpClientSecurityParameters, false);
+ HttpClientSecuritySupport.addDefaultTLSTrustEngineCriteria(clientContext, request);
+ return clientContext;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+ if (httpClient == null) {
+ throw new ComponentInitializationException("Httpclient cannot be null");
+ }
+ if (criteriaToEndpointStrategy == null) {
+ throw new ComponentInitializationException("Criteria to endpoint strategy cannot be null");
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override @Nullable
+ public EntityStatement apply(@Nullable final CriteriaSet criteria) {
+ checkComponentActive();
+ final URI uri = criteriaToEndpointStrategy.apply(criteria);
+ if (uri == null) {
+ log.error("No URI could be resolved for fetching entity statement");
+ return null;
+ }
+ log.debug("Using URI {} for fetching entity statement", uri);
+ final HttpGet httpRequest = new HttpGet(uri);
+ final HttpClientContext httpContext = buildHttpContext(httpRequest);
+ try (final ClassicHttpResponse response = httpClient.executeOpen(null, httpRequest, httpContext)) {
+ final String scheme = httpRequest.getUri().getScheme();
+ assert scheme != null;
+ HttpClientSecuritySupport.checkTLSCredentialEvaluated(httpContext, scheme);
+ if (response.getCode() == HttpStatus.SC_OK) {
+ return EntityStatement.parse(EntityUtils.toString(response.getEntity()));
+ } else {
+ log.debug("Unable to fetch entity configuration from URI: {} (HTTP status {})", uri,
+ response.getCode());
+ return null;
+ }
+ } catch (final org.apache.hc.core5.http.ParseException | URISyntaxException | IOException |
+ ParseException e) {
+ log.debug("Unable to fetch entity statement from URI: {}", uri, e);
+ }
+
+ return null;
+ }
+
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultEntityStatementSignatureValidationFilterStrategy.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultEntityStatementSignatureValidationFilterStrategy.java
new file mode 100644
index 00000000..a40a1279
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultEntityStatementSignatureValidationFilterStrategy.java
@@ -0,0 +1,121 @@
+/*
+ * Licensed 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.idp.plugin.oidc.op.oidfed.metadata;
+
+import java.util.function.BiFunction;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.security.SecurityException;
+import org.opensaml.security.trust.TrustEngine;
+import org.slf4j.Logger;
+
+import com.nimbusds.jwt.SignedJWT;
+import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
+
+import net.shibboleth.oidc.metadata.filter.MetadataFilterContext;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.ThreadSafeAfterInit;
+import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.resolver.CriteriaSet;
+
+/**
+ * Default signature validating filter for entity statement. The signature validation is performed via configurable
+ * {@link TrustEngine}.
+ */
+ at ThreadSafeAfterInit
+public class DefaultEntityStatementSignatureValidationFilterStrategy extends AbstractIdentifiableInitializableComponent
+ implements BiFunction<EntityStatement, MetadataFilterContext, EntityStatement> {
+
+ /** Class logger. */
+ @Nonnull private Logger log =
+ LoggerFactory.getLogger(DefaultEntityStatementSignatureValidationFilterStrategy.class);
+
+ /** Trust engine used to validate a signature. */
+ @NonnullAfterInit private TrustEngine<SignedJWT> trustEngine;
+
+ /** Set of externally specified default criteria for input to the trust engine. */
+ @Nullable private CriteriaSet defaultCriteria;
+
+ /**
+ * Set trust engine used to validate a signature.
+ *
+ * @param engine trust engine
+ */
+ public void setTrustEngine(@Nonnull final TrustEngine<SignedJWT> engine) {
+ checkSetterPreconditions();
+ trustEngine = Constraint.isNotNull(engine, "Trust Engine cannot be null");
+ }
+
+ /**
+ * Set the optional set of default criteria used as input to the trust engine.
+ *
+ * @param criteria criteria set to use
+ */
+ public void setDefaultCriteria(@Nullable final CriteriaSet criteria) {
+ checkSetterPreconditions();
+ defaultCriteria = criteria;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+ if (trustEngine == null) {
+ throw new ComponentInitializationException("Trust Engine cannot be null");
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override @Nullable
+ public EntityStatement apply(@Nullable final EntityStatement entityStatement,
+ @Nullable final MetadataFilterContext filterContext) {
+ checkComponentActive();
+ if (entityStatement == null) {
+ return null;
+ }
+
+ final String entityId = entityStatement.getEntityID().getValue();
+ log.trace("Starting signature validation of entity statement for {}", entityId);
+ final CriteriaSet criteria = new CriteriaSet(new SubjectEntityStatementCriterion(entityStatement));
+ if (validateStatement(entityStatement, criteria, entityId)) {
+ return entityStatement;
+ }
+ return null;
+ }
+
+ protected boolean validateStatement(@Nonnull final EntityStatement entityStatement,
+ @Nonnull final CriteriaSet criteria, @Nullable final String entityId) {
+ if (defaultCriteria != null && !defaultCriteria.isEmpty()) {
+ criteria.addAll(defaultCriteria);
+ }
+ try {
+ final SignedJWT jwt = entityStatement.getSignedStatement();
+ assert jwt != null;
+ if (trustEngine.validate(jwt, criteria)) {
+ log.debug("Successfully validated entity statement for {}", entityId);
+ return true;
+ }
+ } catch (final SecurityException e) {
+ log.debug("Could not validate entity statement for {}", entityId, e);
+ }
+ log.warn("Trust Engine validation failed for {}", entityId);
+ return false;
+ }
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultEntityStatementSubjectIdentifierExtractionStrategy.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultEntityStatementSubjectIdentifierExtractionStrategy.java
new file mode 100644
index 00000000..75e6fec6
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultEntityStatementSubjectIdentifierExtractionStrategy.java
@@ -0,0 +1,40 @@
+/*
+ * Licensed 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.idp.plugin.oidc.op.oidfed.metadata;
+
+import java.util.function.Function;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.ThreadSafe;
+
+import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
+
+/**
+ * Default strategy to extract entity ID from an entity statement.
+ */
+ at ThreadSafe
+public class DefaultEntityStatementSubjectIdentifierExtractionStrategy
+ implements Function<EntityStatement, String> {
+
+ /** {@inheritDoc} */
+ @Override @Nullable
+ public String apply(@Nullable final EntityStatement entityStatement) {
+ if (entityStatement == null) {
+ return null;
+ }
+ return entityStatement.getEntityID().getValue();
+ }
+
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultIssuerEntityIDCriteriaToIdentifierStrategy.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultIssuerEntityIDCriteriaToIdentifierStrategy.java
new file mode 100644
index 00000000..51b131fa
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultIssuerEntityIDCriteriaToIdentifierStrategy.java
@@ -0,0 +1,42 @@
+/*
+ * Licensed 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.idp.plugin.oidc.op.oidfed.metadata;
+
+import java.util.function.Function;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.ThreadSafe;
+
+import net.shibboleth.shared.resolver.CriteriaSet;
+
+/**
+ * Strategy for extracting an issuer entity ID via {@link IssuerEntityIDCriterion} from a {@link CriteriaSet}.
+ */
+ at ThreadSafe
+public class DefaultIssuerEntityIDCriteriaToIdentifierStrategy implements Function<CriteriaSet, String> {
+
+ /** {@inheritDoc} */
+ @Override @Nullable
+ public String apply(@Nullable final CriteriaSet criteria) {
+ if (criteria == null) {
+ return null;
+ }
+ final IssuerEntityIDCriterion criterion = criteria.get(IssuerEntityIDCriterion.class);
+ if (criterion != null) {
+ return criterion.getValue();
+ }
+ return null;
+ }
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultLocalTrustAnchorCredentialResolver.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultLocalTrustAnchorCredentialResolver.java
new file mode 100644
index 00000000..c663131a
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultLocalTrustAnchorCredentialResolver.java
@@ -0,0 +1,107 @@
+/*
+ * Licensed 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.idp.plugin.oidc.op.oidfed.metadata;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.security.credential.Credential;
+import org.slf4j.Logger;
+
+import com.nimbusds.jose.jwk.JWK;
+import com.nimbusds.jose.jwk.JWKSet;
+
+import net.shibboleth.oidc.metadata.cache.MetadataCache;
+import net.shibboleth.oidc.metadata.cache.MetadataCacheException;
+import net.shibboleth.oidc.security.credential.impl.BasicJOSEObjectCredentialResolver;
+import net.shibboleth.shared.annotation.ParameterName;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.resolver.CriteriaSet;
+import net.shibboleth.shared.resolver.ResolverException;
+
+/**
+ * Default resolver for trusted trust anchor key resolution. A configurable {@link MetadataCache} is used for fetching
+ * the trusted/local public credentials for the entity referred via {@link SubjectEntityStatementCriterion}.
+ */
+public class DefaultLocalTrustAnchorCredentialResolver extends BasicJOSEObjectCredentialResolver {
+
+ /** Logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(DefaultLocalTrustAnchorCredentialResolver.class);
+
+ /** Cache containing local copies of trusted trust anchor keys. */
+ @Nonnull private MetadataCache<Map<String, LocalKeyContainer>> localTrustAnchorsCache;
+
+ /**
+ * Constructor.
+ *
+ * @param cache cache containing local copies of trusted trust anchor keys
+ */
+ public DefaultLocalTrustAnchorCredentialResolver(
+ @Nonnull @ParameterName(name="cache") final MetadataCache<Map<String, LocalKeyContainer>> cache) {
+ localTrustAnchorsCache = Constraint.isNotNull(cache, "Local Trust Anchors cache cannot be null");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected Iterable<Credential> resolveFromSource(@Nullable final CriteriaSet criteriaSet) throws ResolverException {
+ if (criteriaSet == null) {
+ throw new ResolverException("No criteria supplied");
+ }
+ final SubjectEntityStatementCriterion subjectCriterion = criteriaSet.get(SubjectEntityStatementCriterion.class);
+ if (subjectCriterion == null) {
+ log.debug("No SubjectEntityStatementCriterion criteria supplied, resolver could not process");
+ throw new ResolverException(
+ "Credential criteria set did not contain an instance of SubjectEntityStatementCriterion");
+ }
+ final String entityId = subjectCriterion.getValue().getEntityID().getValue();
+ log.debug("Attempting to find trusted keys for {}", entityId);
+
+ final List<Map<String, LocalKeyContainer>> keyContainers;
+ try {
+ keyContainers = localTrustAnchorsCache.get(criteriaSet);
+ } catch (final MetadataCacheException e) {
+ throw new ResolverException("Could not resolve local trust anchor keys from the cache", e);
+ }
+ if (keyContainers.isEmpty() || !keyContainers.get(0).containsKey(entityId)) {
+ log.debug("No keys found for {}", entityId);
+ return CollectionSupport.emptyList();
+ }
+ final LocalKeyContainer keyContainer = keyContainers.get(0).get(entityId);
+ if (keyContainer == null || keyContainer.getJWKSet() == null) {
+ log.debug("No JWKSet found for {}", entityId);
+ return CollectionSupport.emptyList();
+ }
+ final JWKSet jwkSet = keyContainer.getJWKSet();
+ final List<Credential> credentials = new ArrayList<>();
+ assert jwkSet != null;
+ for (final JWK jwk : jwkSet.getKeys()) {
+ if (jwk != null) {
+ final Credential cred = buildJWKCredential(jwk, null);
+ if (cred != null) {
+ credentials.add(cred);
+ }
+ }
+ }
+ log.debug("Returning credentials {} for {}", credentials, entityId);
+ return credentials;
+ }
+
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultLocalTrustAnchorsValidator.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultLocalTrustAnchorsValidator.java
new file mode 100644
index 00000000..28d9d10d
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultLocalTrustAnchorsValidator.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed 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.idp.plugin.oidc.op.oidfed.metadata;
+
+import java.util.Map;
+import java.util.function.Predicate;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.ThreadSafe;
+
+import org.slf4j.Logger;
+
+import com.nimbusds.jose.jwk.JWKSet;
+
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * Default validator for local trust anchor keyset. It verifies that the keyset only contains public keys. Otherwise
+ * the keyset is considered as invalid.
+ */
+ at ThreadSafe
+public class DefaultLocalTrustAnchorsValidator implements Predicate<Map<String, LocalKeyContainer>> {
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(DefaultLocalTrustAnchorsValidator.class);
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean test(@Nullable final Map<String, LocalKeyContainer> keyContainers) {
+ if (keyContainers == null) {
+ return false;
+ }
+ for (final String anchor : keyContainers.keySet()) {
+ final LocalKeyContainer container = keyContainers.get(anchor);
+ if (container != null) {
+ final JWKSet jwkSet = container.getJWKSet();
+ if (jwkSet != null && jwkSet.containsNonPublicKeys()) {
+ log.warn("Not accepting keyset for {} as it contains non-public keys", anchor);
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultSubjectEntityIDCriteriaToIdentifierStrategy.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultSubjectEntityIDCriteriaToIdentifierStrategy.java
new file mode 100644
index 00000000..cdc5247d
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultSubjectEntityIDCriteriaToIdentifierStrategy.java
@@ -0,0 +1,42 @@
+/*
+ * Licensed 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.idp.plugin.oidc.op.oidfed.metadata;
+
+import java.util.function.Function;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.ThreadSafe;
+
+import net.shibboleth.shared.resolver.CriteriaSet;
+
+/**
+ * Strategy for extracting the {@link SubjectEntityIDCriterion} contents from a {@link CriteriaSet}.
+ */
+ at ThreadSafe
+public class DefaultSubjectEntityIDCriteriaToIdentifierStrategy implements Function<CriteriaSet, String> {
+
+ /** {@inheritDoc} */
+ @Override @Nullable
+ public String apply(@Nullable final CriteriaSet criteria) {
+ if (criteria == null) {
+ return null;
+ }
+ final SubjectEntityIDCriterion criterion = criteria.get(SubjectEntityIDCriterion.class);
+ if (criterion != null) {
+ return criterion.getValue();
+ }
+ return null;
+ }
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultSubordinateStatementCriteriaToIdentifierStrategy.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultSubordinateStatementCriteriaToIdentifierStrategy.java
new file mode 100644
index 00000000..6e589b29
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultSubordinateStatementCriteriaToIdentifierStrategy.java
@@ -0,0 +1,93 @@
+/*
+ * Licensed 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.idp.plugin.oidc.op.oidfed.metadata;
+
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.ThreadSafeAfterInit;
+import net.shibboleth.shared.collection.Pair;
+import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.resolver.CriteriaSet;
+
+/**
+ * Default identifier strategy for subordinate statement. The criteria set is transformed into a {@link Pair} that
+ * consists of subject and issuer entity IDs.
+ */
+ at ThreadSafeAfterInit
+public class DefaultSubordinateStatementCriteriaToIdentifierStrategy
+ extends AbstractIdentifiableInitializableComponent implements Function<CriteriaSet, Pair<String, String>> {
+
+ /** Strategy to fetch subject entity ID from criteria set. */
+ @NonnullAfterInit private Function<CriteriaSet, String> criteriaToSubjectEntityIdStrategy;
+
+ /** Strategy to fetch issuer entity ID from criteria set. */
+ @NonnullAfterInit private Function<CriteriaSet, String> criteriaToIssuerEntityIdStrategy;
+
+ /**
+ * Set the strategy to fetch subject entity ID from criteria set.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setCriteriaToSubjectEntityIdStrategy(@Nonnull final Function<CriteriaSet, String> strategy) {
+ checkSetterPreconditions();
+ criteriaToSubjectEntityIdStrategy = Constraint.isNotNull(strategy,
+ "Criteria to subject entity ID strategy cannot be null");
+ }
+
+ /**
+ * Set the strategy to fetch issuer entity ID from criteria set.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setCriteriaToIssuerEntityIdStrategy(@Nonnull final Function<CriteriaSet, String> strategy) {
+ checkSetterPreconditions();
+ criteriaToIssuerEntityIdStrategy = Constraint.isNotNull(strategy,
+ "Criteria to issuer entity ID strategy cannot be null");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+ if (criteriaToSubjectEntityIdStrategy == null) {
+ throw new ComponentInitializationException("Criteria to subject entity ID strategy cannot be null");
+ }
+ if (criteriaToIssuerEntityIdStrategy == null) {
+ throw new ComponentInitializationException("Criteria to issuer entity ID strategy cannot be null");
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override @Nullable
+ public Pair<String, String> apply(@Nullable final CriteriaSet criteria) {
+ checkComponentActive();
+ final String subjectEntityId = criteriaToSubjectEntityIdStrategy.apply(criteria);
+ if (subjectEntityId == null) {
+ return null;
+ }
+ final String issuerEntityId = criteriaToIssuerEntityIdStrategy.apply(criteria);
+ if (issuerEntityId == null) {
+ return null;
+ }
+ return new Pair<>(issuerEntityId, subjectEntityId);
+ }
+
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultSubordinateStatementEndpointExtractionStrategy.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultSubordinateStatementEndpointExtractionStrategy.java
new file mode 100644
index 00000000..c5d6920a
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultSubordinateStatementEndpointExtractionStrategy.java
@@ -0,0 +1,149 @@
+/*
+ * Licensed 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.idp.plugin.oidc.op.oidfed.metadata;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.List;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.slf4j.Logger;
+
+import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
+import com.nimbusds.openid.connect.sdk.federation.entities.FederationEntityMetadata;
+
+import net.shibboleth.oidc.metadata.cache.MetadataCache;
+import net.shibboleth.oidc.metadata.cache.MetadataCacheException;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.ThreadSafeAfterInit;
+import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.resolver.CriteriaSet;
+
+/**
+ * Default strategy for fetching endpoint URI used for fetching subordinate statement. The issuer's entity configuration
+ * is fetched from a configurable {@link MetadataCache}.
+ */
+ at ThreadSafeAfterInit
+public class DefaultSubordinateStatementEndpointExtractionStrategy extends AbstractIdentifiableInitializableComponent
+ implements Function<CriteriaSet, URI> {
+
+ /** Class logger. */
+ @Nonnull private final Logger log =
+ LoggerFactory.getLogger(DefaultSubordinateStatementEndpointExtractionStrategy.class);
+
+ /** Cache containing entity configurations. */
+ @NonnullAfterInit private MetadataCache<EntityStatement> entityConfigurationCache;
+
+ /** Strategy to fetch subject entity ID from criteria set. */
+ @NonnullAfterInit private Function<CriteriaSet, String> criteriaToSubjectEntityIdStrategy;
+
+ /** Strategy to fetch issuer entity ID from criteria set. */
+ @NonnullAfterInit private Function<CriteriaSet, String> criteriaToIssuerEntityIdStrategy;
+
+ /**
+ * Set the strategy to fetch subject entity ID from criteria set.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setCriteriaToSubjectEntityIdStrategy(@Nonnull final Function<CriteriaSet, String> strategy) {
+ checkSetterPreconditions();
+ criteriaToSubjectEntityIdStrategy = Constraint.isNotNull(strategy,
+ "Criteria to subject entity ID strategy cannot be null");
+ }
+
+ /**
+ * Set the strategy to fetch issuer entity ID from criteria set.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setCriteriaToIssuerEntityIdStrategy(@Nonnull final Function<CriteriaSet, String> strategy) {
+ checkSetterPreconditions();
+ criteriaToIssuerEntityIdStrategy = Constraint.isNotNull(strategy,
+ "Criteria to issuer entity ID strategy cannot be null");
+ }
+
+ /**
+ * Set the cache containing entity configurations.
+ *
+ * @param cache cache containing entity configurations
+ */
+ public void setEntityConfigurationCache(@Nonnull final MetadataCache<EntityStatement> cache) {
+ checkSetterPreconditions();
+ entityConfigurationCache = Constraint.isNotNull(cache, "Entity Configuration cache cannot be null");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+ if (criteriaToSubjectEntityIdStrategy == null) {
+ throw new ComponentInitializationException("Criteria to subject entity ID strategy cannot be null");
+ }
+ if (criteriaToIssuerEntityIdStrategy == null) {
+ throw new ComponentInitializationException("Criteria to issuer entity ID strategy cannot be null");
+ }
+ if (entityConfigurationCache == null) {
+ throw new ComponentInitializationException("Entity Configuration cache cannot be null");
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override @Nullable
+ public URI apply(@Nullable final CriteriaSet criteria) {
+ checkComponentActive();
+ final String subjectEntityId = criteriaToSubjectEntityIdStrategy.apply(criteria);
+ if (subjectEntityId == null) {
+ return null;
+ }
+ final String issuerEntityId = criteriaToIssuerEntityIdStrategy.apply(criteria);
+ if (issuerEntityId == null) {
+ return null;
+ }
+ final List<EntityStatement> issuerStatements;
+ try {
+ issuerStatements =
+ entityConfigurationCache.get(new CriteriaSet(new SubjectEntityIDCriterion(issuerEntityId)));
+ } catch (final MetadataCacheException e) {
+ log.error("Could not resolve entity configuration for issuer {}", issuerEntityId, e);
+ return null;
+ }
+ if (issuerStatements.isEmpty()) {
+ log.error("Could not resolve entity configuration for issuer {}", issuerEntityId);
+ return null;
+ }
+ final FederationEntityMetadata issuerMetadata =
+ issuerStatements.get(0).getClaimsSet().getFederationEntityMetadata();
+ final URI federationFetchEndpoint =
+ issuerMetadata != null ? issuerMetadata.getFederationFetchEndpointURI() : null;
+ if (federationFetchEndpoint == null) {
+ log.warn("No federation fetch endpoint found from the entity configuration for {}", issuerEntityId);
+ return null;
+ }
+
+ try {
+ return new URI(federationFetchEndpoint + "?iss=" + issuerEntityId + "&sub=" + subjectEntityId);
+ } catch (final URISyntaxException e) {
+ log.error("Could not build URI for fetching subordinate entity statement", e);
+ }
+ return null;
+ }
+
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultSubordinateStatementIdentifierExtractionStrategy.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultSubordinateStatementIdentifierExtractionStrategy.java
new file mode 100644
index 00000000..7f2dc612
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultSubordinateStatementIdentifierExtractionStrategy.java
@@ -0,0 +1,43 @@
+/*
+ * Licensed 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.idp.plugin.oidc.op.oidfed.metadata;
+
+import java.util.function.Function;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.ThreadSafe;
+
+import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
+
+import net.shibboleth.shared.collection.Pair;
+
+/**
+ * Default strategy for transforming subordinate statement into a {@link Pair} consisting of issuer and subject entity
+ * IDs.
+ */
+ at ThreadSafe
+public class DefaultSubordinateStatementIdentifierExtractionStrategy
+ implements Function<EntityStatement, Pair<String, String>> {
+
+ /** {@inheritDoc} */
+ @Override @Nullable
+ public Pair<String, String> apply(@Nullable final EntityStatement entityStatement) {
+ if (entityStatement == null) {
+ return null;
+ }
+ return new Pair<>(entityStatement.getClaimsSet().getIssuerEntityID().getValue(),
+ entityStatement.getEntityID().getValue());
+ }
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultSubordinateStatementSignatureValidationFilterStrategy.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultSubordinateStatementSignatureValidationFilterStrategy.java
new file mode 100644
index 00000000..9e33ec80
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultSubordinateStatementSignatureValidationFilterStrategy.java
@@ -0,0 +1,116 @@
+/*
+ * Licensed 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.idp.plugin.oidc.op.oidfed.metadata;
+
+import java.util.List;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.slf4j.Logger;
+
+import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
+
+import net.shibboleth.oidc.metadata.cache.MetadataCache;
+import net.shibboleth.oidc.metadata.cache.MetadataCacheException;
+import net.shibboleth.oidc.metadata.filter.MetadataFilterContext;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.ThreadSafeAfterInit;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.resolver.CriteriaSet;
+
+/**
+ * Default signature validating filter for subordinate statement. This implementation extends
+ * {@link DefaultEntityStatementSignatureValidationFilterStrategy} by fetching the issuer's entity configuration via
+ * configurable {@link MetadataCache} and passing it via {@link IssuerEntityStatementCriterion} in the criteria set.
+ */
+ at ThreadSafeAfterInit
+public class DefaultSubordinateStatementSignatureValidationFilterStrategy
+ extends DefaultEntityStatementSignatureValidationFilterStrategy {
+
+ /** Class logger. */
+ @Nonnull private Logger log =
+ LoggerFactory.getLogger(DefaultSubordinateStatementSignatureValidationFilterStrategy.class);
+
+ /** Cache used to fetch the issuer entity configuration from. */
+ @NonnullAfterInit private MetadataCache<EntityStatement> entityConfigurationCache;
+
+ /**
+ * Set the cache used to fetch the issuer entity configuration from.
+ *
+ * @param cache cache used to fetch the issuer entity configuration from
+ */
+ public void setEntityConfigurationCache(@Nonnull final MetadataCache<EntityStatement> cache) {
+ checkSetterPreconditions();
+ entityConfigurationCache = Constraint.isNotNull(cache, "Entity Configuration cache cannot be null");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+ if (entityConfigurationCache == null) {
+ throw new ComponentInitializationException("Entity Configuration cache cannot be null");
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override @Nullable
+ public EntityStatement apply(@Nullable final EntityStatement entityStatement,
+ @Nullable final MetadataFilterContext filterContext) {
+ checkComponentActive();
+ if (entityStatement == null) {
+ return null;
+ }
+
+ final String entityId = entityStatement.getEntityID().getValue();
+ final String issuer = entityStatement.getClaimsSet().getIssuerEntityID().getValue();
+ assert issuer != null;
+ log.trace("Validating subordinate statement for {}, issued by {}", entityId, issuer);
+ final EntityStatement issuerStatement = fetchIssuerStatement(issuer);
+ if (issuerStatement == null) {
+ return null;
+ }
+ final CriteriaSet criteria = new CriteriaSet();
+ criteria.add(new IssuerEntityStatementCriterion(issuerStatement));
+ criteria.add(new SubjectEntityStatementCriterion(entityStatement));
+ if (validateStatement(entityStatement, criteria, entityId)) {
+ return entityStatement;
+ }
+ return null;
+ }
+
+ /**
+ * Fetch the issuer entity configuration from the metadata cache.
+ *
+ * @param issuer the issuer entity ID
+ * @return the issuer entity configuration, or null if could not be fetched
+ */
+ @Nullable protected EntityStatement fetchIssuerStatement(@Nonnull final String issuer) {
+ final CriteriaSet criteria = new CriteriaSet(new SubjectEntityIDCriterion(issuer));
+ try {
+ final List<EntityStatement> result = entityConfigurationCache.get(criteria);
+ if (!result.isEmpty()) {
+ return result.get(0);
+ }
+ } catch (final MetadataCacheException e) {
+ log.debug("Error while fetching issuer entity configuration for {}", issuer, e);
+ }
+ log.warn("Could not fetch entity configuration for {}", issuer);
+ return null;
+ }
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultTrustAnchorSignatureValidationFilterStrategy.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultTrustAnchorSignatureValidationFilterStrategy.java
new file mode 100644
index 00000000..b07d2c9b
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultTrustAnchorSignatureValidationFilterStrategy.java
@@ -0,0 +1,100 @@
+/*
+ * Licensed 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.idp.plugin.oidc.op.oidfed.metadata;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.BiFunction;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.slf4j.Logger;
+
+import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
+
+import net.shibboleth.oidc.metadata.filter.MetadataFilterContext;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.ThreadSafeAfterInit;
+import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * Default signature validating filter trust chains. The signature validation is performed by passing the trust anchor
+ * entity statement of each trust chain to the configurable validation filter strategy. All the chains whose anchor
+ * doesn't pass the validation are filtered out from the result.
+ */
+ at ThreadSafeAfterInit
+public class DefaultTrustAnchorSignatureValidationFilterStrategy extends AbstractIdentifiableInitializableComponent
+ implements BiFunction<List<List<EntityStatement>>, MetadataFilterContext, List<List<EntityStatement>>> {
+
+ /** Class logger. */
+ @Nonnull private Logger log =
+ LoggerFactory.getLogger(DefaultTrustAnchorSignatureValidationFilterStrategy.class);
+
+ /** Signature validation filter strategy for trust anchor entity statements. */
+ @NonnullAfterInit private BiFunction<EntityStatement, MetadataFilterContext, EntityStatement>
+ entityStatementSignatureValidationFilterStrategy;
+
+ /**
+ * Set the signature validation filter strategy for trust anchor entity statements.
+ *
+ * @param strategy signature validation filter strategy
+ */
+ public void setEntityStatementSignatureValidationFilterStrategy(@Nonnull final
+ BiFunction<EntityStatement, MetadataFilterContext, EntityStatement> strategy) {
+ checkSetterPreconditions();
+ entityStatementSignatureValidationFilterStrategy = Constraint.isNotNull(strategy,
+ "EntityStatementSignatureValidationFilterStrategy cannot be null");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+ if (entityStatementSignatureValidationFilterStrategy == null) {
+ throw new ComponentInitializationException(
+ "EntityStatementSignatureValidationFilterStrategy cannot be null");
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override @Nullable
+ public List<List<EntityStatement>> apply(@Nullable final List<List<EntityStatement>> trustChains,
+ @Nullable final MetadataFilterContext filterContext) {
+ checkComponentActive();
+ if (trustChains == null) {
+ return null;
+ }
+
+ final List<List<EntityStatement>> result = new ArrayList<>();
+ for (final List<EntityStatement> trustChain : trustChains) {
+ final EntityStatement trustAnchorStatement = trustChain.get(trustChain.size() - 1);
+ final String entityId = trustAnchorStatement.getEntityID().getValue();
+ final EntityStatement filteredStatement =
+ entityStatementSignatureValidationFilterStrategy.apply(trustAnchorStatement, filterContext);
+ if (filteredStatement != null) {
+ log.debug("Successfully validated entity statement for trust anchor {}", entityId);
+ result.add(trustChain);
+ } else {
+ log.debug("Signature validation failed for trust anchor {}", entityId);
+ }
+ }
+ return result;
+ }
+
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultTrustChainExpirationTimeStrategy.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultTrustChainExpirationTimeStrategy.java
new file mode 100644
index 00000000..ddcc640c
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultTrustChainExpirationTimeStrategy.java
@@ -0,0 +1,60 @@
+/*
+ * Licensed 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.idp.plugin.oidc.op.oidfed.metadata;
+
+import java.time.Instant;
+import java.util.List;
+import java.util.function.Function;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.ThreadSafe;
+
+import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
+
+import net.shibboleth.oidc.metadata.cache.ExpirationTimeContext;
+
+/**
+ * Default strategy for fetching expiration time for the list of trust chains. The shortest expiration of all the chains
+ * is taken and compared to the maximum cache duration defined in the {@link ExpirationTimeContext}.
+ */
+ at ThreadSafe
+public class DefaultTrustChainExpirationTimeStrategy
+ implements Function<ExpirationTimeContext<List<List<EntityStatement>>>, Instant> {
+
+ /** {@inheritDoc} */
+ @Nullable public Instant apply(@Nullable final ExpirationTimeContext<List<List<EntityStatement>>> context) {
+ if (context == null) {
+ return null;
+ }
+ final Instant contextExpiration = context.getNow().plus(context.getMaxCacheDuration());
+ final List<List<EntityStatement>> chains = context.getMetadata();
+ if (chains == null || chains.isEmpty()) {
+ return contextExpiration;
+ }
+ Instant metadataExpiration = null;
+ for (final List<EntityStatement> chain : chains) {
+ for (final EntityStatement statement : chain) {
+ final Instant statementExpiration = statement.getClaimsSet().getExpirationTime().toInstant();
+ metadataExpiration = metadataExpiration == null ? statementExpiration :
+ statementExpiration.isBefore(metadataExpiration) ? statementExpiration : metadataExpiration;
+ }
+ }
+ if (metadataExpiration == null) {
+ return contextExpiration;
+ }
+ return metadataExpiration.isBefore(contextExpiration) ? metadataExpiration : contextExpiration;
+ }
+
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultTrustChainFetchingStrategy.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultTrustChainFetchingStrategy.java
new file mode 100644
index 00000000..aa74b727
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultTrustChainFetchingStrategy.java
@@ -0,0 +1,228 @@
+/*
+ * Licensed 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.idp.plugin.oidc.op.oidfed.metadata;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.slf4j.Logger;
+
+import com.nimbusds.openid.connect.sdk.federation.entities.EntityID;
+import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
+
+import net.shibboleth.oidc.metadata.cache.MetadataCache;
+import net.shibboleth.oidc.metadata.cache.MetadataCacheException;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.NonnullElements;
+import net.shibboleth.shared.annotation.constraint.ThreadSafeAfterInit;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.collection.Pair;
+import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.resolver.CriteriaSet;
+
+/**
+ * Default strategy for fetching trust chains for an entity specified in the criteria set. Caches for entity
+ * configurations are subordinate statements are exploited for actual fetching of the entity statements.
+ */
+ at ThreadSafeAfterInit
+public class DefaultTrustChainFetchingStrategy extends AbstractIdentifiableInitializableComponent
+ implements Function<CriteriaSet, List<List<EntityStatement>>> {
+
+ /** Class logger. */
+ @Nonnull private Logger log = LoggerFactory.getLogger(DefaultTrustChainFetchingStrategy.class);
+
+ /** Strategy for fetching entity ID from the criteria set. */
+ @NonnullAfterInit private Function<CriteriaSet, String> criteriaToSubjectEntityIdStrategy;
+
+ /** Cache for entity configurations. */
+ @NonnullAfterInit private MetadataCache<EntityStatement> entityConfigurationCache;
+
+ /** Cache for subordinate entity statements. */
+ @NonnullAfterInit private MetadataCache<EntityStatement> subordinateStatementCache;
+
+ /**
+ * Set the strategy for fetching entity ID from the criteria set.
+ *
+ * @param strategy fetching strategy
+ */
+ public void setCriteriaToSubjectEntityIdStrategy(@Nonnull final Function<CriteriaSet, String> strategy) {
+ checkSetterPreconditions();
+ criteriaToSubjectEntityIdStrategy = Constraint.isNotNull(strategy,
+ "Criteria to subject entity ID strategy cannot be null");
+ }
+
+ /**
+ * Set the cache for entity configurations.
+ *
+ * @param cache entity configurations cache
+ */
+ public void setEntityConfigurationCache(@Nonnull final MetadataCache<EntityStatement> cache) {
+ checkSetterPreconditions();
+ entityConfigurationCache = Constraint.isNotNull(cache, "Entity Configuration cache cannot be null");
+ }
+
+ /**
+ * Set the cache for subordinate statement.
+ *
+ * @param cache subordinate statements cache.
+ */
+ public void setSubordinateStatementCache(@Nonnull final MetadataCache<EntityStatement> cache) {
+ checkSetterPreconditions();
+ subordinateStatementCache = Constraint.isNotNull(cache, "Subordinate statement cache cannot be null");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+ if (criteriaToSubjectEntityIdStrategy == null) {
+ throw new ComponentInitializationException("Criteria to subject entity ID strategy cannot be null");
+ }
+ if (entityConfigurationCache == null) {
+ throw new ComponentInitializationException("Entity configuration cache cannot be null");
+ }
+ if (subordinateStatementCache == null) {
+ throw new ComponentInitializationException("Subordinate statement cache cannot be null");
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override @Nullable
+ public List<List<EntityStatement>> apply(@Nullable final CriteriaSet criteria) {
+ checkComponentActive();
+ if (criteria == null) {
+ return null;
+ }
+ try {
+ final List<EntityStatement> entityConfigurations = entityConfigurationCache.get(criteria);
+ if (entityConfigurations.isEmpty()) {
+ return null;
+ }
+ final EntityStatement entityConfiguration = entityConfigurationCache.get(criteria).get(0);
+ assert entityConfiguration != null;
+ final List<List<EntityStatement>> chain = populateChain(
+ CollectionSupport.listOf(CollectionSupport.listOf(entityConfiguration)));
+ return stripIntermediateConfigurations(entityConfiguration, chain);
+ } catch (final MetadataCacheException e) {
+ log.error("Could not fetch entity configuration for the trust chain", e);
+ }
+ return null;
+ }
+
+ /**
+ * Strips the intermediate entity configurations out from the list of trust chains.
+ *
+ * @param leaf the leaf entity configuration
+ * @param chains the list of trust chains to be filtered
+ * @return the filtered list of trust chains, intermediate configurations filtered out
+ */
+ private List<List<EntityStatement>> stripIntermediateConfigurations(@Nonnull final EntityStatement leaf,
+ @Nonnull @NonnullElements final List<List<EntityStatement>> chains) {
+ final List<List<EntityStatement>> result = new ArrayList<>();
+ for (final List<EntityStatement> chain : chains) {
+ result.add(chain.stream()
+ .filter(es -> (es.equals(leaf) || es.getClaimsSet().getAuthorityHints() == null
+ || es.getClaimsSet().getAuthorityHints().isEmpty()
+ || !es.getEntityID().equals(es.getClaimsSet().getIssuerEntityID())))
+ .toList());
+ }
+ return result;
+ }
+
+ /**
+ * Recursively populates the given list of trust chains until the last entity statement in each chain doesn't
+ * contain any authority hints.
+ *
+ * @param entities the list of trust chains to be populated.
+ * @return
+ */
+ @Nonnull @NonnullElements private List<List<EntityStatement>> populateChain(
+ @Nonnull @NonnullElements final List<List<EntityStatement>> entities) {
+ final List<List<EntityStatement>> result = new ArrayList<>();
+ boolean hints = false;
+ for (final List<EntityStatement> chain : entities) {
+ final EntityStatement entityStatement = chain.get(chain.size() - 1);
+ final List<EntityID> authorityHints = entityStatement.getClaimsSet().getAuthorityHints();
+ if (authorityHints == null || authorityHints.isEmpty()) {
+ result.add(chain);
+ } else {
+ // final entity statement in the chain contains authority hints that needs to be resolved
+ hints = true;
+ final List<Pair<EntityStatement, EntityStatement>> authorities = authorityHints.stream()
+ .map(EntityID::getValue)
+ .map(id -> fetchAuthority(entityStatement, id))
+ .filter(pair -> pair != null && pair.getFirst() != null && pair.getSecond() != null)
+ .toList();
+ authorities.stream().forEach(authority -> {
+ final ArrayList<EntityStatement> newChain = new ArrayList<>(chain);
+ newChain.add(authority.getSecond());
+ newChain.add(authority.getFirst());
+ result.add(newChain);
+ });
+ }
+ }
+ if (hints) {
+ return populateChain(result);
+ }
+ return result;
+ }
+
+ /**
+ * Fetch authority's entity configuration and subordinate statement for the given entity statement and
+ * authority hint.
+ *
+ * @param entity the entity statement whose authority and subordinate statement are to be fetched
+ * @param authorityHint the authority entity ID
+ * @return pair consisting of authority's entity configuration and subordinate statement, or null if they could not
+ * be fetched.
+ */
+ @Nullable protected Pair<EntityStatement, EntityStatement> fetchAuthority(@Nonnull final EntityStatement entity,
+ @Nullable final String authorityHint) {
+ final String entityId = entity.getEntityID().getValue();
+ if (entityId == null || authorityHint == null) {
+ log.error("Entity ID ({}) or authority hint ({}) may not be null", entityId, authorityHint);
+ return null;
+ }
+ final CriteriaSet criteria = new CriteriaSet(new SubjectEntityIDCriterion(entityId),
+ new IssuerEntityIDCriterion(authorityHint));
+
+ try {
+ final EntityStatement authorityConfiguration = getFirstIfFound(
+ entityConfigurationCache.get(new CriteriaSet(new SubjectEntityIDCriterion(authorityHint))));
+ final EntityStatement metadata = getFirstIfFound(subordinateStatementCache.get(criteria));
+ return new Pair<>(authorityConfiguration, metadata);
+ } catch (final MetadataCacheException e) {
+ log.error("Could not resolve authority hint {} for {)", authorityHint, entityId);
+ return null;
+ }
+ }
+
+ /**
+ * Get the first entity statement from the given list if it's not empty.
+ *
+ * @param statements list of entity statements
+ * @return first entity statemenet, or null the list was empty.
+ */
+ @Nullable private EntityStatement getFirstIfFound(@Nonnull final List<EntityStatement> statements) {
+ return statements.size() > 0 ? statements.get(0) : null;
+ }
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultTrustChainSubjectIdentifierExtractionStrategy.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultTrustChainSubjectIdentifierExtractionStrategy.java
new file mode 100644
index 00000000..83675438
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultTrustChainSubjectIdentifierExtractionStrategy.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed 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.idp.plugin.oidc.op.oidfed.metadata;
+
+import java.util.List;
+import java.util.function.Function;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.ThreadSafe;
+
+import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
+
+/**
+ * Default identifier strategy for the list of trust chains: the entity ID of the leaf entity is always used.
+ */
+ at ThreadSafe
+public class DefaultTrustChainSubjectIdentifierExtractionStrategy
+ implements Function<List<List<EntityStatement>>, String> {
+
+ /** {@inheritDoc} */
+ @Override @Nullable
+ public String apply(@Nullable final List<List<EntityStatement>> trustChains) {
+ if (trustChains == null || trustChains.isEmpty()) {
+ return null;
+ }
+ final List<EntityStatement> firstChain = trustChains.get(0);
+ if (firstChain == null || firstChain.isEmpty() || firstChain.get(0) == null) {
+ return null;
+ }
+ return firstChain.get(0).getEntityID().getValue();
+ }
+
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/IssuerEntityIDCriterion.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/IssuerEntityIDCriterion.java
new file mode 100644
index 00000000..a47d7ebe
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/IssuerEntityIDCriterion.java
@@ -0,0 +1,77 @@
+/*
+ * Licensed 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.idp.plugin.oidc.op.oidfed.metadata;
+
+import java.util.Objects;
+
+import javax.annotation.Nonnull;
+
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.resolver.Criterion;
+
+/**
+ * A {@link Criterion} representing an issuer entity ID in entity statement.
+ */
+public class IssuerEntityIDCriterion implements Criterion {
+
+ /** The entity ID value. */
+ @Nonnull final String value;
+
+ /**
+ * Constructor.
+ *
+ * @param entityId the entity ID value, must not be null
+ */
+ public IssuerEntityIDCriterion(@Nonnull final String entityId) {
+ value = Constraint.isNotEmpty(entityId, "Entity ID cannot be null");
+ }
+
+ /**
+ * Get the entity ID value.
+ *
+ * @return the entity ID value
+ */
+ public String getValue() {
+ return value;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public String toString() {
+ return "IssuerEntityIDCriterion [value=" + value + "]";
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public int hashCode() {
+ return Objects.hash(value);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ final IssuerEntityIDCriterion other = (IssuerEntityIDCriterion) obj;
+ return value.equals(other.value);
+ }
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/IssuerEntityStatementCriterion.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/IssuerEntityStatementCriterion.java
new file mode 100644
index 00000000..1e3a88e2
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/IssuerEntityStatementCriterion.java
@@ -0,0 +1,79 @@
+/*
+ * Licensed 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.idp.plugin.oidc.op.oidfed.metadata;
+
+import java.util.Objects;
+
+import javax.annotation.Nonnull;
+
+import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
+
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.resolver.Criterion;
+
+/**
+ * A {@link Criterion} representing an issuer entity statement.
+ */
+public class IssuerEntityStatementCriterion implements Criterion {
+
+ /** The entity statement value. */
+ @Nonnull final EntityStatement value;
+
+ /**
+ * Constructor.
+ *
+ * @param entityStatement the entity statement value, must not be null
+ */
+ public IssuerEntityStatementCriterion(@Nonnull final EntityStatement entityStatement) {
+ value = Constraint.isNotNull(entityStatement, "Entity statement cannot be null");
+ }
+
+ /**
+ * Get the entity statement value.
+ *
+ * @return the entity statement value
+ */
+ public EntityStatement getValue() {
+ return value;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public String toString() {
+ return "IssuerEntityStatementCriterion [value=" + value.getSignedStatement().serialize() + "]";
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public int hashCode() {
+ return Objects.hash(value);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ final IssuerEntityStatementCriterion other = (IssuerEntityStatementCriterion) obj;
+ return value.getSignedStatement().serialize().equals(other.value.getSignedStatement().serialize());
+ }
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/JWKSetDeserializer.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/JWKSetDeserializer.java
new file mode 100644
index 00000000..c543b314
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/JWKSetDeserializer.java
@@ -0,0 +1,57 @@
+/*
+ * Licensed 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.idp.plugin.oidc.op.oidfed.metadata;
+
+import java.io.IOException;
+import java.text.ParseException;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.slf4j.Logger;
+
+import com.fasterxml.jackson.core.JacksonException;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonDeserializer;
+import com.nimbusds.jose.jwk.JWKSet;
+
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * Jackson deserializer for {@link LocalKeyContainer}.
+ */
+public class JWKSetDeserializer extends JsonDeserializer<LocalKeyContainer> {
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(JWKSetDeserializer.class);
+
+ /** {@inheritDoc} */
+ @Override
+ public LocalKeyContainer deserialize(@Nullable final JsonParser jsonParser,
+ @Nullable final DeserializationContext deserializationContext)
+ throws IOException, JacksonException {
+ assert jsonParser != null;
+ final String raw = jsonParser.readValueAsTree().toString();
+ log.trace("Raw string to be parsed into JWTSet: {}", raw);
+ try {
+ final LocalKeyContainer keyContainer = new LocalKeyContainer();
+ keyContainer.setJWKSet(JWKSet.parse(raw));
+ return keyContainer;
+ } catch (final ParseException e) {
+ throw new IOException("Could not parse JWKSet", e);
+ }
+ }
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/LocalKeyContainer.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/LocalKeyContainer.java
new file mode 100644
index 00000000..a070c9e3
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/LocalKeyContainer.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed 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.idp.plugin.oidc.op.oidfed.metadata;
+
+import javax.annotation.Nullable;
+
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.nimbusds.jose.jwk.JWKSet;
+
+/**
+ * Key set container class wrapping {@link JWKSet}.
+ */
+ at JsonDeserialize(using = JWKSetDeserializer.class)
+public class LocalKeyContainer {
+
+ /** The key set. */
+ @Nullable private JWKSet jwkSet;
+
+ /**
+ * Constructor.
+ */
+ public LocalKeyContainer() {
+ }
+
+ /**
+ * Get the key set.
+ *
+ * @return the key set
+ */
+ @Nullable JWKSet getJWKSet() {
+ return jwkSet;
+ }
+
+ /**
+ * Set the key set.
+ * @param set key set
+ */
+ public void setJWKSet(@Nullable final JWKSet set) {
+ jwkSet = set;
+ }
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/SubjectEntityIDCriterion.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/SubjectEntityIDCriterion.java
new file mode 100644
index 00000000..7352e0e2
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/SubjectEntityIDCriterion.java
@@ -0,0 +1,79 @@
+/*
+ * Licensed 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.idp.plugin.oidc.op.oidfed.metadata;
+
+import java.util.Objects;
+
+import javax.annotation.Nonnull;
+
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.resolver.Criterion;
+
+/**
+ * A {@link Criterion} representing a subject entity ID in entity statement.
+ */
+public class SubjectEntityIDCriterion implements Criterion {
+
+ /** The entity ID value. */
+ @Nonnull @NotEmpty private final String value;
+
+ /**
+ * Constructor.
+ *
+ * @param entityId the entity ID value, must not be null
+ */
+ public SubjectEntityIDCriterion(@Nonnull final String entityId) {
+ value = Constraint.isNotEmpty(entityId, "Entity ID cannot be null");
+ }
+
+ /**
+ * Get the entity ID value.
+ *
+ * @return the entity ID value
+ */
+ public String getValue() {
+ return value;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public String toString() {
+ return "SubjectEntityIDCriterion [value=" + value + "]";
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public int hashCode() {
+ return Objects.hash(value);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ final SubjectEntityIDCriterion other = (SubjectEntityIDCriterion) obj;
+ return value.equals(other.value);
+ }
+
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/SubjectEntityStatementCriterion.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/SubjectEntityStatementCriterion.java
new file mode 100644
index 00000000..32228c30
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/SubjectEntityStatementCriterion.java
@@ -0,0 +1,79 @@
+/*
+ * Licensed 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.idp.plugin.oidc.op.oidfed.metadata;
+
+import java.util.Objects;
+
+import javax.annotation.Nonnull;
+
+import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
+
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.resolver.Criterion;
+
+/**
+ * A {@link Criterion} representing a subject entity statement.
+ */
+public class SubjectEntityStatementCriterion implements Criterion {
+
+ /** The entity statement value. */
+ @Nonnull final EntityStatement value;
+
+ /**
+ * Constructor.
+ *
+ * @param entityStatement the entity statement value, must not be null
+ */
+ public SubjectEntityStatementCriterion(@Nonnull final EntityStatement entityStatement) {
+ value = Constraint.isNotNull(entityStatement, "Entity statement cannot be null");
+ }
+
+ /**
+ * Get the entity statement value.
+ *
+ * @return the entity statement value
+ */
+ @Nonnull public EntityStatement getValue() {
+ return value;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public String toString() {
+ return "SubjectEntityStatementCriterion [value=" + value.getSignedStatement().serialize() + "]";
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public int hashCode() {
+ return Objects.hash(value);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ final SubjectEntityStatementCriterion other = (SubjectEntityStatementCriterion) obj;
+ return value.getSignedStatement().serialize().equals(other.value.getSignedStatement().serialize());
+ }
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/SubjectLocallyTrustedKeysCriterion.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/SubjectLocallyTrustedKeysCriterion.java
new file mode 100644
index 00000000..56d012e8
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/SubjectLocallyTrustedKeysCriterion.java
@@ -0,0 +1,79 @@
+/*
+ * Licensed 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.idp.plugin.oidc.op.oidfed.metadata;
+
+import java.util.Objects;
+
+import javax.annotation.Nonnull;
+
+import com.nimbusds.jose.jwk.JWKSet;
+
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.resolver.Criterion;
+
+/**
+ * A {@link Criterion} representing locally trusted keys for a subject.
+ */
+public class SubjectLocallyTrustedKeysCriterion implements Criterion {
+
+ /** The locally trusted keys. */
+ @Nonnull final JWKSet value;
+
+ /**
+ * Constructor.
+ *
+ * @param jwkSet the locally trusted key set, must not be null
+ */
+ public SubjectLocallyTrustedKeysCriterion(@Nonnull final JWKSet jwkSet) {
+ value = Constraint.isNotNull(jwkSet, "JWKSet cannot be null");
+ }
+
+ /**
+ * Get the locally trusted keys.
+ *
+ * @return the locally rusted keys
+ */
+ @Nonnull public JWKSet getValue() {
+ return value;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public String toString() {
+ return "SubjectLocallyTrustedKeysCriterion [value=" + value.toString() + "]";
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public int hashCode() {
+ return Objects.hash(value);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ final SubjectLocallyTrustedKeysCriterion other = (SubjectLocallyTrustedKeysCriterion) obj;
+ return value.toString().equals(other.value.toString());
+ }
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/TokenPayloadAsymmetricKeyTrustEngine.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/TokenPayloadAsymmetricKeyTrustEngine.java
new file mode 100644
index 00000000..3347fcaf
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/TokenPayloadAsymmetricKeyTrustEngine.java
@@ -0,0 +1,78 @@
+/*
+ * Licensed 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.idp.plugin.oidc.op.oidfed.metadata;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.security.SecurityException;
+import org.opensaml.security.credential.Credential;
+import org.slf4j.Logger;
+
+import com.nimbusds.jwt.SignedJWT;
+
+import net.shibboleth.oidc.security.credential.JOSEObjectCredentialResolver;
+import net.shibboleth.oidc.security.impl.BaseSignedJWTTrustEngine;
+import net.shibboleth.shared.annotation.ParameterName;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.resolver.CriteriaSet;
+import net.shibboleth.shared.resolver.ResolverException;
+
+public class TokenPayloadAsymmetricKeyTrustEngine extends BaseSignedJWTTrustEngine<Credential> {
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(TokenPayloadAsymmetricKeyTrustEngine.class);
+
+ /** Resolver of credentials from JOSEObject. */
+ @Nonnull private final JOSEObjectCredentialResolver joseObjectCredentialResolver;
+
+ /**
+ * Constructor.
+ *
+ * @param joseObjectResolver resolver of credentials from JOSEObject payload.
+ */
+ protected TokenPayloadAsymmetricKeyTrustEngine(
+ @Nonnull final @ParameterName(name="JOSEObjectResolver") JOSEObjectCredentialResolver joseObjectResolver) {
+ super(joseObjectResolver);
+ joseObjectCredentialResolver = joseObjectResolver;
+ }
+
+ @Override
+ /** {@inheritDoc} */
+ protected boolean doValidate(@Nonnull final SignedJWT signedJWT, @Nonnull final CriteriaSet trustBasisCriteria)
+ throws SecurityException {
+ try {
+ for (final Credential credential : joseObjectCredentialResolver.resolve(trustBasisCriteria)) {
+ assert credential != null;
+ if (verifySignature(signedJWT, credential)) {
+ log.debug("Token successfully validated with credential {}", credential.getKeyNames());
+ return true;
+ }
+ }
+ } catch (final ResolverException e) {
+ throw new SecurityException("Error resolving credentials from JOSEObject", e);
+ }
+
+ return false;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected boolean evaluateTrust(@Nonnull final Credential untrustedCredential,
+ @Nullable final Credential trustBasis) throws SecurityException {
+ throw new SecurityException("evaluateTrust-method not implemented");
+ }
+
+}
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
index 070fa00c..fc1eec64 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
@@ -888,4 +888,192 @@
<bean id="DefaultMetadataSkeleton" class="org.springframework.core.io.FileSystemResource" lazy-init="true"
c:path="%{idp.oidc.discovery.template:%{idp.home}/static/openid-configuration.json}" />
+ <bean id="shibboleth.oidfed.EntityConfigurationMetadataCache" parent="shibboleth.oidc.CacheBuilder">
+ <constructor-arg>
+ <bean p:cacheId="DefaultEntityConfigurationMetadataCache" parent="shibboleth.oidfed.EntityConfigurationMetadataCacheBuilderSpec"
+ p:cleanupTaskInterval="PT30S"/>
+ </constructor-arg>
+ </bean>
+
+ <bean id="shibboleth.oidfed.SubordinateEntityStatementMetadataCache" parent="shibboleth.oidc.CacheBuilder">
+ <constructor-arg>
+ <bean p:cacheId="DefaultSubordinateEntityStarementMetadataCache" parent="shibboleth.oidfed.SubordinateEntityStatementMetadataCacheBuilderSpec"
+ p:cleanupTaskInterval="PT30S"/>
+ </constructor-arg>
+ </bean>
+
+ <bean id="shibboleth.oidfed.TrustChainMetadataCache" parent="shibboleth.oidc.CacheBuilder">
+ <constructor-arg>
+ <bean p:cacheId="DefaultTrustChainMetadataCache" parent="shibboleth.oidfed.TrustChainMetadataCacheBuilderSpec"
+ p:cleanupTaskInterval="PT30S"/>
+ </constructor-arg>
+ </bean>
+
+ <bean id="shibboleth.oidfed.LocalTrustAnchorsMetadataCache" parent="shibboleth.oidc.CacheBuilder">
+ <constructor-arg>
+ <bean p:cacheId="DefaultLocalTrustAnchorsMetadataCache" parent="shibboleth.oidfed.LocalTrustAnchorsMetadataCacheBuilderSpec"/>
+ </constructor-arg>
+ </bean>
+
+ <bean id="shibboleth.oidfed.EntityConfigurationMetadataCacheBuilderSpec"
+ class="net.shibboleth.oidc.metadata.cache.impl.DynamicMetadataCacheBuilderSpec"
+ p:criteriaToIdentifierStrategy-ref="shibboleth.oidfed.DefaultSubjectEntityIDCriteriaToIdentifierStrategy"
+ p:metadataExpirationTimeStrategy-ref="shibboleth.oidfed.DefaultEntityStatementExpirationTimeStrategy"
+ p:minCacheDuration="%{idp.oidfed.entityConfiguration.maxRefreshDelay:PT1S}"
+ p:maxCacheDuration="%{idp.oidfed.entityConfiguration.maxRefreshDelay:PT30S}">
+ <property name="identifierExtractionStrategy">
+ <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultEntityStatementSubjectIdentifierExtractionStrategy"/>
+ </property>
+ <property name="fetchStrategy">
+ <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultEntityStatementFetchingStrategy"
+ p:httpClient="#{getObject('shibboleth.oidfed.HttpClient') ?: getObject('shibboleth.InternalHttpClient')}"
+ p:httpClientSecurityParameters="#{getObject('shibboleth.oidfed.NonBrowser.HttpClientSecurityParameters')}">
+ <property name="criteriaToEndpointStrategy">
+ <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultEntityConfigurationEndpointExtractionStrategy"
+ p:criteriaToEntityIdStrategy-ref="shibboleth.oidfed.DefaultSubjectEntityIDCriteriaToIdentifierStrategy"/>
+ </property>
+ </bean>
+ </property>
+ <property name="metadataFilterStrategy">
+ <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultEntityStatementSignatureValidationFilterStrategy">
+ <property name="trustEngine">
+ <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.TokenPayloadAsymmetricKeyTrustEngine">
+ <constructor-arg>
+ <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultEntityStatementCredentialResolver" />
+ </constructor-arg>
+ </bean>
+ </property>
+ </bean>
+ </property>
+ </bean>
+
+ <bean id="shibboleth.oidfed.DefaultSubjectEntityIDCriteriaToIdentifierStrategy"
+ class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultSubjectEntityIDCriteriaToIdentifierStrategy" />
+
+ <bean id="shibboleth.oidfed.DefaultEntityStatementExpirationTimeStrategy"
+ class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultEntityStatementExpirationTimeStrategy"/>
+
+ <bean id="shibboleth.oidfed.SubordinateEntityStatementMetadataCacheBuilderSpec"
+ class="net.shibboleth.oidc.metadata.cache.impl.DynamicMetadataCacheBuilderSpec"
+ p:metadataExpirationTimeStrategy-ref="shibboleth.oidfed.DefaultEntityStatementExpirationTimeStrategy"
+ p:minCacheDuration="%{idp.oidfed.entityConfiguration.maxRefreshDelay:PT1S}"
+ p:maxCacheDuration="%{idp.oidfed.entityConfiguration.maxRefreshDelay:PT30S}">
+ <property name="identifierExtractionStrategy">
+ <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultSubordinateStatementIdentifierExtractionStrategy"/>
+ </property>
+ <property name="criteriaToIdentifierStrategy">
+ <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultSubordinateStatementCriteriaToIdentifierStrategy"
+ p:criteriaToSubjectEntityIdStrategy-ref="shibboleth.oidfed.DefaultSubjectEntityIDCriteriaToIdentifierStrategy">
+ <property name="criteriaToIssuerEntityIdStrategy">
+ <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultIssuerEntityIDCriteriaToIdentifierStrategy"/>
+ </property>
+ </bean>
+ </property>
+ <property name="metadataFilterStrategy">
+ <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultSubordinateStatementSignatureValidationFilterStrategy"
+ p:entityConfigurationCache-ref="shibboleth.oidfed.EntityConfigurationMetadataCache">
+ <property name="trustEngine">
+ <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.TokenPayloadAsymmetricKeyTrustEngine">
+ <constructor-arg>
+ <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultEntityStatementCredentialResolver" />
+ </constructor-arg>
+ </bean>
+ </property>
+ </bean>
+ </property>
+ <property name="fetchStrategy">
+ <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultEntityStatementFetchingStrategy"
+ p:httpClient="#{getObject('shibboleth.oidfed.HttpClient') ?: getObject('shibboleth.InternalHttpClient')}"
+ p:httpClientSecurityParameters="#{getObject('shibboleth.oidfed.NonBrowser.HttpClientSecurityParameters')}">
+ <property name="criteriaToEndpointStrategy">
+ <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultSubordinateStatementEndpointExtractionStrategy"
+ p:criteriaToSubjectEntityIdStrategy-ref="shibboleth.oidfed.DefaultSubjectEntityIDCriteriaToIdentifierStrategy"
+ p:entityConfigurationCache-ref="shibboleth.oidfed.EntityConfigurationMetadataCache">
+ <property name="criteriaToIssuerEntityIdStrategy">
+ <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultIssuerEntityIDCriteriaToIdentifierStrategy"/>
+ </property>
+ </bean>
+ </property>
+ </bean>
+ </property>
+ </bean>
+
+ <bean id="shibboleth.oidfed.TrustChainMetadataCacheBuilderSpec"
+ class="net.shibboleth.oidc.metadata.cache.impl.DynamicMetadataCacheBuilderSpec"
+ p:criteriaToIdentifierStrategy-ref="shibboleth.oidfed.DefaultSubjectEntityIDCriteriaToIdentifierStrategy"
+ p:minCacheDuration="%{idp.oidfed.entityConfiguration.maxRefreshDelay:PT1S}"
+ p:maxCacheDuration="%{idp.oidfed.entityConfiguration.maxRefreshDelay:PT30S}">
+ <property name="identifierExtractionStrategy">
+ <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultTrustChainSubjectIdentifierExtractionStrategy" />
+ </property>
+ <property name="metadataExpirationTimeStrategy">
+ <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultTrustChainExpirationTimeStrategy"/>
+ </property>
+ <property name="metadataFilterStrategy">
+ <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultTrustAnchorSignatureValidationFilterStrategy">
+ <property name="entityStatementSignatureValidationFilterStrategy">
+ <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultEntityStatementSignatureValidationFilterStrategy">
+ <property name="trustEngine">
+ <bean class="net.shibboleth.oidc.security.impl.ExplicitKeySignedJWTTrustEngine">
+ <constructor-arg index="0">
+ <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultLocalTrustAnchorCredentialResolver"
+ c:cache-ref="shibboleth.oidfed.LocalTrustAnchorsMetadataCache" />
+ </constructor-arg>
+ <constructor-arg index="1">
+ <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultEntityStatementCredentialResolver" />
+ </constructor-arg>
+ </bean>
+ </property>
+ </bean>
+ </property>
+ </bean>
+ </property>
+ <property name="fetchStrategy">
+ <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultTrustChainFetchingStrategy"
+ p:criteriaToSubjectEntityIdStrategy-ref="shibboleth.oidfed.DefaultSubjectEntityIDCriteriaToIdentifierStrategy"
+ p:entityConfigurationCache-ref="shibboleth.oidfed.EntityConfigurationMetadataCache"
+ p:subordinateStatementCache-ref="shibboleth.oidfed.SubordinateEntityStatementMetadataCache"/>
+ </property>
+ </bean>
+
+ <bean id="shibboleth.oidfed.DefaultLocalTrustAnchorsFilename" class="java.lang.String" factory-method="valueOf">
+ <constructor-arg value="%{idp.oidfed.DefaultLocalTrustAnchorsFile:%{idp.home}/conf/oidfed-trust-anchors.json}" />
+ </bean>
+
+ <bean id="shibboleth.oidfed.LocalTrustAnchorsMetadataCacheBuilderSpec"
+ class="net.shibboleth.oidc.metadata.cache.impl.BatchMetadataCacheBuilderSpec"
+ p:criteriaToIdentifierStrategy-ref="shibboleth.oidfed.LocalTrustAnchorsDefaultIdentifierStrategy"
+ p:identifierExtractionStrategy-ref="shibboleth.oidfed.LocalTrustAnchorsDefaultIdentifierStrategy"
+ p:metadataValidPredicate="#{getObject('shibboleth.oidfed.LocalTrustAnchorsValidator') ?: getObject('shibboleth.oidfed.DefaultLocalTrustAnchorsValidator')}">
+ <property name="loadingStrategy">
+ <bean class="net.shibboleth.oidc.metadata.cache.impl.DefaultResourceLoadingStrategy">
+ <constructor-arg name="metadata">
+ <bean class="org.springframework.core.io.Resource"
+ factory-bean="PreferFileSystemResourceLoader" factory-method="getResource">
+ <constructor-arg>
+ <ref bean="shibboleth.oidfed.DefaultLocalTrustAnchorsFilename" />
+ </constructor-arg>
+ </bean>
+ </constructor-arg>
+ </bean>
+ </property>
+ <property name="parsingStrategy">
+ <bean class="net.shibboleth.oidc.metadata.cache.impl.DefaultJSONMapParsingStrategy"
+ c:valueClass="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.LocalKeyContainer"/>
+ </property>
+ <property name="sourceMetadataExpiryStrategy">
+ <bean class="net.shibboleth.oidc.metadata.cache.impl.DefaultSourceMetadataExpirationTimeStrategy"
+ c:duration="PT10M"/>
+ </property>
+ </bean>
+
+ <bean id="PreferFileSystemResourceLoader"
+ class="net.shibboleth.shared.spring.resource.PreferFileSystemResourceLoader"/>
+
+ <bean id="shibboleth.oidfed.DefaultLocalTrustAnchorsValidator"
+ class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultLocalTrustAnchorsValidator"/>
+
+ <bean id="shibboleth.oidfed.LocalTrustAnchorsDefaultIdentifierStrategy"
+ parent="shibboleth.Functions.Constant" c:target="#{getObject('shibboleth.oidfed.DefaultLocalTrustAnchorsFilename') ?: 'undefined'}" />
+
</beans>
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list