[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