[java-idp-plugin-oidc-op-oidfed] 01/02: Initial (still incomplete) implementation for resolve entity API client
Henri Mikkonen
henri.mikkonen at iki.fi
Fri Sep 26 14:16:33 UTC 2025
This is an automated email from the git hooks/post-receive script.
hjmikkon pushed a commit to branch main
in repository java-idp-plugin-oidc-op-oidfed.
View the commit online:
http://git.shibboleth.net/view/?p=java-idp-plugin-oidc-op-oidfed.git;a=commit;h=a1c9c7f57b9a9b15e30007aef7a5e95f12f2db10
commit a1c9c7f57b9a9b15e30007aef7a5e95f12f2db10
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Fri Sep 26 17:04:37 2025 +0300
Initial (still incomplete) implementation for resolve entity API client
- Currently integrated only to the automatic registration process
- idp.oidfed.trustchain.resolver.useResolverApiCondition can be used for wiring custom condition for activating the resolution via API (default false)
- idp.oidfed.trustchain.resolver.fallbackToLocalCondition can be used for wiring custom condition for fallbacking to local resolution (default true)
- shibboleth.oidfed.ResolveEntityTrustChainMetadataCache is used for caching responses
- conf/oidfed/oidfed-trustchain-resolver.xml contains a configurable map of trusted entities for resolution via API
- shibboleth.oidfed.DefaultTrustedEntitiesLookupStrategy
- map keyed with entity IDs, value list of trust_anchor parameters used in the API request message
- shibboleth.oidfed.TrustedEntitiesLookupStrategy can be used to wire custom Function<ProfileRequestContext, Map<String,List<String>>
---
...ntityStatementSignatureValidationComponent.java | 47 ++
...actTrustEngineSignatureValidationComponent.java | 14 +-
.../DefaultEntityStatementFetchingStrategy.java | 4 +-
...StatementSignatureValidationFilterStrategy.java | 2 +-
...efaultProvidedTrustChainValidationStrategy.java | 2 +-
...yResponseSignatureValidationFilterStrategy.java | 128 +++++
...ultResolveEntityTrustChainFetchingStrategy.java | 218 +++++++++
.../oidfed/profile/impl/CallResolveEntityApi.java | 515 +++++++++++++++++++++
...eAutomaticRegistrationProfileConfiguration.java | 16 +-
.../DefaultLocalMetadataPolicyMergingStrategy.java | 3 +-
.../META-INF/net.shibboleth.idp/postconfig.xml | 51 +-
.../oidfed/metadata-lookup-ext-oidfed-beans.xml | 19 +
.../oidfed/metadata-lookup-ext-oidfed-flow.xml | 29 +-
.../idp/service/relying-party/postconfig.xml | 22 +-
.../conf/oidfed/oidfed-trustchain-resolver.xml | 40 ++
.../idp/plugin/oidc/op/oidfed/module.properties | 4 +
.../flow/oidfed/AbstractFederationFlowTest.java | 51 +-
.../AuthorizeFlowAutomaticRegistrationTest.java | 98 ++++
.../net/shibboleth/idp/module/conf/global.xml | 8 +
.../net/shibboleth/idp/module/conf/oidc.properties | 4 +-
.../conf/oidfed/oidfed-trustchain-resolver.xml | 24 +
21 files changed, 1268 insertions(+), 31 deletions(-)
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/AbstractEntityStatementSignatureValidationComponent.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/AbstractEntityStatementSignatureValidationComponent.java
new file mode 100644
index 0000000..2c48bf2
--- /dev/null
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/AbstractEntityStatementSignatureValidationComponent.java
@@ -0,0 +1,47 @@
+/*
+ * 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.trust.TrustEngine;
+
+import com.nimbusds.jwt.SignedJWT;
+import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
+
+import net.shibboleth.shared.resolver.CriteriaSet;
+
+/**
+ * Abstract component performing entity statement signature validation via {@link TrustEngine}.
+ */
+public class AbstractEntityStatementSignatureValidationComponent
+ extends AbstractTrustEngineSignatureValidationComponent {
+
+ /**
+ * Validates the given entity statement via trust engine and the given criteria.
+ *
+ * @param entityStatement the entity statement to be validated
+ * @param criteria the criteria (expanded with the optional default criteria)
+ * @param entityId the entity ID used for logging
+ * @return true if validation was successful, false otherwise
+ */
+ protected boolean validateStatement(@Nonnull final EntityStatement entityStatement,
+ @Nonnull final CriteriaSet criteria, @Nullable final String entityId) {
+ final SignedJWT jwt = entityStatement.getSignedStatement();
+ assert jwt != null;
+ return validateJwt(jwt, criteria, entityId);
+ }
+}
\ No newline at end of file
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/AbstractTrustEngineSignatureValidationComponent.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/AbstractTrustEngineSignatureValidationComponent.java
index 45c847c..73ec509 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/AbstractTrustEngineSignatureValidationComponent.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/AbstractTrustEngineSignatureValidationComponent.java
@@ -22,7 +22,6 @@ 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.shared.annotation.constraint.NonnullAfterInit;
import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
@@ -75,21 +74,19 @@ public class AbstractTrustEngineSignatureValidationComponent extends AbstractIde
}
/**
- * Validates the given entity statement via trust engine and the given criteria.
+ * Validates the given JWT via trust engine and the given criteria.
*
- * @param entityStatement the entity statement to be validated
+ * @param jwt the JWT to be validated
* @param criteria the criteria (expanded with the optional default criteria)
* @param entityId the entity ID used for logging
* @return true if validation was successful, false otherwise
*/
- protected boolean validateStatement(@Nonnull final EntityStatement entityStatement,
- @Nonnull final CriteriaSet criteria, @Nullable final String entityId) {
+ protected boolean validateJwt(@Nonnull final SignedJWT jwt, @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;
@@ -100,5 +97,4 @@ public class AbstractTrustEngineSignatureValidationComponent extends AbstractIde
log.warn("Trust Engine validation failed for {}", entityId);
return false;
}
-
-}
+}
\ No newline at end of file
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultEntityStatementFetchingStrategy.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultEntityStatementFetchingStrategy.java
index acf0579..f508d1f 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultEntityStatementFetchingStrategy.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultEntityStatementFetchingStrategy.java
@@ -138,11 +138,11 @@ public class DefaultEntityStatementFetchingStrategy extends AbstractIdentifiable
final String scheme = httpRequest.getUri().getScheme();
assert scheme != null;
HttpClientSecuritySupport.checkTLSCredentialEvaluated(httpContext, scheme);
- if (response.getCode() == HttpStatus.SC_OK) {
+ if (response != null && response.getCode() == HttpStatus.SC_OK) {
return EntityStatementHelper.deserializeEntityStatement(EntityUtils.toString(response.getEntity()));
} else {
log.debug("Unable to fetch entity configuration from URI: {} (HTTP status {})", uri,
- response.getCode());
+ response == null ? null : response.getCode());
return null;
}
} catch (final ParseException | URISyntaxException | IOException e) {
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultEntityStatementSignatureValidationFilterStrategy.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultEntityStatementSignatureValidationFilterStrategy.java
index a3a06e4..e4952b2 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultEntityStatementSignatureValidationFilterStrategy.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultEntityStatementSignatureValidationFilterStrategy.java
@@ -35,7 +35,7 @@ import net.shibboleth.shared.resolver.CriteriaSet;
*/
@ThreadSafeAfterInit
public class DefaultEntityStatementSignatureValidationFilterStrategy
- extends AbstractTrustEngineSignatureValidationComponent
+ extends AbstractEntityStatementSignatureValidationComponent
implements BiFunction<EntityStatement, MetadataFilterContext, EntityStatement> {
/** Class logger. */
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultProvidedTrustChainValidationStrategy.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultProvidedTrustChainValidationStrategy.java
index d6bbcd7..cd48e81 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultProvidedTrustChainValidationStrategy.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultProvidedTrustChainValidationStrategy.java
@@ -43,7 +43,7 @@ import net.shibboleth.shared.resolver.CriteriaSet;
* trust anchor signature validation filter.
*/
public class DefaultProvidedTrustChainValidationStrategy
- extends AbstractTrustEngineSignatureValidationComponent
+ extends AbstractEntityStatementSignatureValidationComponent
implements BiPredicate<ProfileRequestContext, List<EntityStatement>> {
/** Class logger. */
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultResolveEntityResponseSignatureValidationFilterStrategy.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultResolveEntityResponseSignatureValidationFilterStrategy.java
new file mode 100644
index 0000000..7284e9c
--- /dev/null
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultResolveEntityResponseSignatureValidationFilterStrategy.java
@@ -0,0 +1,128 @@
+/*
+ * 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.List;
+import java.util.function.BiFunction;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+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.idp.plugin.oidc.op.oidfed.messaging.impl.ResolveEntityResponse;
+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 resolve entity response. The signature validation is performed via
+ * configurable {@link TrustEngine}.
+ */
+ at ThreadSafeAfterInit
+public class DefaultResolveEntityResponseSignatureValidationFilterStrategy
+ extends AbstractTrustEngineSignatureValidationComponent
+ implements BiFunction<ResolveEntityResponseContainer, MetadataFilterContext, ResolveEntityResponseContainer> {
+
+ /** Class logger. */
+ @Nonnull private Logger log =
+ LoggerFactory.getLogger(DefaultResolveEntityResponseSignatureValidationFilterStrategy.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 ResolveEntityResponseContainer apply(@Nullable final ResolveEntityResponseContainer response,
+ @Nullable final MetadataFilterContext filterContext) {
+ checkComponentActive();
+ if (response == null) {
+ return null;
+ }
+ if (response.getResponse() instanceof ResolveEntityResponse successResponse) {
+ final SignedJWT jwt = successResponse.getJWT();
+ final String entityId;
+ try {
+ entityId = jwt.getJWTClaimsSet().getIssuer();
+ } catch (final ParseException e) {
+ log.error("Could not parse issuer entity ID value from the response", e);
+ return null;
+ }
+ final EntityStatement issuerStatement = fetchIssuerStatement(entityId);
+ if (issuerStatement == null) {
+ return null;
+ }
+ log.trace("Starting signature validation of success response from {}", entityId);
+ final CriteriaSet criteria = new CriteriaSet(new SubjectEntityStatementCriterion(issuerStatement));
+ if (validateJwt(jwt, criteria, entityId)) {
+ return response;
+ }
+ } else {
+ log.trace("Ignoring signagure validation for the error response");
+ return response;
+ }
+ 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-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultResolveEntityTrustChainFetchingStrategy.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultResolveEntityTrustChainFetchingStrategy.java
new file mode 100644
index 0000000..03bfa2d
--- /dev/null
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultResolveEntityTrustChainFetchingStrategy.java
@@ -0,0 +1,218 @@
+/*
+ * 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.time.Instant;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+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.ContentType;
+import org.apache.hc.core5.http.HttpHeaders;
+import org.apache.hc.core5.http.HttpStatus;
+import org.apache.hc.core5.http.NameValuePair;
+import org.apache.hc.core5.http.ParseException;
+import org.apache.hc.core5.http.io.entity.EntityUtils;
+import org.apache.hc.core5.http.message.BasicNameValuePair;
+import org.apache.hc.core5.net.URIBuilder;
+import org.opensaml.security.httpclient.HttpClientSecurityParameters;
+import org.opensaml.security.httpclient.HttpClientSecuritySupport;
+import org.slf4j.Logger;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.nimbusds.jwt.SignedJWT;
+
+import net.shibboleth.idp.plugin.oidc.op.oidfed.messaging.impl.ResolveEntityRequest;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.messaging.impl.ResolveEntityResponse;
+import net.shibboleth.oidc.metadata.cache.MetadataCache;
+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 trust chains via resolve entity API for an entity specified in the criteria set.
+ * Caches for entity configurations and subordinate statements are exploited for actual fetching of the entity
+ * statements. The entity configuration may also be delivered via {@link SubjectEntityStatementCriterion} in the
+ * criteria set.
+ */
+ at ThreadSafeAfterInit
+public class DefaultResolveEntityTrustChainFetchingStrategy extends AbstractIdentifiableInitializableComponent
+ implements Function<CriteriaSet, ResolveEntityResponseContainer> {
+
+ /** Class logger. */
+ @Nonnull private Logger log = LoggerFactory.getLogger(DefaultResolveEntityTrustChainFetchingStrategy.class);
+
+ /** Cache containing local copies of trusted trust anchor keys. */
+ @NonnullAfterInit private MetadataCache<Map<String, LocalKeyContainer>> localTrustAnchorsCache;
+
+ /** HTTP client to use. */
+ @NonnullAfterInit protected HttpClient httpClient;
+
+ /** HTTP client security parameters. */
+ @Nullable private HttpClientSecurityParameters httpClientSecurityParameters;
+
+ /** JSON object mapper used for decoding JSON into Map. */
+ @NonnullAfterInit private ObjectMapper objectMapper;
+
+ /**
+ * Set the cache containing local copies of trusted trust anchor keys.
+ *
+ * @param cache cache containing local copies of trusted trust anchor keys.
+ */
+ public void setLocalTrustAnchorsCache(@Nonnull final MetadataCache<Map<String, LocalKeyContainer>> cache) {
+ checkSetterPreconditions();
+ localTrustAnchorsCache = Constraint.isNotNull(cache, "Local Trust Anchor cache cannot be null");
+ }
+
+ /**
+ * 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 JSON {@link ObjectMapper} used for decoding JSON into Map.
+ *
+ * @param mapper object mapper
+ */
+ public void setObjectMapper(@Nonnull final ObjectMapper mapper) {
+ checkSetterPreconditions();
+
+ objectMapper = Constraint.isNotNull(mapper, "Object mapper cannot be null");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+ if (localTrustAnchorsCache == null) {
+ throw new ComponentInitializationException("Local Trust Anchor cache cannot be null");
+ }
+ if (httpClient == null) {
+ throw new ComponentInitializationException("Httpclient cannot be null");
+ }
+ if (objectMapper == null) {
+ throw new ComponentInitializationException("Object mapper cannot be null");
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override @Nullable
+ public ResolveEntityResponseContainer apply(@Nullable final CriteriaSet criteria) {
+ checkComponentActive();
+ if (criteria == null) {
+ return null;
+ }
+
+ final ResolveEntityRequestCriterion requestCriterion = criteria.get(ResolveEntityRequestCriterion.class);
+ if (requestCriterion == null) {
+ log.debug("No request criterion given, returning null");
+ return null;
+ }
+ final ResponseContainerExpirationCriterion expirationCriterion =
+ criteria.get(ResponseContainerExpirationCriterion.class);
+ if (expirationCriterion == null) {
+ log.debug("No expiration criterion given, returning null");
+ return null;
+ }
+ final ResolveEntityRequest resolveRequest = requestCriterion.getRequest();
+ final HttpGet httpRequest = new HttpGet(resolveRequest.getEndpointURI());
+ final List<NameValuePair> nvps = new ArrayList<>();
+ nvps.add(new BasicNameValuePair("sub", resolveRequest.getSubject()));
+ resolveRequest.getTrustAnchors()
+ .forEach(trustAnchor -> nvps.add(new BasicNameValuePair("trust_anchor", trustAnchor)));
+ resolveRequest.getEntityTypes()
+ .forEach(entityType -> nvps.add(new BasicNameValuePair("entity_type", entityType)));
+ final URI uri;
+ try {
+ uri = new URIBuilder(httpRequest.getUri()).addParameters(nvps).build();
+ } catch (final URISyntaxException e) {
+ log.error("Could not build endpoint URI for the resolve entity request", e);
+ return null;
+ }
+ log.debug("Using URI {} for fetching resolve entity API response", uri);
+ httpRequest.setHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_FORM_URLENCODED);
+ httpRequest.setUri(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 != null && response.getCode() == HttpStatus.SC_OK) {
+ final SignedJWT responseJwt = SignedJWT.parse(EntityUtils.toString(response.getEntity()));
+ final ResolveEntityResponse resolveResponse = new ResolveEntityResponse(responseJwt);
+ final Instant expirationTime = expirationCriterion.getExpirationInstant();
+ return new ResolveEntityResponseContainer(resolveResponse, resolveRequest, expirationTime);
+ } else {
+ log.debug("Unable to fetch resolve entity response from URI: {} (HTTP status {})", uri,
+ response == null ? null : response.getCode());
+ //TODO: cache non-success results?
+ return null;
+ }
+ } catch (final ParseException | URISyntaxException | IOException | java.text.ParseException e) {
+ log.debug("Unable to fetch resolve entity response from URI: {}", uri, e);
+ }
+
+ return 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;
+ }
+
+}
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/CallResolveEntityApi.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/CallResolveEntityApi.java
new file mode 100644
index 0000000..132fe9a
--- /dev/null
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/CallResolveEntityApi.java
@@ -0,0 +1,515 @@
+/*
+ * 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.profile.impl;
+
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.text.ParseException;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
+import org.slf4j.Logger;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.nimbusds.jwt.SignedJWT;
+import com.nimbusds.oauth2.sdk.id.ClientID;
+import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
+
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.messaging.impl.ResolveEntityRequest;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.messaging.impl.ResolveEntityResponse;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.EntityStatementHelper;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.ResolveEntityRequestCriterion;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.ResolveEntityResponseContainer;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.ResponseContainerExpirationCriterion;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.SubjectEntityIDCriterion;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.SubjectEntityStatementCriterion;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.navigate.DefaultPreSelectedTrustChainIDsLookupStrategy;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.navigate.DefaultTrustChainIDsLookupStrategy;
+import net.shibboleth.idp.profile.AbstractProfileAction;
+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.NonnullBeforeExec;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.collection.Pair;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.logic.FunctionSupport;
+import net.shibboleth.shared.logic.PredicateSupport;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.resolver.CriteriaSet;
+
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.action.ActionSupport;
+
+/**
+ * Resolves trust chain, policy-enforced metadata and trust marks via configurable
+ * {@link #resolveEntityTrustChainMetadataCache}. The data is populated to the {@link RelyingPartyTrustChainContext}.
+ *
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#INVALID_MSG_CTX}
+ */
+public class CallResolveEntityApi extends AbstractProfileAction {
+
+ /** Class logger. */
+ @Nonnull private Logger log = LoggerFactory.getLogger(CallResolveEntityApi.class);
+
+ /** Strategy used to create the trust chain context. */
+ @Nonnull private Function<ProfileRequestContext, RelyingPartyTrustChainContext> trustChainContextCreationStrategy;
+
+ /** Cache used to fetch the issuer entity configuration from. */
+ @NonnullAfterInit private MetadataCache<EntityStatement> entityConfigurationCache;
+
+ /** Cache containing responses from resolve entity APIs. */
+ @NonnullAfterInit private MetadataCache<ResolveEntityResponseContainer> resolveEntityTrustChainMetadataCache;
+
+ /** Strategy used to obtain the client id value of the request. */
+ @NonnullAfterInit private Function<MessageContext, ClientID> clientIDLookupStrategy;
+
+ /** Strategy used to fetch the pre-selected trust chain entity IDs. */
+ @Nonnull private Function<ProfileRequestContext, List<String>> preSelectedTrustChainIdsLookupStrategy;
+
+ /** Strategy used to get entity IDs from a trust chain. */
+ @Nonnull private Function<List<EntityStatement>, List<String>> trustChainIDsLookupStrategy;
+
+ /** Strategy used to fetch entity configuration delivered to the trust chain cache. */
+ @Nonnull private Function<ProfileRequestContext, EntityStatement> entityConfigurationLookupStrategy;
+
+ /** Condition to require entity configuration via {@link #entityConfigurationLookupStrategy}. */
+ @Nonnull private Predicate<ProfileRequestContext> requireEntityConfigurationCondition;
+
+ /** Strategy used to get map of trusted entities for resolve entity APIs. */
+ @NonnullAfterInit private Function<ProfileRequestContext, Map<String,List<String>>> trustedEntitiesLookupStrategy;
+
+ /** Strategy used to fetch the entity types used in the resolve entity request. */
+ @Nonnull private Function<ProfileRequestContext, List<String>> entityTypesLookupStrategy;
+
+ /** Lookup function to supply cached response lifetime. */
+ @Nonnull private Function<ProfileRequestContext,Duration> cachedResponseLifetimeLookupStrategy;
+
+ /** JSON object mapper used for decoding JSON into Map. */
+ @NonnullAfterInit private ObjectMapper objectMapper;
+
+ /** OAuth2 client id. */
+ @NonnullBeforeExec private String clientId;
+
+ /** Trusted entities to use. */
+ @NonnullBeforeExec private Map<String,List<String>> trustedEntities;
+
+ /**
+ * Constructor.
+ */
+ public CallResolveEntityApi() {
+ final Function<ProfileRequestContext, RelyingPartyTrustChainContext> tccs =
+ new ChildContextLookup<>(RelyingPartyTrustChainContext.class, true).compose(
+ new InboundMessageContextLookup());
+ assert tccs != null;
+ trustChainContextCreationStrategy = tccs;
+ preSelectedTrustChainIdsLookupStrategy = new DefaultPreSelectedTrustChainIDsLookupStrategy();
+ trustChainIDsLookupStrategy = new DefaultTrustChainIDsLookupStrategy();
+ entityConfigurationLookupStrategy = FunctionSupport.constant(null);
+ requireEntityConfigurationCondition = PredicateSupport.alwaysFalse();
+ entityTypesLookupStrategy = FunctionSupport.constant(List.of("openid_relying_party"));
+ cachedResponseLifetimeLookupStrategy = FunctionSupport.constant(Duration.ofMinutes(5));
+ }
+
+ /**
+ * Set the strategy used to create the trust chain context.
+ *
+ * @param strategy creation strategy
+ */
+ public void setTrustChainContextCreationStrategy(
+ @Nonnull final Function<ProfileRequestContext, RelyingPartyTrustChainContext> strategy) {
+ checkSetterPreconditions();
+ trustChainContextCreationStrategy =
+ Constraint.isNotNull(strategy, "TrustChainContextCreationStrategy cannot be null");
+ }
+
+ /**
+ * 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");
+ }
+
+ public void setResolveEntityTrustChainMetadataCache(
+ @Nonnull final MetadataCache<ResolveEntityResponseContainer> cache) {
+ checkSetterPreconditions();
+ resolveEntityTrustChainMetadataCache =
+ Constraint.isNotNull(cache, "ResolveEntityTrustChainMetadataCache cannot be null");
+ }
+
+ /**
+ * Set the strategy used to locate the client id of the request.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setClientIDLookupStrategy(@Nonnull final Function<MessageContext, ClientID> strategy) {
+ checkSetterPreconditions();
+ clientIDLookupStrategy =
+ Constraint.isNotNull(strategy, "ClientIDLookupStrategy cannot be null");
+ }
+
+ /**
+ * Set the strategy used to fetch the pre-selected trust chain entity IDs.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setPreSelectedTrustChainIdsLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext, List<String>> strategy) {
+ checkSetterPreconditions();
+ preSelectedTrustChainIdsLookupStrategy = Constraint.isNotNull(strategy,
+ "PreSelectedTrustChainIdsLookupStrategy cannot be null");
+ }
+
+ /**
+ * Set the strategy used to get entity IDs from a trust chain.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setTrustChainIDsLookupStrategy(@Nonnull final Function<List<EntityStatement>, List<String>> strategy) {
+ checkSetterPreconditions();
+ trustChainIDsLookupStrategy = Constraint.isNotNull(strategy, "TrustChainIDsLookupStrategy cannot be null");
+ }
+
+ /**
+ * Set the strategy used to fetch entity configuration delivered to the trust chain cache.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setEntityConfigurationLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext, EntityStatement> strategy) {
+ checkSetterPreconditions();
+ entityConfigurationLookupStrategy = Constraint.isNotNull(strategy,
+ "EntityConfigurationLookupStrategy cannot be null");
+ }
+
+ /**
+ * Set the condition to require entity configuration via {@link #entityConfigurationLookupStrategy}.
+ * @param predicate condition
+ */
+ public void setRequireEntityConfigurationCondition(@Nonnull final Predicate<ProfileRequestContext> predicate) {
+ checkSetterPreconditions();
+ requireEntityConfigurationCondition =
+ Constraint.isNotNull(predicate, "RequireEntityConfigurationCondition cannot be null");
+ }
+
+ /**
+ * Set the strategy used to get map of trusted entities for resolve entity APIs.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setTrustedEntitiesLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext,Map<String,List<String>>> strategy) {
+ checkSetterPreconditions();
+ trustedEntitiesLookupStrategy = Constraint.isNotNull(strategy, "TrustedEntitiesLookupStrategy cannot be null");
+ }
+
+ /**
+ * Set the strategy used to fetch the entity types used in the resolve entity request.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setEntityTypesLookupStrategy(@Nonnull final Function<ProfileRequestContext, List<String>> strategy) {
+ checkSetterPreconditions();
+ entityTypesLookupStrategy = Constraint.isNotNull(strategy, "EntityTypesLookupStrategy cannot be null");
+ }
+
+ /**
+ * Set the JSON {@link ObjectMapper} used for decoding JSON into Map.
+ *
+ * @param mapper object mapper
+ */
+ public void setObjectMapper(@Nonnull final ObjectMapper mapper) {
+ checkSetterPreconditions();
+
+ objectMapper = Constraint.isNotNull(mapper, "Object mapper cannot be null");
+ }
+
+ /**
+ * Set a lookup strategy for the cached response lifetime.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setCachedResponseLifetimeLookupStrategy(
+ @Nullable final Function<ProfileRequestContext,Duration> strategy) {
+ cachedResponseLifetimeLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+
+ if (entityConfigurationCache == null) {
+ throw new ComponentInitializationException("Entity Configuration cache cannot be null");
+ }
+ if (resolveEntityTrustChainMetadataCache == null) {
+ throw new ComponentInitializationException("ResolveEntityTrustChainMetadataCache cannot be null");
+ }
+ if (clientIDLookupStrategy == null) {
+ throw new ComponentInitializationException("ClientIDLookupStrategy cannot be null");
+ }
+ if (trustedEntitiesLookupStrategy == null) {
+ throw new ComponentInitializationException("TrustedEntitiesLookupStrategy cannot be null");
+ }
+ if (objectMapper == null) {
+ throw new ComponentInitializationException("ObjectMapper cannot be null");
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+ if (!super.doPreExecute(profileRequestContext)) {
+ return false;
+ }
+
+ final ClientID id = clientIDLookupStrategy.apply(profileRequestContext.getInboundMessageContext());
+ if (id == null) {
+ log.error("{} Unable to obtain client ID", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
+ return false;
+ }
+ clientId = id.getValue();
+ try {
+ new URL(clientId).toURI();
+ } catch (final URISyntaxException | MalformedURLException e) {
+ log.debug("{} The client ID {} is not a valid URL, nothing to do", getLogPrefix(), clientId);
+ return false;
+ }
+
+ trustedEntities = trustedEntitiesLookupStrategy.apply(profileRequestContext);
+ if (trustedEntities == null || trustedEntities.isEmpty()) {
+ log.warn("{} No trusted entities resolved for {} nothing to do", getLogPrefix(), clientId);
+ return false;
+ }
+
+ return true;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+ log.debug("{} Resolving trust chain via resolve entity API for {}", getLogPrefix(), clientId);
+ assert clientId != null;
+ final CriteriaSet criteriaSet = new CriteriaSet(new SubjectEntityIDCriterion(clientId));
+ final EntityStatement entityConfiguration = entityConfigurationLookupStrategy.apply(profileRequestContext);
+ if (entityConfiguration != null) {
+ log.debug("{} Entity configuration resolved and included to the criteria set", getLogPrefix());
+ criteriaSet.add(new SubjectEntityStatementCriterion(entityConfiguration));
+ } else if (requireEntityConfigurationCondition.test(profileRequestContext)) {
+ log.error("{} Mandatory entity configuration could not be resolved", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_CREDENTIALS);
+ return;
+ }
+ final Duration cachedLifetime = cachedResponseLifetimeLookupStrategy.apply(profileRequestContext);
+ if (cachedLifetime == null) {
+ log.warn("{} Could not resolve lifetime for success responses", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return;
+ }
+ criteriaSet.add(new ResponseContainerExpirationCriterion(Instant.now().plus(cachedLifetime)));
+
+ final List<String> preSelectedChain =
+ Optional.ofNullable(preSelectedTrustChainIdsLookupStrategy.apply(profileRequestContext))
+ .orElse(CollectionSupport.emptyList());
+
+ for (final String trustedEntity : trustedEntities.keySet()) {
+ final List<String> trustAnchors = trustedEntities.get(trustedEntity);
+ if (trustAnchors == null || trustAnchors.isEmpty()) {
+ log.warn("{} No trust anchors defined for trusted entity {}", getLogPrefix(), trustedEntity);
+ continue;
+ }
+ final URI uri = fetchResolveEntityEndpoint(trustedEntity);
+ if (uri == null) {
+ log.warn("{} Could not fetch federation resolve endpoint for {}", getLogPrefix(), trustedEntity);
+ continue;
+ }
+ final ResolveEntityRequest entityRequest = new ResolveEntityRequest(
+ uri, clientId, trustAnchors, List.of("openid_relying_party"));
+ criteriaSet.add(new ResolveEntityRequestCriterion(entityRequest));
+ final List<ResolveEntityResponseContainer> cacheResult;
+ try {
+ cacheResult = resolveEntityTrustChainMetadataCache.get(criteriaSet);
+ } catch (final MetadataCacheException e) {
+ log.warn("{} Could not resolve entity for {}", getLogPrefix(), clientId, e);
+ continue;
+ }
+ if (cacheResult.isEmpty()) {
+ log.debug("{} No data resolved for {}", getLogPrefix(), clientId);
+ continue;
+ }
+ if (cacheResult.get(0).getResponse() instanceof ResolveEntityResponse successResponse) {
+ final List<String> rawTrustChain;
+ final Map<String, Object> rawMetadata;
+ final List<Object> rawTrustMarks;
+ try {
+ rawTrustChain = successResponse.getJWT().getJWTClaimsSet().getStringListClaim("trust_chain");
+ rawMetadata = successResponse.getJWT().getJWTClaimsSet().getJSONObjectClaim("metadata");
+ rawTrustMarks = successResponse.getJWT().getJWTClaimsSet().getListClaim("trust_marks");
+ } catch (final ParseException e) {
+ log.error("{} Could not parse resolve entity response contents", getLogPrefix(), e);
+ continue;
+ }
+ if (rawTrustChain == null || rawTrustChain.isEmpty() || rawMetadata == null || rawMetadata.isEmpty()) {
+ log.warn("{} Could not parse mandatory parameters from the response", getLogPrefix());
+ continue;
+ }
+ final List<EntityStatement> chain = rawTrustChain.stream()
+ .filter(Objects::nonNull)
+ .map(entry -> EntityStatementHelper.deserializeEntityStatement(entry))
+ .toList();
+ if (!preSelectedChain.isEmpty() && !preSelectedChain.equals(trustChainIDsLookupStrategy.apply(chain))) {
+ log.debug("{} Ignored resolved trust chain that doesn't match with preselected chain", getLogPrefix());
+ continue;
+ }
+ final Map<String, Map<String, Object>> metadata = rawMetadata.entrySet().stream()
+ .filter(entry -> entry.getKey() instanceof String && entry.getValue() instanceof Map<?,?>)
+ .collect(Collectors.toMap(entry -> (String) entry.getKey(),
+ entry -> ((Map<?,?>)entry.getValue()).entrySet().stream()
+ .filter(e -> e.getKey() instanceof String)
+ .collect(Collectors.toMap(e -> (String) e.getKey(), e -> e.getValue()))));
+
+ final RelyingPartyTrustChainContext trustChainContext =
+ trustChainContextCreationStrategy.apply(profileRequestContext);
+ final List<Pair<List<EntityStatement>, Map<String,Map<String,Object>>>> policyCompliantChains =
+ new ArrayList<>();
+ policyCompliantChains.add(new Pair<>(chain, metadata));
+ trustChainContext.setPolicyCompliantTrustChains(policyCompliantChains);
+ log.debug("{} Populated policy compliant trust chains with {}", getLogPrefix(), policyCompliantChains);
+
+ final List<SignedJWT> trustMarks = rawTrustMarks == null ? null : rawTrustMarks
+ .stream()
+ .filter(Map.class::isInstance)
+ .map(Map.class::cast)
+ .map(map -> parseTrustMark(map.get("trust_mark")))
+ .filter(Objects::nonNull)
+ .toList();
+ if (trustMarks != null) {
+ final Map<String, List<SignedJWT>> trustMarksByEntity = new HashMap<>();
+ for (final String entity : chain.stream().map(entity -> entity.getEntityID().getValue()).toList()) {
+ final List<SignedJWT> trustMarksForEntity = trustMarks.stream()
+ .filter(jwt -> {
+ try {
+ return entity.equals(jwt.getJWTClaimsSet().getSubject());
+ } catch (final ParseException e1) {
+ return false;
+ }
+ }).toList();
+ if (!trustMarksForEntity.isEmpty()) {
+ trustMarksByEntity.put(entity, trustMarksForEntity);
+ }
+ }
+ if (!trustMarksByEntity.isEmpty()) {
+ trustChainContext.setVerifiedTrustMarks(trustMarksByEntity);
+ final Map<String, List<String>> trustMarkIds = trustMarksByEntity.entrySet().stream()
+ .collect(Collectors.toMap(entry -> entry.getKey(), entry -> entry.getValue().stream()
+ .map(jwt -> {
+ try {
+ return jwt.getJWTClaimsSet().getStringClaim("trust_mark_id");
+ } catch (final ParseException e1) {
+ return null;
+ }
+ })
+ .filter(Objects::nonNull)
+ .toList()));
+ log.debug("{} The following trust marks are included: {}", getLogPrefix(), trustMarkIds);
+ trustChainContext.setVerifiedTrustMarkIds(trustMarkIds);
+
+ }
+ } else {
+ log.debug("{} No trust marks included in the response", getLogPrefix());
+ }
+ return;
+ } else {
+ log.debug("{} The response was not a success response: {}", getLogPrefix(),
+ cacheResult.get(0).getResponse());
+ continue;
+ }
+ }
+ log.debug("{} No previously rejected policy-compliant trust chains resolved", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, "CheckFallback");
+
+ }
+
+ /**
+ * Fetch the resolve entity endpoint via entity configuration from the metadata cache.
+ *
+ * @param subject the entity ID
+ * @return the resolve entity endpoint, or null if could not be fetched
+ */
+ @Nullable protected URI fetchResolveEntityEndpoint(@Nonnull final String subject) {
+ final CriteriaSet criteria = new CriteriaSet(new SubjectEntityIDCriterion(subject));
+ try {
+ final List<EntityStatement> result = entityConfigurationCache.get(criteria);
+ if (!result.isEmpty()) {
+ return Optional.ofNullable(
+ EntityStatementHelper.parseMetadata(objectMapper, result.get(0)).get("federation_entity"))
+ .filter(Map.class::isInstance)
+ .map(map -> (Map<String,Object>) map)
+ .map(map -> map.get("federation_resolve_endpoint"))
+ .filter(String.class::isInstance)
+ .map(String.class::cast)
+ .map(URI::create)
+ .orElse(null);
+ }
+ } catch (final MetadataCacheException e) {
+ log.debug("Error while fetching entity configuration for {}", subject, e);
+ }
+ log.warn("Could not fetch entity configuration for {}", subject);
+ return null;
+ }
+
+ /**
+ * Parses the trust mark.
+ *
+ * @param trustMark trust mark to be verified, expected to be parseable from string
+ * @return trust mark JWT if valid, null otherwise
+ */
+ @Nullable private SignedJWT parseTrustMark(@Nullable final Object trustMark) {
+ if (trustMark instanceof String string) {
+ try {
+ return SignedJWT.parse(string);
+ } catch (final ParseException e) {
+ log.error("Could not parse the trust mark into a JWT", e);
+ }
+ }
+ return null;
+ }
+
+}
\ No newline at end of file
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ValidateAutomaticRegistrationProfileConfiguration.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ValidateAutomaticRegistrationProfileConfiguration.java
index 530c3ca..a770c3f 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ValidateAutomaticRegistrationProfileConfiguration.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ValidateAutomaticRegistrationProfileConfiguration.java
@@ -199,21 +199,25 @@ public class ValidateAutomaticRegistrationProfileConfiguration extends AbstractP
ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
return;
}
- final OIDCClientInformation clientInformation = new OIDCClientInformation(
- new ClientID(selectedTrustChain.getFirst().get(0).getEntityID().getValue()), metadata);
-
+ final OIDCClientInformation clientInformation;
final Map<String, MetadataPolicy> localMetadataPolicy =
localMetadataPolicyLookupStrategy.apply(profileRequestContext);
if (localMetadataPolicy != null && !localMetadataPolicy.isEmpty()) {
log.debug("{} Applying local metadata policy into the client metadata", getLogPrefix());
- final OIDCClientInformation enforcedMetadata =
- localMetadataPolicyMergingStrategy.apply(clientInformation, localMetadataPolicy);
- if (enforcedMetadata == null) {
+ clientInformation = localMetadataPolicyMergingStrategy.apply(
+ new OIDCClientInformation(
+ new ClientID(selectedTrustChain.getFirst().get(0).getEntityID().getValue()),
+ metadata), localMetadataPolicy);
+ if (clientInformation == null) {
log.error("{} Could not apply the local metadata policy into the client metadata", getLogPrefix());
ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
return;
}
selectedTrustChain.setSecond(Map.of("openid_relying_party", clientInformation.toJSONObject()));
+ } else {
+ clientInformation = new OIDCClientInformation(
+ new ClientID(selectedTrustChain.getFirst().get(0).getEntityID().getValue()), metadata);
+
}
final String clientId = clientInformation.getID().getValue();
final List<String> mandatoryTrustMarks = mandatoryTrustMarksLookupStrategy.apply(profileRequestContext);
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/DefaultLocalMetadataPolicyMergingStrategy.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/DefaultLocalMetadataPolicyMergingStrategy.java
index 684aa0d..14eeb8f 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/DefaultLocalMetadataPolicyMergingStrategy.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/DefaultLocalMetadataPolicyMergingStrategy.java
@@ -79,7 +79,7 @@ public class DefaultLocalMetadataPolicyMergingStrategy extends AbstractIdentifia
if (localPolicy == null || localPolicy.isEmpty()) {
return inputMetadata;
}
- final JSONObject result = inputMetadata.getOIDCMetadata().toJSONObject();
+ final JSONObject result = inputMetadata.toJSONObject();
boolean compliant = true;
for (final String claim : localPolicy.keySet()) {
final MetadataPolicy policy = localPolicy.get(claim);
@@ -102,6 +102,7 @@ public class DefaultLocalMetadataPolicyMergingStrategy extends AbstractIdentifia
} else {
log.debug("The requested metadata is compliant with the policy");
try {
+ log.trace("Attempting to parse the metadata {}", result.toJSONString());
return OIDCClientInformation.parse(result);
} catch (final ParseException e) {
log.error("Could not parse the metadata object", e);
diff --git a/idp-oidfed-op-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml b/idp-oidfed-op-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
index e37696b..a94a38f 100644
--- a/idp-oidfed-op-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
+++ b/idp-oidfed-op-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
@@ -70,6 +70,13 @@
</constructor-arg>
</bean>
+ <bean id="shibboleth.oidfed.ResolveEntityTrustChainMetadataCache" parent="shibboleth.oidc.CacheBuilder">
+ <constructor-arg>
+ <bean p:cacheId="DefaultResolveEntityTrustChainMetadataCache" parent="shibboleth.oidfed.ResolveEntityTrustChainMetadataCacheBuilderSpec"
+ 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"/>
@@ -206,6 +213,43 @@
</property>
</bean>
+ <bean id="shibboleth.oidfed.ResolveEntityTrustChainMetadataCacheBuilderSpec"
+ class="net.shibboleth.oidc.metadata.cache.impl.DynamicMetadataCacheBuilderSpec"
+ p:minCacheDuration="%{idp.oidfed.entityConfiguration.maxRefreshDelay:PT1S}"
+ p:maxCacheDuration="%{idp.oidfed.entityConfiguration.maxRefreshDelay:PT30S}">
+ <property name="criteriaToIdentifierStrategy">
+ <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultResolveEntityRequestCriteriaToIdentifierStrategy" />
+ </property>
+ <property name="identifierExtractionStrategy">
+ <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultResolveEntityResponseIdentifierExtractionStrategy" />
+ </property>
+ <property name="metadataExpirationTimeStrategy">
+ <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultResolveEntityResponseContainerExpirationTimeStrategy"/>
+ </property>
+ <property name="metadataFilterStrategy">
+ <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultResolveEntityResponseSignatureValidationFilterStrategy"
+ p:entityConfigurationCache-ref="shibboleth.oidfed.EntityConfigurationMetadataCache">
+ <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.DefaultEntityConfigurationCredentialResolver" />
+ </constructor-arg>
+ <constructor-arg index="1">
+ <bean class="net.shibboleth.oidc.security.credential.impl.BasicJOSEObjectCredentialResolver" />
+ </constructor-arg>
+ </bean>
+ </property>
+ </bean>
+ </property>
+ <property name="fetchStrategy">
+ <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultResolveEntityTrustChainFetchingStrategy"
+ p:httpClient="#{getObject('shibboleth.oidfed.HttpClient') ?: getObject('shibboleth.InternalHttpClient')}"
+ p:httpClientSecurityParameters="#{getObject('shibboleth.oidfed.NonBrowser.HttpClientSecurityParameters')}"
+ p:localTrustAnchorsCache-ref="shibboleth.oidfed.LocalTrustAnchorsMetadataCache"
+ p:objectMapper-ref="shibboleth.oidfed.policy.JSONObjectMapper"/>
+ </property>
+ </bean>
+
<util:map id="shibboleth.oidfed.DefaultFederationPolicyConstraints"
value-type="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.constraints.FederationPolicyConstraint">
<entry key="max_path_length">
@@ -519,5 +563,10 @@
<bean class="com.nimbusds.oauth2.sdk.ErrorObject" c:_0="invalid_metadata" c:_1="Requested metadata is not compliant with the merged policy" c:_2="400" />
</property>
</bean>
-
+
+ <alias alias="UseResolverApiCondition" name="%{idp.oidfed.trustchain.resolver.useResolverApiCondition:shibboleth.Conditions.FALSE}" />
+ <alias alias="FallbackToLocalResolutionCondition" name="%{idp.oidfed.trustchain.resolver.fallbackToLocalCondition:shibboleth.Conditions.TRUE}" />
+
+ <import resource="${idp.home}/conf/oidfed/oidfed-trustchain-resolver.xml"/>
+
</beans>
\ No newline at end of file
diff --git a/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup-ext/oidfed/metadata-lookup-ext-oidfed-beans.xml b/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup-ext/oidfed/metadata-lookup-ext-oidfed-beans.xml
index e16681a..4a4cacc 100644
--- a/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup-ext/oidfed/metadata-lookup-ext-oidfed-beans.xml
+++ b/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup-ext/oidfed/metadata-lookup-ext-oidfed-beans.xml
@@ -14,6 +14,21 @@
</property>
</bean>
+ <bean id="TrustChainCandidatesExist" parent="shibboleth.Conditions.Expression"
+ c:expression="#input.ensureInboundMessageContext().containsSubcontext(T(net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.RelyingPartyTrustChainContext)) and #input.ensureInboundMessageContext().ensureSubcontext(T(net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.RelyingPartyTrustChainContext)).getPolicyCompliantTrustChains() != null and #input.ensureInboundMessageContext().ensureSubcontext(T(net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.RelyingPartyTrustChainCont [...]
+
+ <bean id="CallResolveEntityApi"
+ class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.CallResolveEntityApi"
+ p:entityConfigurationCache-ref="shibboleth.oidfed.EntityConfigurationMetadataCache"
+ p:resolveEntityTrustChainMetadataCache-ref="shibboleth.oidfed.ResolveEntityTrustChainMetadataCache"
+ p:objectMapper-ref="shibboleth.oidc.JSONObjectMapper"
+ p:trustedEntitiesLookupStrategy="#{getObject('shibboleth.oidfed.TrustedEntitiesLookupStrategy') ?: getObject('shibboleth.oidfed.DefaultTrustedEntitiesLookupStrategy')}"
+ p:preSelectedTrustChainIdsLookupStrategy="#{getObject('shibboleth.oidfed.PreSelectedTrustChainIDsLookupStrategy') ?: getObject('shibboleth.oidfed.DefaultPreSelectedTrustChainIDsLookupStrategy')}">
+ <property name="clientIDLookupStrategy">
+ <bean class="net.shibboleth.idp.plugin.oidc.op.profile.logic.DefaultMetadataLookupExtensionContextClientIDLookupFunction" />
+ </property>
+ </bean>
+
<bean id="ResolveTrustChains" class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.ResolveTrustChains"
scope="prototype"
p:trustChainCache-ref="#{'%{idp.oidfed.authorize.TrustChainMetadataCache:shibboleth.oidfed.TrustChainMetadataCache}'.trim()}"
@@ -113,6 +128,10 @@
<bean id="ValidateAutomaticRegistrationProfileConfiguration"
class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.ValidateAutomaticRegistrationProfileConfiguration"
scope="prototype">
+ <property name="localMetadataPolicyLookupStrategy">
+ <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.navigate.LocalMetadataPolicyLookupFunction"
+ p:relyingPartyContextLookupStrategy-ref="AutomaticRegistrationRelyingPartyCreationStrategy"/>
+ </property>
<property name="localMetadataPolicyMergingStrategy">
<bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.navigate.DefaultLocalMetadataPolicyMergingStrategy"
p:metadataPolicyEnforcer-ref="#{'%{idp.oidfed.authorize.MetadataPolicyEnforcer:DefaultMetadataPolicyEnforcer}'.trim()}"/>
diff --git a/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup-ext/oidfed/metadata-lookup-ext-oidfed-flow.xml b/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup-ext/oidfed/metadata-lookup-ext-oidfed-flow.xml
index ddbff40..51d6133 100644
--- a/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup-ext/oidfed/metadata-lookup-ext-oidfed-flow.xml
+++ b/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup-ext/oidfed/metadata-lookup-ext-oidfed-flow.xml
@@ -7,17 +7,38 @@
<set name="conversationScope.automaticallyRegistered" value="false" />
</on-entry>
<if test="AutomaticRegistrationCondition.test(opensamlProfileRequestContext) and !opensamlProfileRequestContext.ensureInboundMessageContext().containsSubcontext(T(net.shibboleth.oidc.metadata.context.OIDCMetadataContext))"
+ then="ChooseResolutionMethod" else="proceed" />
+ </decision-state>
+
+ <decision-state id="ChooseResolutionMethod">
+ <if test="UseResolverApiCondition.test(opensamlProfileRequestContext)"
+ then="CallResolveEntityApi" else="ResolveTrustChains" />
+ </decision-state>
+
+ <action-state id="CallResolveEntityApi">
+ <evaluate expression="CallResolveEntityApi" />
+ <evaluate expression="'proceed'" />
+ <transition on="proceed" to="CheckIfDoAutomaticRegistration">
+ <set name="flowScope.transitionForReselectTrustChain" value="'CallResolveEntityApi'" />
+ </transition>
+ <transition on="CheckFallback" to="CheckIfFallbackToLocalResolution" />
+ </action-state>
+
+ <decision-state id="CheckIfFallbackToLocalResolution">
+ <if test="FallbackToLocalResolutionCondition.test(opensamlProfileRequestContext)"
then="ResolveTrustChains" else="proceed" />
</decision-state>
<action-state id="ResolveTrustChains">
<evaluate expression="ResolveTrustChains" />
<evaluate expression="'proceed'" />
- <transition on="proceed" to="CheckIfDoAutomaticRegistration" />
+ <transition on="proceed" to="CheckIfDoAutomaticRegistration">
+ <set name="flowScope.transitionForReselectTrustChain" value="'DoAutomaticRegistration'" />
+ </transition>
</action-state>
<decision-state id="CheckIfDoAutomaticRegistration">
- <if test="opensamlProfileRequestContext.ensureInboundMessageContext().containsSubcontext(T(net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.RelyingPartyTrustChainContext)) and opensamlProfileRequestContext.ensureInboundMessageContext().ensureSubcontext(T(net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.RelyingPartyTrustChainContext)).getPolicyCompliantTrustChains() != null and opensamlProfileRequestContext.ensureInboundMessageContext().ensureSubcontext(T(net.shibboleth. [...]
+ <if test="TrustChainCandidatesExist.test(opensamlProfileRequestContext)"
then="DoAutomaticRegistration" else="proceed" />
</decision-state>
@@ -29,7 +50,7 @@
<evaluate expression="ValidateAutomaticRegistrationProfileConfiguration" />
<evaluate expression="InitializeRelyingPartyContext" />
<evaluate expression="'proceed'" />
- <transition on="ReselectTrustChain" to="DoAutomaticRegistration" />
+ <transition on="ReselectTrustChain" to="#{transitionForReselectTrustChain}" />
<transition on="proceed" to="proceed">
<set name="conversationScope.automaticallyRegistered" value="opensamlProfileRequestContext.ensureInboundMessageContext().containsSubcontext(T(net.shibboleth.oidc.metadata.context.OIDCMetadataContext))" />
</transition>
@@ -38,10 +59,12 @@
<end-state id="proceed"/>
<end-state id="InvalidMetadataPolicy"/>
<end-state id="InvalidMetadataAgainstPolicy"/>
+ <end-state id="HandleError"/>
<global-transitions>
<transition on="InvalidMetadataPolicy" to="InvalidMetadataPolicy" />
<transition on="InvalidMetadataAgainstPolicy" to="InvalidMetadataAgainstPolicy" />
+ <transition on="HandleError" to="HandleError" />
</global-transitions>
diff --git a/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml b/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml
index b62fda2..e3e0353 100644
--- a/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml
+++ b/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml
@@ -19,7 +19,22 @@
<bean id="OIDFED.AutomaticRegistration" parent="AbstractOIDFederationProfile" lazy-init="true"
class="net.shibboleth.idp.plugin.oidc.op.oidfed.config.DefaultOIDFederationAutomaticRegistrationProfileConfiguration"
- p:mandatoryTrustMarks="%{idp.oidfed.automaticRegistration.mandatoryTrustMarks:}" />
+ p:mandatoryTrustMarks="%{idp.oidfed.automaticRegistration.mandatoryTrustMarks:}">
+ <property name="localMetadataPolicyLookupStrategy">
+ <bean parent="shibboleth.Functions.Constant">
+ <constructor-arg name="target">
+ <util:map>
+ <entry key="scope">
+ <bean class="net.shibboleth.oidc.metadata.policy.MetadataPolicy" p:defaultValue="openid" />
+ </entry>
+ <entry key="token_endpoint_auth_method">
+ <bean class="net.shibboleth.oidc.metadata.policy.MetadataPolicy" p:defaultValue="private_key_jwt" />
+ </entry>
+ </util:map>
+ </constructor-arg>
+ </bean>
+ </property>
+ </bean>
<bean id="OIDFED.ExplicitRegistration" parent="AbstractOIDFederationProfile" lazy-init="true"
class="net.shibboleth.idp.plugin.oidc.op.oidfed.config.DefaultOIDFederationExplicitRegistrationProfileConfiguration"
@@ -63,13 +78,12 @@
<bean id="shibboleth.oidfed.DefaultSecurityConfiguration"
class="net.shibboleth.oidc.profile.config.JSONSecurityConfiguration" c:clockSkew="%{idp.policy.clockSkew:PT1M}">
<constructor-arg name="idGenerator">
- <bean
- class="net.shibboleth.shared.security.IdentifierGenerationStrategy" factory-method="getInstance">
+ <bean class="net.shibboleth.shared.security.IdentifierGenerationStrategy" factory-method="getInstance">
<constructor-arg>
<util:constant
static-field="net.shibboleth.shared.security.IdentifierGenerationStrategy.ProviderType.SECURE" />
</constructor-arg>
- </bean>
+ </bean>
</constructor-arg>
<property name="jwtSignatureSigningConfiguration">
<ref bean="#{'%{idp.oidfed.signing.config:shibboleth.oidfed.SigningConfiguration}'.trim()}" />
diff --git a/idp-oidfed-op-impl/src/main/resources/net/shibboleth/idp/plugin/oidc/op/oidfed/conf/oidfed/oidfed-trustchain-resolver.xml b/idp-oidfed-op-impl/src/main/resources/net/shibboleth/idp/plugin/oidc/op/oidfed/conf/oidfed/oidfed-trustchain-resolver.xml
new file mode 100644
index 0000000..8a26100
--- /dev/null
+++ b/idp-oidfed-op-impl/src/main/resources/net/shibboleth/idp/plugin/oidc/op/oidfed/conf/oidfed/oidfed-trustchain-resolver.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans"
+ xmlns:context="http://www.springframework.org/schema/context"
+ xmlns:util="http://www.springframework.org/schema/util" xmlns:p="http://www.springframework.org/schema/p"
+ xmlns:c="http://www.springframework.org/schema/c" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
+ http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
+ http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"
+
+ default-init-method="initialize" default-destroy-method="destroy">
+
+ <!--
+ The default function for fetching the map of trusted entities for trust chain resolution via resolve entity API.
+ The map is keyed with entityId of trusted entities whose federation_resolve_endpoint is used as the API endpoint.
+ The entity configuration is used as a trusted source for the endpoint. The map value is a list of trust anchors
+ that will be used for the trust_anchor paramter(s) in the API request.
+
+ The default function may be overridden via 'shibboleth.oidfed.TrustedEntitiesLookupStrategy' bean that must
+ implement Function<ProfileRequestContext, Map<String,List<String>>>.
+ -->
+ <bean id="shibboleth.oidfed.DefaultTrustedEntitiesLookupStrategy" parent="shibboleth.Functions.Constant">
+ <constructor-arg name="target">
+ <util:map>
+ <!--
+ Example entry for a trusted entity 'https://trust-anchor.federation.local'
+ its 'federation_resolve_endpoint' is exploited with the configured trust_anchor parameters:
+ '...&trust_anchor=https://trust-anchor.federation.local'
+ -->
+ <!--
+ <entry key="https://trust-anchor.federation.local">
+ <util:list value-type="java.lang.String">
+ <value>https://trust-anchor.federation.local</value>
+ </util:list>
+ </entry>
+ -->
+ </util:map>
+ </constructor-arg>
+ </bean>
+
+</beans>
diff --git a/idp-oidfed-op-impl/src/main/resources/net/shibboleth/idp/plugin/oidc/op/oidfed/module.properties b/idp-oidfed-op-impl/src/main/resources/net/shibboleth/idp/plugin/oidc/op/oidfed/module.properties
index c7ded79..54d11a0 100644
--- a/idp-oidfed-op-impl/src/main/resources/net/shibboleth/idp/plugin/oidc/op/oidfed/module.properties
+++ b/idp-oidfed-op-impl/src/main/resources/net/shibboleth/idp/plugin/oidc/op/oidfed/module.properties
@@ -15,3 +15,7 @@ idp.oidc.OP.oidfed.1.replace = false
idp.oidc.OP.oidfed.2.src = /net/shibboleth/idp/plugin/oidc/op/oidfed/conf/oidfed/oidfed-entity-configuration-metadata.json
idp.oidc.OP.oidfed.2.dest = conf/oidfed/oidfed-entity-configuration-metadata.json
idp.oidc.OP.oidfed.2.replace = false
+
+idp.oidc.OP.oidfed.3.src = /net/shibboleth/idp/plugin/oidc/op/oidfed/conf/oidfed/oidfed-trustchain-resolver.xml
+idp.oidc.OP.oidfed.3.dest = conf/oidfed/oidfed-trustchain-resolver.xml
+idp.oidc.OP.oidfed.3.replace = false
diff --git a/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/AbstractFederationFlowTest.java b/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/AbstractFederationFlowTest.java
index a4924d5..9d44226 100644
--- a/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/AbstractFederationFlowTest.java
+++ b/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/AbstractFederationFlowTest.java
@@ -23,6 +23,7 @@ import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
+import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.security.KeyPair;
import java.security.NoSuchAlgorithmException;
@@ -91,6 +92,7 @@ public class AbstractFederationFlowTest extends AbstractOidcFlowTest {
final String intermediateIdPattern = "https://intermediate-authority%s.federation.local";
final String anchorId = "https://trust-anchor.federation.local";
final String anchorFetchEndpoint = anchorId + "/fetch";
+ final String anchorResolveEndpoint = anchorId + "/resolve";
String issuer = "https://op.example.org";
JWK rpKey;
@@ -170,6 +172,17 @@ public class AbstractFederationFlowTest extends AbstractOidcFlowTest {
return fetchEndpoint + "?sub=" + subject;
}
+ protected String resolveEntityUrl(final String resolveEndpoint, final String subject,
+ final String... trustAnchors) {
+ final StringBuilder builder = new StringBuilder(resolveEndpoint + "?sub=" +
+ URLEncoder.encode(subject, Charset.forName("UTF-8")));
+ for (final String trustAnchor : trustAnchors) {
+ builder.append("&trust_anchor=" + URLEncoder.encode(trustAnchor, Charset.forName("UTF-8")));
+ }
+ builder.append("&entity_type=openid_relying_party");
+ return builder.toString();
+ }
+
protected void mapResponse(final String requestUri, final ClassicHttpResponse classicResponse) throws IOException {
when(federationHttpClient.executeOpen(any(), argThat(new RequestUriMatcher(requestUri)), any()))
.thenReturn(classicResponse);
@@ -271,7 +284,7 @@ public class AbstractFederationFlowTest extends AbstractOidcFlowTest {
.expirationTime(Date.from(Instant.now().plusSeconds(300)))
.claim("jwks", new JWKSet(trustedAnchorKey).toJSONObject(true))
.claim("metadata", Map.of("federation_entity", Map.of("federation_fetch_endpoint",
- anchorFetchEndpoint)))
+ anchorFetchEndpoint, "federation_resolve_endpoint", anchorResolveEndpoint)))
.build();
final EntityStatement anchorConfiguration =
TrustChainTestUtil.entityStatement(JWSAlgorithm.RS256, trustedAnchorKey, claimsSet);
@@ -351,7 +364,27 @@ public class AbstractFederationFlowTest extends AbstractOidcFlowTest {
TrustChainTestUtil.entityStatement(JWSAlgorithm.RS256, issuerKey, claimsSet);
return rpConfiguration.getSignedStatement().serialize();
}
-
+
+ protected String rpResolveEntityResponse(final String clientId, final Map<String, Object> metadata) {
+ final List<String> trustChain;
+ try {
+ trustChain = List.of(rpEntityConfiguration(clientId),
+ subordinateStatement(clientId, Map.of("openid_relying_party",
+ new OIDCClientMetadata().toJSONObject())),
+ trustedAnchorConfiguration());
+ } catch (URISyntaxException e) {
+ Assert.fail("Could not build URI", e);
+ return null;
+ }
+ final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder().issuer(anchorId).subject(clientId)
+ .issueTime(Date.from(Instant.now()))
+ .expirationTime(Date.from(Instant.now().plusSeconds(300)))
+ .claim("trust_chain", trustChain)
+ .claim("metadata", metadata)
+ .build();
+ return TrustChainTestUtil.signedJwt(JWSAlgorithm.RS256, trustedAnchorKey, "application/resolve-response+jwt",
+ claimsSet).serialize();
+ }
protected String uniqueClientId() {
return String.format(clientIdPattern, clientIndex.getAndIncrement());
}
@@ -420,6 +453,20 @@ public class AbstractFederationFlowTest extends AbstractOidcFlowTest {
}
}
+ protected void rpResolveEntityConfigureMockHttpClient(final String clientId) {
+ try {
+ mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+ final OIDCClientMetadata metadata = new OIDCClientMetadata();
+ metadata.setRedirectionURI(new URI(redirectUri));
+ metadata.setJWKSet(new JWKSet(rpKey.toPublicJWK()));
+ mapResponse(resolveEntityUrl(anchorResolveEndpoint, clientId, anchorId),
+ mockResponse(rpResolveEntityResponse(clientId, Map.of("openid_relying_party",
+ metadata.toJSONObject()))));
+ } catch (UnsupportedOperationException | IOException | URISyntaxException e) {
+ Assert.fail("Could not initialize mock HTTP client", e);
+ }
+ }
+
@SuppressWarnings("unchecked")
protected void rpConfigureMockHttpClient(final String clientId, final Map<String, Object> testVector) {
final String intermediateId = uniqueIntermediateId();
diff --git a/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/AuthorizeFlowAutomaticRegistrationTest.java b/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/AuthorizeFlowAutomaticRegistrationTest.java
index 9c4d31c..c5ea9e0 100644
--- a/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/AuthorizeFlowAutomaticRegistrationTest.java
+++ b/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/AuthorizeFlowAutomaticRegistrationTest.java
@@ -35,6 +35,7 @@ import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.webflow.executor.FlowExecutionResult;
import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -58,6 +59,9 @@ import net.shibboleth.shared.security.DataSealerException;
public class AuthorizeFlowAutomaticRegistrationTest extends AbstractFederationFlowTest {
+ public static final String USE_CUSTOM_RESOLVER_API_CONDITION = "useCustomResolverApi";
+ public static final String USE_CUSTOM_FAILBACK_TO_LOCAL_CONDITION = "useCustomFallbackToLocal";
+
@Autowired
@Qualifier("shibboleth.StorageService")
StorageService storageService;
@@ -65,6 +69,11 @@ public class AuthorizeFlowAutomaticRegistrationTest extends AbstractFederationFl
public AuthorizeFlowAutomaticRegistrationTest() {
super(AuthorizeFlowTest.FLOW_ID);
}
+
+ @BeforeMethod
+ public void removeHeaders() {
+ request.removeHeader(USE_CUSTOM_RESOLVER_API_CONDITION);
+ }
@Test
public void testWithValidTrustChain_noRequestObject()
@@ -116,6 +125,95 @@ public class AuthorizeFlowAutomaticRegistrationTest extends AbstractFederationFl
Assert.assertEquals(unwrapTrustChainFromAuthorizeCode(successResponse), List.of(clientId, anchorId));
}
+ @Test
+ public void testWithValidTrustChain_resolveApi_signedRequestObject()
+ throws IOException, UnsupportedOperationException, URISyntaxException {
+ final String clientId = uniqueClientId();
+ rpResolveEntityConfigureMockHttpClient(clientId);
+ request.addHeader(USE_CUSTOM_RESOLVER_API_CONDITION, "true");
+ final FlowExecutionResult result =
+ launchAuthenticationRequest(clientId, "openid profile", signedRequestObject(Map.of(
+ "iss", clientId,
+ "client_id", clientId,
+ "aud", issuer,
+ "exp", Instant.now().plus(Duration.ofMinutes(5)).toEpochMilli(),
+ "jti", UUID.randomUUID(),
+ "response_type", "code",
+ "scope", "openid profile",
+ "redirect_uri", redirectUri)));
+ final AuthenticationResponse responseMessage = parseSuccessResponse(result, AuthenticationResponse.class);
+ final AuthenticationSuccessResponse successResponse = responseMessage.toSuccessResponse();
+ Assert.assertEquals(successResponse.getRedirectionURI().toString(), redirectUri);
+ Assert.assertNull(successResponse.getIDToken());
+ Assert.assertNull(successResponse.getAccessToken());
+ Assert.assertNotNull(successResponse.getAuthorizationCode());
+ Assert.assertEquals(unwrapTrustChainFromAuthorizeCode(successResponse), List.of(clientId, anchorId));
+ }
+
+ @Test
+ public void testWithValidTrustChain_resolveApiFailsNoFaildback_signedRequestObject()
+ throws IOException, UnsupportedOperationException, URISyntaxException {
+ final String clientId = uniqueClientId();
+ rpConfigureMockHttpClient(clientId);
+ request.addHeader(USE_CUSTOM_RESOLVER_API_CONDITION, "true");
+ final FlowExecutionResult result =
+ launchAuthenticationRequest(clientId, "openid profile", signedRequestObject(Map.of(
+ "iss", clientId,
+ "client_id", clientId,
+ "aud", issuer,
+ "exp", Instant.now().plus(Duration.ofMinutes(5)).toEpochMilli(),
+ "jti", UUID.randomUUID(),
+ "response_type", "code",
+ "scope", "openid profile",
+ "redirect_uri", redirectUri)));
+ Assert.assertEquals(result.getOutcome().getId(), "ErrorView");
+ }
+
+ @Test
+ public void testWithValidTrustChain_resolveApiFailsFailbackFails_signedRequestObject()
+ throws IOException, UnsupportedOperationException, URISyntaxException {
+ final String clientId = uniqueClientId();
+ request.addHeader(USE_CUSTOM_RESOLVER_API_CONDITION, "true");
+ request.addHeader(USE_CUSTOM_FAILBACK_TO_LOCAL_CONDITION, "true");
+ final FlowExecutionResult result =
+ launchAuthenticationRequest(clientId, "openid profile", signedRequestObject(Map.of(
+ "iss", clientId,
+ "client_id", clientId,
+ "aud", issuer,
+ "exp", Instant.now().plus(Duration.ofMinutes(5)).toEpochMilli(),
+ "jti", UUID.randomUUID(),
+ "response_type", "code",
+ "scope", "openid profile",
+ "redirect_uri", redirectUri)));
+ Assert.assertEquals(result.getOutcome().getId(), "ErrorView");
+ }
+
+ @Test
+ public void testWithValidTrustChain_resolveApiFailsFailbackSuccess_signedRequestObject()
+ throws IOException, UnsupportedOperationException, URISyntaxException {
+ final String clientId = uniqueClientId();
+ rpConfigureMockHttpClient(clientId);
+ request.addHeader(USE_CUSTOM_RESOLVER_API_CONDITION, "true");
+ request.addHeader(USE_CUSTOM_FAILBACK_TO_LOCAL_CONDITION, "true");
+ final FlowExecutionResult result =
+ launchAuthenticationRequest(clientId, "openid profile", signedRequestObject(Map.of(
+ "iss", clientId,
+ "client_id", clientId,
+ "aud", issuer,
+ "exp", Instant.now().plus(Duration.ofMinutes(5)).toEpochMilli(),
+ "jti", UUID.randomUUID(),
+ "response_type", "code",
+ "scope", "openid profile",
+ "redirect_uri", redirectUri)));
+ final AuthenticationResponse responseMessage = parseSuccessResponse(result, AuthenticationResponse.class);
+ final AuthenticationSuccessResponse successResponse = responseMessage.toSuccessResponse();
+ Assert.assertEquals(successResponse.getRedirectionURI().toString(), redirectUri);
+ Assert.assertNull(successResponse.getIDToken());
+ Assert.assertNull(successResponse.getAccessToken());
+ Assert.assertNotNull(successResponse.getAuthorizationCode());
+ Assert.assertEquals(unwrapTrustChainFromAuthorizeCode(successResponse), List.of(clientId, anchorId));
+ }
+
@Test
public void testWithInvalidTrustChain_signedRequestObject_unmatchingRpEntityConfigurationSignature()
throws IOException, UnsupportedOperationException, URISyntaxException {
diff --git a/idp-oidfed-op-impl/src/test/resources/net/shibboleth/idp/module/conf/global.xml b/idp-oidfed-op-impl/src/test/resources/net/shibboleth/idp/module/conf/global.xml
index e11b63a..1589b62 100644
--- a/idp-oidfed-op-impl/src/test/resources/net/shibboleth/idp/module/conf/global.xml
+++ b/idp-oidfed-op-impl/src/test/resources/net/shibboleth/idp/module/conf/global.xml
@@ -89,4 +89,12 @@
<constructor-arg value="#{T(org.apache.hc.client5.http.classic.HttpClient)}" />
</bean>
+ <bean id="HeaderUseResolveApiCondition" parent="shibboleth.Conditions.Expression"
+ p:customObject-ref="shibboleth.HttpServletRequestSupplier"
+ c:expression="'true'.equals(#custom.get().getHeader('useCustomResolverApi'))" />
+
+ <bean id="HeaderFallbackToLocalCondition" parent="shibboleth.Conditions.Expression"
+ p:customObject-ref="shibboleth.HttpServletRequestSupplier"
+ c:expression="'true'.equals(#custom.get().getHeader('useCustomFallbackToLocal'))" />
+
</beans>
diff --git a/idp-oidfed-op-impl/src/test/resources/net/shibboleth/idp/module/conf/oidc.properties b/idp-oidfed-op-impl/src/test/resources/net/shibboleth/idp/module/conf/oidc.properties
index b996d9f..d663f4f 100644
--- a/idp-oidfed-op-impl/src/test/resources/net/shibboleth/idp/module/conf/oidc.properties
+++ b/idp-oidfed-op-impl/src/test/resources/net/shibboleth/idp/module/conf/oidc.properties
@@ -37,4 +37,6 @@ idp.authn.OAuth2Client.audit.format = %a|%T|%SP|%I|%s|%AF|%CV|%u|%tu|%AR|%UA|%is
idp.oidfed.authorize.automaticRegistrationCondition = shibboleth.Conditions.TRUE
idp.oidfed.par.automaticRegistrationCondition = shibboleth.Conditions.TRUE
idp.oidfed.configuration.resolver.values = CustomEntityConfigurationValues
-idp.oidfed.configuration.MetadataSkaletonFile = src/test/resources/net/shibboleth/idp/module/conf/oidfed-entity-configuration-metadata.json
\ No newline at end of file
+idp.oidfed.configuration.MetadataSkaletonFile = src/test/resources/net/shibboleth/idp/module/conf/oidfed-entity-configuration-metadata.json
+idp.oidfed.trustchain.resolver.useResolverApiCondition = HeaderUseResolveApiCondition
+idp.oidfed.trustchain.resolver.fallbackToLocalCondition = HeaderFallbackToLocalCondition
\ No newline at end of file
diff --git a/idp-oidfed-op-impl/src/test/resources/net/shibboleth/idp/module/conf/oidfed/oidfed-trustchain-resolver.xml b/idp-oidfed-op-impl/src/test/resources/net/shibboleth/idp/module/conf/oidfed/oidfed-trustchain-resolver.xml
new file mode 100644
index 0000000..e831bef
--- /dev/null
+++ b/idp-oidfed-op-impl/src/test/resources/net/shibboleth/idp/module/conf/oidfed/oidfed-trustchain-resolver.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans"
+ xmlns:context="http://www.springframework.org/schema/context"
+ xmlns:util="http://www.springframework.org/schema/util" xmlns:p="http://www.springframework.org/schema/p"
+ xmlns:c="http://www.springframework.org/schema/c" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
+ http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
+ http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"
+
+ default-init-method="initialize" default-destroy-method="destroy">
+
+ <bean id="shibboleth.oidfed.DefaultTrustedEntitiesLookupStrategy" parent="shibboleth.Functions.Constant">
+ <constructor-arg name="target">
+ <util:map>
+ <entry key="https://trust-anchor.federation.local">
+ <util:list value-type="java.lang.String">
+ <value>https://trust-anchor.federation.local</value>
+ </util:list>
+ </entry>
+ </util:map>
+ </constructor-arg>
+ </bean>
+
+</beans>
\ No newline at end of file
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list