[java-idp-oidc] branch dev/JOIDC-222 updated: JOIDC-222 - Support for OpenID Federation
Henri Mikkonen
henri.mikkonen at iki.fi
Fri Apr 18 11:49:40 UTC 2025
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=12f632bef628d33a26c9140d6f27b00080273867
The following commit(s) were added to refs/heads/dev/JOIDC-222 by this push:
new 12f632be JOIDC-222 - Support for OpenID Federation
12f632be is described below
commit 12f632bef628d33a26c9140d6f27b00080273867
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Fri Apr 18 14:49:15 2025 +0300
JOIDC-222 - Support for OpenID Federation
https://shibboleth.atlassian.net/browse/JOIDC-222
- Initial (incomplete) implementation of Resolve Entity API (oidfed/resolve-entity). WIP.
- Currently hardcoded to serve only 'openid_relying_party' entity types
- New profile configuration: OIDFED.ResolveEntity (DefaultOIDFederationResolveEntityProfileConfiguration)
- Comfigurable lifetimes for cached success and error responses (default 5 minutes
- The responses are cached by using the request message contents (parameters) as the key
- Only locally trusted trust anchors are included in the key
---
...ederationResolveEntityProfileConfiguration.java | 164 +++++++++++++++
...ederationResolveEntityProfileConfiguration.java | 66 ++++++
...yCachedErrorResponseLifetimeLookupFunction.java | 45 ++++
...achedSuccessResponseLifetimeLookupFunction.java | 45 ++++
.../decoding/impl/ResolveEntityRequestDecoder.java | 107 ++++++++++
.../messaging/impl/ResolveEntityRequest.java | 133 ++++++++++++
.../messaging/impl/ResolveEntityResponse.java | 99 +++++++++
.../DefaultEntityStatementFetchingStrategy.java | 5 +-
...eEntityRequestCriteriaToIdentifierStrategy.java | 42 ++++
...ityResponseContainerExpirationTimeStrategy.java | 59 ++++++
...faultResolveEntityResponseFetchingStrategy.java | 65 ++++++
...EntityResponseIdentifierExtractionStrategy.java | 38 ++++
.../ResolveEntityContainerExpirationCriterion.java | 79 +++++++
.../metadata/ResolveEntityRequestCriterion.java | 80 +++++++
.../metadata/ResolveEntityResponseContainer.java | 85 ++++++++
.../metadata/ResolveEntityResponseCriterion.java | 80 +++++++
.../metadata/TrustAnchorEntityIDsCriterion.java | 80 +++++++
.../BuildResolveEntityErrorResponseFromEvent.java | 157 ++++++++++++++
.../profile/impl/BuildResolveEntityResponse.java | 153 ++++++++++++++
.../impl/FormOutboundResolveEntityResponse.java | 230 +++++++++++++++++++++
.../impl/LookupCachedResolveEntityRespomse.java | 151 ++++++++++++++
.../oidfed/profile/impl/OidFederationEventIds.java | 46 +++++
.../impl/RelyingPartyResolveEntityContext.java | 79 +++++++
.../op/oidfed/profile/impl/SelectTrustChain.java | 3 +-
.../profile/impl/ValidateResolveEntityRequest.java | 185 +++++++++++++++++
.../profile/impl/ValidateSelectedTrustChain.java | 224 ++++++++++++++++++++
.../META-INF/net.shibboleth.idp/postconfig.xml | 21 ++
.../oidfed/resolve-entity/resolve-entity-beans.xml | 198 ++++++++++++++++++
.../oidfed/resolve-entity/resolve-entity-flow.xml | 77 +++++++
.../idp/service/relying-party/postconfig.xml | 5 +-
.../profile/flow/oidfed/ResolveEntityFlowTest.java | 82 ++++++++
.../shibboleth/idp/module/conf/relying-party.xml | 1 +
32 files changed, 2879 insertions(+), 5 deletions(-)
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/config/DefaultOIDFederationResolveEntityProfileConfiguration.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/config/DefaultOIDFederationResolveEntityProfileConfiguration.java
new file mode 100644
index 00000000..a0dfb3d3
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/config/DefaultOIDFederationResolveEntityProfileConfiguration.java
@@ -0,0 +1,164 @@
+/*
+ * 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.config;
+
+import java.time.Duration;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import net.shibboleth.profile.config.AbstractConditionalProfileConfiguration;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.annotation.constraint.Positive;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.logic.FunctionSupport;
+
+/**
+ * Implementation of a profile configuration for the OpenID Federation Resolve Entity.
+ */
+public class DefaultOIDFederationResolveEntityProfileConfiguration extends AbstractConditionalProfileConfiguration
+ implements OIDFederationResolveEntityProfileConfiguration {
+
+ /** OIDC provider information profile counter name. */
+ @Nonnull @NotEmpty public static final String PROFILE_COUNTER = "net.shibboleth.idp.profiles.oidfed.resolve-entity";
+
+ /** Lookup function to override issuer value. */
+ @Nonnull private Function<ProfileRequestContext,String> issuerLookupStrategy;
+
+ /** Lookup function to supply cached success response lifetime. */
+ @Nonnull private Function<ProfileRequestContext,Duration> cachedSuccessResponseLifetimeLookupStrategy;
+
+ /** Lookup function to supply cached error response lifetime. */
+ @Nonnull private Function<ProfileRequestContext,Duration> cachedErrorResponseLifetimeLookupStrategy;
+
+ /**
+ * Constructor.
+ */
+ public DefaultOIDFederationResolveEntityProfileConfiguration() {
+ this(PROFILE_ID);
+ }
+
+ /**
+ * Creates a new configuration instance.
+ *
+ * @param profileId Unique profile identifier.
+ */
+ public DefaultOIDFederationResolveEntityProfileConfiguration(@Nonnull @NotEmpty final String profileId) {
+ super(profileId);
+ issuerLookupStrategy = FunctionSupport.constant(null);
+ cachedSuccessResponseLifetimeLookupStrategy = FunctionSupport.constant(Duration.ofMinutes(5));
+ cachedErrorResponseLifetimeLookupStrategy = FunctionSupport.constant(Duration.ofMinutes(5));
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ @Nullable @NotEmpty public String getIssuer(@Nullable final ProfileRequestContext profileRequestContext) {
+ return issuerLookupStrategy.apply(profileRequestContext);
+ }
+
+ /**
+ * Set overridden issuer value.
+ *
+ * @param issuer issuer value
+ */
+ public void setIssuer(@Nullable @NotEmpty final String issuer) {
+ issuerLookupStrategy = FunctionSupport.constant(issuer);
+ }
+
+ /**
+ * Sets lookup strategy for overridden issuer value.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setIssuerLookupStrategy(@Nonnull final Function<ProfileRequestContext,String> strategy) {
+ issuerLookupStrategy = Constraint.isNotNull(strategy, "Issuer lookup strategy cannot be null");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ @Positive @Nonnull
+ public Duration getCachedSuccessResponseLifetime(@Nullable final ProfileRequestContext profileRequestContext) {
+ final Duration lifetime = cachedSuccessResponseLifetimeLookupStrategy.apply(profileRequestContext);
+
+ Constraint.isTrue(lifetime != null && !lifetime.isZero() && !lifetime.isNegative(),
+ "Success response lifetime must be greater than 0");
+ assert lifetime != null;
+ return lifetime;
+ }
+
+ /**
+ * Set the lifetime of a cached success response.
+ *
+ * @param lifetime lifetime of a cached success response
+ */
+ public void setCachedSuccessResponseLifetime(@Positive @Nonnull final Duration lifetime) {
+ final Duration successLifetime = Constraint.isNotNull(lifetime,
+ "Cached success response lifetime cannot be null");
+ Constraint.isTrue(!successLifetime.isZero() && !successLifetime.isNegative(),
+ "Cached success response lifetime must be greater than 0");
+
+ cachedSuccessResponseLifetimeLookupStrategy = FunctionSupport.constant(successLifetime);
+ }
+
+ /**
+ * Set a lookup strategy for the cached success response lifetime.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setCachedSuccessResponseLifetimeLookupStrategy(
+ @Nullable final Function<ProfileRequestContext,Duration> strategy) {
+ cachedSuccessResponseLifetimeLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ @Positive @Nonnull
+ public Duration getCachedErrorResponseLifetime(@Nullable final ProfileRequestContext profileRequestContext) {
+ final Duration lifetime = cachedErrorResponseLifetimeLookupStrategy.apply(profileRequestContext);
+
+ Constraint.isTrue(lifetime != null && !lifetime.isZero() && !lifetime.isNegative(),
+ "Error response lifetime must be greater than 0");
+ assert lifetime != null;
+ return lifetime;
+ }
+
+ /**
+ * Set the lifetime of a cached error response.
+ *
+ * @param lifetime lifetime of a cached error response
+ */
+ public void setCachedErrorResponseLifetime(@Positive @Nonnull final Duration lifetime) {
+ final Duration errorLifetime = Constraint.isNotNull(lifetime,
+ "Cached error response lifetime cannot be null");
+ Constraint.isTrue(!errorLifetime.isZero() && !errorLifetime.isNegative(),
+ "Cached error response lifetime must be greater than 0");
+
+ cachedErrorResponseLifetimeLookupStrategy = FunctionSupport.constant(errorLifetime);
+ }
+
+ /**
+ * Set a lookup strategy for the cached error response lifetime.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setCachedErrorResponseLifetimeLookupStrategy(
+ @Nullable final Function<ProfileRequestContext,Duration> strategy) {
+ cachedErrorResponseLifetimeLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null");
+ }
+
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/config/OIDFederationResolveEntityProfileConfiguration.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/config/OIDFederationResolveEntityProfileConfiguration.java
new file mode 100644
index 00000000..f5afddaa
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/config/OIDFederationResolveEntityProfileConfiguration.java
@@ -0,0 +1,66 @@
+/*
+ * 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.config;
+
+import java.time.Duration;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import net.shibboleth.profile.config.OverriddenIssuerProfileConfiguration;
+import net.shibboleth.shared.annotation.ConfigurationSetting;
+import net.shibboleth.shared.annotation.constraint.Positive;
+
+/**
+ * Profile configuration for an OpenID Federation Resolve Entity.
+ */
+public interface OIDFederationResolveEntityProfileConfiguration extends OverriddenIssuerProfileConfiguration,
+ OIDFederationProfileConfiguration {
+
+ /** OIDC base protocol URI. */
+ public static final String PROTOCOL_URI = "https://openid.net/specs/openid-federation-1_0.html";
+
+ /** ID for this profile configuration. */
+ public static final String PROFILE_ID = "http://shibboleth.net/ns/profiles/oidfed/resolve-entity";
+
+ /**
+ * Get cached success response lifetime.
+ *
+ * <p>Defaults to 5 minutes.</p>
+ *
+ * @param profileRequestContext profile request context
+ *
+ * @return cached success response lifetime
+ */
+ @ConfigurationSetting(name="cachedSuccessResponseLifetime")
+ @Positive @Nonnull
+ Duration getCachedSuccessResponseLifetime(@Nullable final ProfileRequestContext profileRequestContext);
+
+ /**
+ * Get cached error response lifetime.
+ *
+ * <p>Defaults to 5 minutes.</p>
+ *
+ * @param profileRequestContext profile request context
+ *
+ * @return cached error response lifetime
+ */
+ @ConfigurationSetting(name="cachedErrorResponseLifetime")
+ @Positive @Nonnull
+ Duration getCachedErrorResponseLifetime(@Nullable final ProfileRequestContext profileRequestContext);
+
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/config/ResolveEntityCachedErrorResponseLifetimeLookupFunction.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/config/ResolveEntityCachedErrorResponseLifetimeLookupFunction.java
new file mode 100644
index 00000000..d22579c7
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/config/ResolveEntityCachedErrorResponseLifetimeLookupFunction.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.config;
+
+import java.time.Duration;
+import java.util.Optional;
+
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import net.shibboleth.profile.context.navigate.AbstractRelyingPartyLookupFunction;
+
+/**
+ * A function that returns
+ * {@link OIDFederationResolveEntityProfileConfiguration#getCachedErrorResponseLifetime(ProfileRequestContext)}.
+ *
+ * <p>If a specific setting is unavailable, a null value is returned.</p>
+ */
+public class ResolveEntityCachedErrorResponseLifetimeLookupFunction
+ extends AbstractRelyingPartyLookupFunction<Duration> {
+
+ /** {@inheritDoc} */
+ @Override
+ @Nullable public Duration apply(@Nullable final ProfileRequestContext input) {
+ return Optional.ofNullable(getRelyingPartyContextLookupStrategy().apply(input))
+ .map(relyingPartyContext -> relyingPartyContext.getProfileConfig())
+ .filter(OIDFederationResolveEntityProfileConfiguration.class::isInstance)
+ .map(OIDFederationResolveEntityProfileConfiguration.class::cast)
+ .map(config -> config.getCachedErrorResponseLifetime(input))
+ .orElse(null);
+ }
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/config/ResolveEntityCachedSuccessResponseLifetimeLookupFunction.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/config/ResolveEntityCachedSuccessResponseLifetimeLookupFunction.java
new file mode 100644
index 00000000..1ffecda0
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/config/ResolveEntityCachedSuccessResponseLifetimeLookupFunction.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.config;
+
+import java.time.Duration;
+import java.util.Optional;
+
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import net.shibboleth.profile.context.navigate.AbstractRelyingPartyLookupFunction;
+
+/**
+ * A function that returns
+ * {@link OIDFederationResolveEntityProfileConfiguration#getCachedSuccessResponseLifetime(ProfileRequestContext)}.
+ *
+ * <p>If a specific setting is unavailable, a null value is returned.</p>
+ */
+public class ResolveEntityCachedSuccessResponseLifetimeLookupFunction
+ extends AbstractRelyingPartyLookupFunction<Duration> {
+
+ /** {@inheritDoc} */
+ @Override
+ @Nullable public Duration apply(@Nullable final ProfileRequestContext input) {
+ return Optional.ofNullable(getRelyingPartyContextLookupStrategy().apply(input))
+ .map(relyingPartyContext -> relyingPartyContext.getProfileConfig())
+ .filter(OIDFederationResolveEntityProfileConfiguration.class::isInstance)
+ .map(OIDFederationResolveEntityProfileConfiguration.class::cast)
+ .map(config -> config.getCachedSuccessResponseLifetime(input))
+ .orElse(null);
+ }
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/decoding/impl/ResolveEntityRequestDecoder.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/decoding/impl/ResolveEntityRequestDecoder.java
new file mode 100644
index 00000000..11668fac
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/decoding/impl/ResolveEntityRequestDecoder.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.decoding.impl;
+
+import java.io.IOException;
+import java.net.URI;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.decoder.MessageDecodingException;
+import org.slf4j.Logger;
+
+import com.google.common.base.MoreObjects;
+import com.nimbusds.oauth2.sdk.http.HTTPRequest;
+import com.nimbusds.oauth2.sdk.http.JakartaServletUtils;
+
+import jakarta.servlet.http.HttpServletRequest;
+import net.shibboleth.idp.plugin.oidc.op.decoding.impl.RequestUtil;
+import net.shibboleth.idp.plugin.oidc.op.oauth2.decoding.impl.BaseOAuth2RequestDecoder;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.messaging.impl.ResolveEntityRequest;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * Message decoder decoding OpenID Federation Resolve Entity request {@link ResolveEntityRequest}.
+ *
+ * @since 4.3.0
+ */
+public class ResolveEntityRequestDecoder extends BaseOAuth2RequestDecoder<ResolveEntityRequest> {
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(ResolveEntityRequestDecoder.class);
+
+ /** {@inheritDoc} */
+ @Override
+ protected ResolveEntityRequest parseMessage() throws MessageDecodingException {
+ final HttpServletRequest request = getHttpServletRequest();
+ assert request != null;
+ if (!"GET".equalsIgnoreCase(request.getMethod())) {
+ throw new MessageDecodingException("This message decoder only supports the HTTP GET method");
+ }
+ try {
+ final HTTPRequest httpRequest = JakartaServletUtils.createHTTPRequest(request);
+ getProtocolMessageLogger().trace("Inbound request {}", RequestUtil.toString(httpRequest, null));
+ final URI uri = httpRequest.getURI();
+ if (uri == null) {
+ throw new MessageDecodingException("Could not parse request URI");
+ }
+ final Map<String, List<String>> parameters = httpRequest.getQueryParameters();
+ final String subject = Optional.ofNullable(parameters.get("sub"))
+ .filter(Objects::nonNull)
+ .filter(list -> list.size() == 1)
+ .map(list -> list.get(0))
+ .orElse(null);
+ if (subject == null) {
+ throw new MessageDecodingException("No single sub value in the request");
+ }
+ final List<String> trustAnchors = Optional.ofNullable(parameters.get("trust_anchors"))
+ .filter(Objects::nonNull)
+ .filter(list -> list.size() > 0)
+ .orElse(null);
+ if (trustAnchors == null) {
+ throw new MessageDecodingException("No trust_anchors included in the request");
+ }
+ final String entityType = Optional.ofNullable(parameters.get("entity_type"))
+ .filter(Objects::nonNull)
+ .filter(list -> list.size() == 1)
+ .map(list -> list.get(0))
+ .orElse(null);
+ if (entityType == null) {
+ throw new MessageDecodingException("No single entity_type value in the request");
+ }
+ return new ResolveEntityRequest(uri, subject, trustAnchors, entityType);
+ } catch (final IOException e) {
+ log.error("Could not create HTTP request from the request", e);
+ throw new MessageDecodingException(e);
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected String getMessageToLog(@Nullable final ResolveEntityRequest message) {
+ return message == null ? null : MoreObjects.toStringHelper(this).omitNullValues()
+ .add("subject", message.getSubject())
+ .add("trustAnchors", message.getTrustAnchors())
+ .add("entityType", message.getEntityType())
+ .add("endpointURI", getEndpointURI(message))
+ .toString();
+ }
+
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/messaging/impl/ResolveEntityRequest.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/messaging/impl/ResolveEntityRequest.java
new file mode 100644
index 00000000..79d59f05
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/messaging/impl/ResolveEntityRequest.java
@@ -0,0 +1,133 @@
+/*
+ * 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.messaging.impl;
+
+import java.net.URI;
+import java.util.List;
+
+import javax.annotation.Nonnull;
+
+import com.google.common.base.MoreObjects;
+import com.nimbusds.oauth2.sdk.Request;
+import com.nimbusds.oauth2.sdk.http.HTTPRequest;
+
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.logic.Constraint;
+
+/**
+ * Request message to the OpenID federation resolve entity API.
+ */
+public class ResolveEntityRequest implements Request {
+
+ /** The endpoint URI of the request. */
+ @Nonnull private final URI endpointUri;
+
+ /** The requested subject. */
+ @Nonnull @NotEmpty private final String subject;
+
+ /** The requested trust anchors. */
+ @Nonnull @NotEmpty private final List<String> trustAnchors;
+
+ /** The requested entity type to resolve. */
+ @Nonnull @NotEmpty private final String entityType;
+
+ /**
+ *
+ * Constructor.
+ *
+ * @param uri endpoint URI
+ * @param sub subject
+ * @param anchors trust anchors
+ * @param type entity type
+ */
+ public ResolveEntityRequest(@Nonnull final URI uri,
+ @Nonnull @NotEmpty final String sub,
+ @Nonnull @NotEmpty final List<String> anchors,
+ @Nonnull @NotEmpty final String type) {
+ endpointUri = Constraint.isNotNull(uri, "Endpoint URI cannot be null");
+ subject = Constraint.isNotNull(sub, "Subject cannot be empty");
+ Constraint.isNotEmpty(anchors, "Trust anchors cannot be empty");
+ trustAnchors = anchors;
+ entityType = Constraint.isNotNull(type, "Entity type cannot be empty");
+ }
+
+ /**
+ * Returns the requested subject.
+ *
+ * @return The subject.
+ */
+ @Nonnull @NotEmpty public String getSubject() {
+ return subject;
+ }
+
+ /**
+ * Returns the requested trust anchor.
+ *
+ * @return The trust anchor.
+ */
+ @Nonnull @NotEmpty public List<String> getTrustAnchors() {
+ return trustAnchors;
+ }
+
+ /**
+ * Returns the requested entity type to resolve.
+ *
+ * @return The entity type to resolve.
+ */
+ @Nonnull @NotEmpty public String getEntityType() {
+ return entityType;
+ }
+
+ /** {@inheritDoc} */
+ @Override @Nonnull public URI getEndpointURI() {
+ return endpointUri;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public HTTPRequest toHTTPRequest() {
+ //TODO
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public String toString() {
+ return MoreObjects.toStringHelper(this)
+ .add("subject", getSubject())
+ .add("trustAnchors", getTrustAnchors())
+ .add("entityType", getEntityType())
+ .add("endpointURI", getEndpointURI())
+ .toString();
+ }
+
+ /** {@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 ResolveEntityRequest other = (ResolveEntityRequest) obj;
+ return endpointUri.equals(other.endpointUri) && subject.equals(other.subject) &&
+ entityType.equals(other.entityType) && trustAnchors.containsAll(other.trustAnchors) &&
+ other.trustAnchors.containsAll(trustAnchors);
+ }
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/messaging/impl/ResolveEntityResponse.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/messaging/impl/ResolveEntityResponse.java
new file mode 100644
index 00000000..73728591
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/messaging/impl/ResolveEntityResponse.java
@@ -0,0 +1,99 @@
+/*
+ * 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.messaging.impl;
+
+import javax.annotation.Nonnull;
+
+import com.nimbusds.common.contenttype.ContentType;
+import com.nimbusds.jwt.SignedJWT;
+import com.nimbusds.oauth2.sdk.ParseException;
+import com.nimbusds.oauth2.sdk.Response;
+import com.nimbusds.oauth2.sdk.http.HTTPResponse;
+
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.StringSupport;
+
+/**
+ * Response message to the OpenID federation resolve entity endpoint.
+ */
+public class ResolveEntityResponse implements Response {
+
+ /** The content type. */
+ @Nonnull public static final ContentType CONTENT_TYPE = new ContentType("application", "resolve-response+jwt");
+
+ /** The JWT included in the response. */
+ @Nonnull private final SignedJWT jwt;
+
+ /**
+ * Constructor.
+ *
+ * @param statement JWT
+ */
+ public ResolveEntityResponse(@Nonnull final SignedJWT statement) {
+ jwt = Constraint.isNotNull(statement, "Entity statement cannot be null");
+ }
+
+ /**
+ * Get the JWT included in the response.
+ *
+ * @return JWT
+ */
+ @Nonnull public SignedJWT getJWT() {
+ return jwt;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean indicatesSuccess() {
+ return true;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public HTTPResponse toHTTPResponse() {
+ final HTTPResponse httpResponse = new HTTPResponse(HTTPResponse.SC_OK);
+ httpResponse.setEntityContentType(CONTENT_TYPE);
+ httpResponse.setContent(jwt.serialize());
+ return httpResponse;
+ }
+
+ /**
+ * Parses a federation resolve entity success response from the given HTTP response.
+ *
+ * @param httpResponse the HTTP response
+ * @return resolve entity success response
+ * @throws ParseException if HTTP response could not be parsed into resolve entity response
+ */
+ @Nonnull
+ public static ResolveEntityResponse parse(@Nonnull final HTTPResponse httpResponse)
+ throws ParseException {
+
+ httpResponse.ensureStatusCode(HTTPResponse.SC_OK);
+ httpResponse.ensureEntityContentType(CONTENT_TYPE);
+ final String content = httpResponse.getContent();
+
+ if (StringSupport.trimOrNull(content) == null) {
+ throw new ParseException("Message body is empty");
+ }
+
+ try {
+ final SignedJWT jwt = SignedJWT.parse(httpResponse.getContent());
+ assert jwt != null;
+ return new ResolveEntityResponse(jwt);
+ } catch (final java.text.ParseException e) {
+ throw new ParseException(e.getMessage(), 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/DefaultEntityStatementFetchingStrategy.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultEntityStatementFetchingStrategy.java
index f6ec9424..0493ca47 100644
--- 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
@@ -42,6 +42,7 @@ import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponen
import net.shibboleth.shared.component.ComponentInitializationException;
import net.shibboleth.shared.logic.Constraint;
import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.primitive.StringSupport;
import net.shibboleth.shared.resolver.CriteriaSet;
/**
@@ -126,8 +127,8 @@ public class DefaultEntityStatementFetchingStrategy extends AbstractIdentifiable
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");
+ if (uri == null || StringSupport.trimOrNull(uri.getScheme()) == null) {
+ log.error("No valid URI could be resolved for fetching entity statement: {}", uri);
return null;
}
log.debug("Using URI {} for fetching entity statement", uri);
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultResolveEntityRequestCriteriaToIdentifierStrategy.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultResolveEntityRequestCriteriaToIdentifierStrategy.java
new file mode 100644
index 00000000..d2e77ae3
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultResolveEntityRequestCriteriaToIdentifierStrategy.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.Optional;
+import java.util.function.Function;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.ThreadSafe;
+
+import net.shibboleth.shared.resolver.CriteriaSet;
+
+/**
+ * Strategy for extracting a resolve entity request as String via {@link ResolveEntityRequestCriterion} from
+ * {@link CriteriaSet}.
+ */
+ at ThreadSafe
+public class DefaultResolveEntityRequestCriteriaToIdentifierStrategy
+ implements Function<CriteriaSet, String> {
+
+ /** {@inheritDoc} */
+ @Override @Nullable
+ public String apply(@Nullable final CriteriaSet criteria) {
+ return Optional.ofNullable(criteria)
+ .map(set -> set.get(ResolveEntityRequestCriterion.class))
+ .map(criterion -> criterion.getRequest())
+ .map(request -> request.toString())
+ .orElse(null);
+ }
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultResolveEntityResponseContainerExpirationTimeStrategy.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultResolveEntityResponseContainerExpirationTimeStrategy.java
new file mode 100644
index 00000000..4730da4d
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultResolveEntityResponseContainerExpirationTimeStrategy.java
@@ -0,0 +1,59 @@
+/*
+ * 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.time.Instant;
+import java.util.function.Function;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.ThreadSafe;
+
+import net.shibboleth.idp.plugin.oidc.op.oidfed.messaging.impl.ResolveEntityResponse;
+import net.shibboleth.oidc.metadata.cache.ExpirationTimeContext;
+
+/**
+ * Default strategy for fetching expiration time for the resolve entity response container. The expiration instant is
+ * fetched from which is before: the success response message's JWT expiration time or the instant returned by
+ * {@link ResolveEntityResponseContainer#getExpirationInstant()}.
+ */
+ at ThreadSafe
+public class DefaultResolveEntityResponseContainerExpirationTimeStrategy
+ implements Function<ExpirationTimeContext<ResolveEntityResponseContainer>, Instant> {
+
+ /** {@inheritDoc} */
+ @Nullable public Instant apply(@Nullable final ExpirationTimeContext<ResolveEntityResponseContainer> context) {
+ if (context == null) {
+ return null;
+ }
+ final Instant contextExpiration = context.getNow().plus(context.getMaxCacheDuration());
+ final ResolveEntityResponseContainer container = context.getMetadata();
+ if (container == null || container.getExpirationInstant() == null) {
+ return contextExpiration;
+ }
+ final Instant containerExpiration = container.getExpirationInstant();
+ if (container.getResponse() instanceof ResolveEntityResponse successResponse) {
+ try {
+ final Instant jwtExpiration =
+ successResponse.getJWT().getJWTClaimsSet().getExpirationTime().toInstant();
+ return jwtExpiration.isBefore(containerExpiration) ? jwtExpiration : containerExpiration;
+ } catch (final ParseException e) {
+ // ignore, use container expiration
+ }
+ }
+ return containerExpiration;
+ }
+
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultResolveEntityResponseFetchingStrategy.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultResolveEntityResponseFetchingStrategy.java
new file mode 100644
index 00000000..13270d4f
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultResolveEntityResponseFetchingStrategy.java
@@ -0,0 +1,65 @@
+/*
+ * 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 org.slf4j.Logger;
+
+import net.shibboleth.shared.annotation.constraint.ThreadSafeAfterInit;
+import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.resolver.CriteriaSet;
+
+/**
+ * Default strategy for fetching resolve entity response container via criteria.
+ */
+ at ThreadSafeAfterInit
+public class DefaultResolveEntityResponseFetchingStrategy extends AbstractIdentifiableInitializableComponent
+ implements Function<CriteriaSet, ResolveEntityResponseContainer> {
+
+ /** Class logger. */
+ @Nonnull private Logger log = LoggerFactory.getLogger(DefaultResolveEntityResponseFetchingStrategy.class);
+
+ /** {@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 ResolveEntityResponseCriterion responseCriterion = criteria.get(ResolveEntityResponseCriterion.class);
+ if (responseCriterion == null) {
+ log.debug("No response criterion given, returning null");
+ return null;
+ }
+ final ResolveEntityContainerExpirationCriterion expirationCriterion =
+ criteria.get(ResolveEntityContainerExpirationCriterion.class);
+ if (expirationCriterion == null) {
+ log.debug("No expiration criterion given, returning null");
+ return null;
+ }
+ return new ResolveEntityResponseContainer(responseCriterion.getResponse(), requestCriterion.getRequest(),
+ expirationCriterion.getExpirationInstant());
+ }
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultResolveEntityResponseIdentifierExtractionStrategy.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultResolveEntityResponseIdentifierExtractionStrategy.java
new file mode 100644
index 00000000..a5009e49
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultResolveEntityResponseIdentifierExtractionStrategy.java
@@ -0,0 +1,38 @@
+/*
+ * 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.Optional;
+import java.util.function.Function;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.ThreadSafe;
+
+/**
+ * Default strategy for extracting request identifier out of {@link ResolveEntityResponseContainer}.
+ */
+ at ThreadSafe
+public class DefaultResolveEntityResponseIdentifierExtractionStrategy
+ implements Function<ResolveEntityResponseContainer, String> {
+
+ /** {@inheritDoc} */
+ @Override @Nullable
+ public String apply(@Nullable final ResolveEntityResponseContainer responseContainer) {
+ return Optional.ofNullable(responseContainer)
+ .map(container -> container.getRequest())
+ .map(request -> request.toString())
+ .orElse(null);
+ }
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/ResolveEntityContainerExpirationCriterion.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/ResolveEntityContainerExpirationCriterion.java
new file mode 100644
index 00000000..83888cab
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/ResolveEntityContainerExpirationCriterion.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.time.Instant;
+import java.util.Objects;
+
+import javax.annotation.Nonnull;
+
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.resolver.Criterion;
+
+/**
+ * A {@link Criterion} representing expiration instant for a resolve entity container.
+ */
+public class ResolveEntityContainerExpirationCriterion implements Criterion {
+
+ /** The expiration instant. */
+ @Nonnull private final Instant instant;
+
+ /**
+ * Constructor.
+ *
+ * @param expirationInstant expiration instant, must not be null
+ */
+ public ResolveEntityContainerExpirationCriterion(@Nonnull final Instant expirationInstant) {
+ instant = Constraint.isNotNull(expirationInstant, "Expiration instant cannot be null");
+ }
+
+ /**
+ * Get the expiration instant.
+ *
+ * @return the expiration instant
+ */
+ @Nonnull public Instant getExpirationInstant() {
+ return instant;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public String toString() {
+ return "ResolveEntityContainerExpirationCriterion [instant=" + instant.toEpochMilli() + "]";
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public int hashCode() {
+ return Objects.hash(instant);
+ }
+
+ /** {@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 ResolveEntityContainerExpirationCriterion other = (ResolveEntityContainerExpirationCriterion) obj;
+ return instant.equals(other.instant);
+ }
+
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/ResolveEntityRequestCriterion.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/ResolveEntityRequestCriterion.java
new file mode 100644
index 00000000..d59a72f4
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/ResolveEntityRequestCriterion.java
@@ -0,0 +1,80 @@
+/*
+ * 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.idp.plugin.oidc.op.oidfed.messaging.impl.ResolveEntityRequest;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.resolver.Criterion;
+
+/**
+ * A {@link Criterion} representing request message to a resolve entity API.
+ */
+public class ResolveEntityRequestCriterion implements Criterion {
+
+ /** The request message. */
+ @Nonnull private final ResolveEntityRequest request;
+
+ /**
+ * Constructor.
+ *
+ * @param requestMessage request message, must not be null
+ */
+ public ResolveEntityRequestCriterion(@Nonnull final ResolveEntityRequest requestMessage) {
+ request = Constraint.isNotNull(requestMessage, "Request cannot be null");
+ }
+
+ /**
+ * Get the request message.
+ *
+ * @return the request message
+ */
+ @Nonnull public ResolveEntityRequest getRequest() {
+ return request;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public String toString() {
+ return "ResolveEntityRequestCriterion [request=" + request + "]";
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public int hashCode() {
+ return Objects.hash(request);
+ }
+
+ /** {@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 ResolveEntityRequestCriterion other = (ResolveEntityRequestCriterion) obj;
+ //TODO: proper equals-check
+ return request.equals(other.request);
+ }
+
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/ResolveEntityResponseContainer.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/ResolveEntityResponseContainer.java
new file mode 100644
index 00000000..1ae9856f
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/ResolveEntityResponseContainer.java
@@ -0,0 +1,85 @@
+/*
+ * 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.Serializable;
+import java.time.Instant;
+
+import javax.annotation.Nonnull;
+
+import com.nimbusds.oauth2.sdk.Response;
+
+import net.shibboleth.idp.plugin.oidc.op.oidfed.messaging.impl.ResolveEntityRequest;
+import net.shibboleth.shared.logic.Constraint;
+
+/**
+ * A container class for metadata caches carrying request and response message details related to Resolve Entity API.
+ */
+public class ResolveEntityResponseContainer implements Serializable {
+
+ /** Serial version UID. */
+ private static final long serialVersionUID = 756269369356865370L;
+
+ /** Response message. */
+ @Nonnull private final Response response;
+
+ /** Request message. */
+ @Nonnull private final ResolveEntityRequest request;
+
+ /** Expiration instant for this container. */
+ @Nonnull private final Instant expiration;
+
+ /**
+ * Constructor.
+ *
+ * @param responseMessage response message
+ * @param requestMessage request message
+ * @param expirationInstant expiration instant
+ */
+ public ResolveEntityResponseContainer(@Nonnull final Response responseMessage,
+ @Nonnull final ResolveEntityRequest requestMessage, @Nonnull final Instant expirationInstant) {
+ response = Constraint.isNotNull(responseMessage, "Response message cannot be null");
+ request = Constraint.isNotNull(requestMessage, "Request message cannot be null");
+ expiration = Constraint.isNotNull(expirationInstant, "Expiration instant cannot be null");
+ }
+
+ /**
+ * Get response message.
+ *
+ * @return response message
+ */
+ public Response getResponse() {
+ return response;
+ }
+
+ /**
+ * Get request message.
+ *
+ * @return request message
+ */
+ public ResolveEntityRequest getRequest() {
+ return request;
+ }
+
+ /**
+ * Get expiration instant.
+ *
+ * @return expiration instant
+ */
+ public Instant getExpirationInstant() {
+ return expiration;
+ }
+
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/ResolveEntityResponseCriterion.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/ResolveEntityResponseCriterion.java
new file mode 100644
index 00000000..43691690
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/ResolveEntityResponseCriterion.java
@@ -0,0 +1,80 @@
+/*
+ * 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.oauth2.sdk.Response;
+
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.resolver.Criterion;
+
+/**
+ * A {@link Criterion} representing response message to a resolve entity request.
+ */
+public class ResolveEntityResponseCriterion implements Criterion {
+
+ /** The response message. */
+ @Nonnull private final Response response;
+
+ /**
+ * Constructor.
+ *
+ * @param responseMessage response message, must not be null
+ */
+ public ResolveEntityResponseCriterion(@Nonnull final Response responseMessage) {
+ response = Constraint.isNotNull(responseMessage, "Response cannot be null");
+ }
+
+ /**
+ * Get the response message.
+ *
+ * @return the response message
+ */
+ @Nonnull public Response getResponse() {
+ return response;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public String toString() {
+ return "ResolveEntityResponseCriterion [response=" + response + "]";
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public int hashCode() {
+ return Objects.hash(response);
+ }
+
+ /** {@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 ResolveEntityResponseCriterion other = (ResolveEntityResponseCriterion) obj;
+ return response.equals(other.response);
+ }
+
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/TrustAnchorEntityIDsCriterion.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/TrustAnchorEntityIDsCriterion.java
new file mode 100644
index 00000000..97934e88
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/TrustAnchorEntityIDsCriterion.java
@@ -0,0 +1,80 @@
+/*
+ * 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.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 trust anchor entity IDs in resolve entity request.
+ */
+public class TrustAnchorEntityIDsCriterion implements Criterion {
+
+ /** The entity ID values. */
+ @Nonnull final List<String> values;
+
+ /**
+ * Constructor.
+ *
+ * @param entityIds the entity ID values, must not be null nor empty
+ */
+ public TrustAnchorEntityIDsCriterion(@Nonnull @NotEmpty final List<String> entityIds) {
+ Constraint.isNotEmpty(entityIds, "Entity IDs cannot be null nor empty");
+ values = entityIds;
+ }
+
+ /**
+ * Get the entity ID values.
+ *
+ * @return the entity ID values
+ */
+ @Nonnull @NotEmpty public List<String> getValues() {
+ return values;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public String toString() {
+ return "TrustAnchorEntityIDsCriterion [values=" + values + "]";
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public int hashCode() {
+ return Objects.hash(values);
+ }
+
+ /** {@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 TrustAnchorEntityIDsCriterion other = (TrustAnchorEntityIDsCriterion) obj;
+ return values.containsAll(other.values) && other.values.containsAll(values);
+ }
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/BuildResolveEntityErrorResponseFromEvent.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/BuildResolveEntityErrorResponseFromEvent.java
new file mode 100644
index 00000000..53006b02
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/BuildResolveEntityErrorResponseFromEvent.java
@@ -0,0 +1,157 @@
+/*
+ * 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.time.Duration;
+import java.time.Instant;
+import java.util.List;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.context.EventContext;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
+import org.slf4j.Logger;
+
+import com.nimbusds.oauth2.sdk.ErrorObject;
+
+import net.shibboleth.idp.plugin.oidc.op.oidfed.config.ResolveEntityCachedErrorResponseLifetimeLookupFunction;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.messaging.impl.ResolveEntityRequest;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.ResolveEntityContainerExpirationCriterion;
+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.ResolveEntityResponseCriterion;
+import net.shibboleth.idp.plugin.oidc.op.profile.impl.AbstractBuildErrorResponseFromEvent;
+import net.shibboleth.oidc.metadata.cache.MetadataCache;
+import net.shibboleth.oidc.metadata.cache.MetadataCacheException;
+import net.shibboleth.oidc.profile.messaging.JSONErrorResponse;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.resolver.CriteriaSet;
+
+/**
+ * This action reads an event from the configured {@link EventContext} lookup strategy, constructs a JSON error response
+ * message and attaches it as the outbound message. If {@link RelyingPartyResolveEntityContext} is found, it's exploited
+ * for storing the response message in the configured {@link this#responseCache}.
+ */
+public class BuildResolveEntityErrorResponseFromEvent extends AbstractBuildErrorResponseFromEvent<JSONErrorResponse> {
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(BuildResolveEntityErrorResponseFromEvent.class);
+
+ /** Metadata cache for cached response containers. */
+ @NonnullAfterInit private MetadataCache<ResolveEntityResponseContainer> responseCache;
+
+ /** Strategy used to locate the lifetime for the cached response record. */
+ @Nonnull private Function<ProfileRequestContext,Duration> cachedResponseLifetimeLookupStrategy;
+
+ /** Strategy used to locate the resolve entity context. */
+ @Nonnull
+ private Function<ProfileRequestContext, RelyingPartyResolveEntityContext> resolveEntityContextLookupStrategy;
+
+ /**
+ * Constructor.
+ */
+ public BuildResolveEntityErrorResponseFromEvent() {
+ cachedResponseLifetimeLookupStrategy = new ResolveEntityCachedErrorResponseLifetimeLookupFunction();
+ final Function<ProfileRequestContext, RelyingPartyResolveEntityContext> recls =
+ new ChildContextLookup<>(RelyingPartyResolveEntityContext.class).compose(
+ new InboundMessageContextLookup());
+ assert recls != null;
+ resolveEntityContextLookupStrategy = recls;
+
+ }
+
+ /**
+ * Set the metadata cache for cached response containers.
+ *
+ * @param cache What to set.
+ */
+ public void setResponseCache(@Nonnull final MetadataCache<ResolveEntityResponseContainer> cache) {
+ checkSetterPreconditions();
+ responseCache = Constraint.isNotNull(cache, "Response metadata cache cannot be null");
+ }
+
+ /**
+ * Set the strategy used to locate the lifetime for the cached response record.
+ *
+ * @param strategy What to set.
+ */
+ public void setCachedResponseLifetimeLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext, Duration> strategy) {
+ checkSetterPreconditions();
+ cachedResponseLifetimeLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null");
+ }
+
+ /**
+ * Set the strategy used to locate the resolve entity context
+ *
+ * @param strategy What to set.
+ */
+ public void setResolveEntityContextLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext,RelyingPartyResolveEntityContext> strategy) {
+ checkSetterPreconditions();
+ resolveEntityContextLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null!");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+
+ if (responseCache == null) {
+ throw new ComponentInitializationException("Response metadata cache cannot be null");
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected JSONErrorResponse buildErrorResponse(final ErrorObject error,
+ final ProfileRequestContext profileRequestContext) {
+ final JSONErrorResponse response = new JSONErrorResponse(error);
+ final RelyingPartyResolveEntityContext resolveEntityContext =
+ resolveEntityContextLookupStrategy.apply(profileRequestContext);
+
+ if (resolveEntityContext != null && resolveEntityContext.getValidatedRequest() != null) {
+ final ResolveEntityResponseCriterion responseCriterion = new ResolveEntityResponseCriterion(response);
+ final Duration cachedResponseLifetime = cachedResponseLifetimeLookupStrategy.apply(profileRequestContext);
+ final Instant expiration = Instant.now().plus(cachedResponseLifetime);
+ assert expiration != null;
+ final ResolveEntityContainerExpirationCriterion expirationCriterion =
+ new ResolveEntityContainerExpirationCriterion(expiration);
+ final ResolveEntityRequest validatedRequest = resolveEntityContext.getValidatedRequest();
+ assert validatedRequest != null;
+ final ResolveEntityRequestCriterion requestCriterion = new ResolveEntityRequestCriterion(validatedRequest);
+ final CriteriaSet criteria = new CriteriaSet(requestCriterion, responseCriterion, expirationCriterion);
+ try {
+ final List<ResolveEntityResponseContainer> result = responseCache.get(criteria);
+ if (result.size() != 1) {
+ log.error("{} Unexpected result (size={}) when storing response record into the metadata cache",
+ getLogPrefix(), result.size());
+ } else {
+ log.debug("{} Response stored into the cache", getLogPrefix());
+ }
+ } catch (final MetadataCacheException e) {
+ log.error("{} Could not store the response record into tht metadata cache", getLogPrefix(), e);
+ }
+ }
+
+ return response;
+ }
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/BuildResolveEntityResponse.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/BuildResolveEntityResponse.java
new file mode 100644
index 00000000..bf5a7b2b
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/BuildResolveEntityResponse.java
@@ -0,0 +1,153 @@
+/*
+ * 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.time.Instant;
+import java.util.Date;
+import java.util.List;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.action.ActionSupport;
+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.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformation;
+
+import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.collection.Pair;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * An action that uses the information from {@link RelyingPartyTrustChainContext} for creating a new JWT to be used for
+ * creating a response to OpenID Federation Resolve Entity API.
+ */
+public class BuildResolveEntityResponse extends AbstractBuildEntityStatementAction {
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(BuildResolveEntityResponse.class);
+
+ /** Strategy used to lookup the trust chain context. */
+ @Nonnull private Function<ProfileRequestContext, RelyingPartyTrustChainContext> trustChainContextLookupStrategy;
+
+ /** Trust chain context to operate on. */
+ @NonnullBeforeExec private RelyingPartyTrustChainContext trustChainContext;
+
+ /** Constructor. */
+ public BuildResolveEntityResponse() {
+ final Function<ProfileRequestContext, RelyingPartyTrustChainContext> tcls =
+ new ChildContextLookup<>(RelyingPartyTrustChainContext.class).compose(
+ new InboundMessageContextLookup());
+ assert tcls != null;
+ trustChainContextLookupStrategy = tcls;
+ }
+
+ /**
+ * Set the strategy used to lookup the trust chain context.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setTrustChainContextLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext, RelyingPartyTrustChainContext> strategy) {
+ checkSetterPreconditions();
+ trustChainContextLookupStrategy =
+ Constraint.isNotNull(strategy, "TrustChainContextLookupStrategy cannot be null");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+ if (!super.doPreExecute(profileRequestContext)) {
+ return false;
+ }
+
+ trustChainContext = trustChainContextLookupStrategy.apply(profileRequestContext);
+ if (trustChainContext == null) {
+ log.error("{} Unable to locate trust chain context", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
+ return false;
+ }
+
+ return true;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected boolean populateClaimsSetBuilder(@Nonnull final JWTClaimsSet.Builder builder,
+ @Nonnull final ProfileRequestContext profileRequestContext) {
+ final Pair<List<EntityStatement>,OIDCClientInformation> selectedTrustChain =
+ trustChainContext.getSelectedTrustChain();
+ if (selectedTrustChain == null) {
+ log.debug("{} No selected trust chain found form the context", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return false;
+ }
+ final OIDCClientInformation clientInformation = selectedTrustChain.getSecond();
+ if (clientInformation == null) {
+ log.debug("{} No client information set in the trust chain context", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return false;
+ }
+ builder.claim("metadata",
+ CollectionSupport.singletonMap("openid_relying_party", clientInformation.toJSONObject()));
+
+ final List<EntityStatement> trustChain = selectedTrustChain.getFirst();
+ if (trustChain == null || trustChain.isEmpty()) {
+ log.debug("{} No selected trust chain set in the trust chain context", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return false;
+ }
+ builder.claim("trust_chain", trustChain.stream()
+ .map(statement -> statement.getSignedStatement().serialize())
+ .toList());
+
+ final Instant expirationTime = resolveTrustChainExpiration(trustChain);
+ if (expirationTime == null) {
+ log.error("{} Coud not resolve expiration time from the selected trust chain context", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
+ return false;
+ }
+ builder.expirationTime(Date.from(expirationTime));
+
+
+ return true;
+ }
+
+ /**
+ * Resolve expiration time for the given trust chain.
+ *
+ * @param trustChain trust chain
+ * @return expiration time
+ */
+ @Nullable private Instant resolveTrustChainExpiration(@Nonnull final List<EntityStatement> trustChain) {
+ Instant metadataExpiration = null;
+ for (final EntityStatement statement : trustChain) {
+ final Instant statementExpiration = statement.getClaimsSet().getExpirationTime().toInstant();
+ metadataExpiration = metadataExpiration == null ? statementExpiration :
+ statementExpiration.isBefore(metadataExpiration) ? statementExpiration : metadataExpiration;
+ }
+ return metadataExpiration;
+ }
+
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/FormOutboundResolveEntityResponse.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/FormOutboundResolveEntityResponse.java
new file mode 100644
index 00000000..4ceb9033
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/FormOutboundResolveEntityResponse.java
@@ -0,0 +1,230 @@
+/*
+ * 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.time.Duration;
+import java.time.Instant;
+import java.util.List;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
+import org.opensaml.profile.context.navigate.OutboundMessageContextLookup;
+import org.slf4j.Logger;
+
+import com.nimbusds.jwt.SignedJWT;
+import com.nimbusds.oauth2.sdk.Response;
+import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
+
+import net.shibboleth.idp.plugin.oidc.op.oidfed.config.ResolveEntityCachedSuccessResponseLifetimeLookupFunction;
+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.ResolveEntityContainerExpirationCriterion;
+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.ResolveEntityResponseCriterion;
+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.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.resolver.CriteriaSet;
+
+/**
+ * This action builds a response for the OpenID federation resolve entity request. The response contains an
+ * {@link EntityStatement}. The response is put in the {@link this#responseCache} using the lifetime resolved via
+ * {@link this#cachedResponseLifetimeLookupStrategy}.
+ *
+ * @since 4.3.0
+ */
+public class FormOutboundResolveEntityResponse extends AbstractProfileAction {
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(FormOutboundResolveEntityResponse.class);
+
+ /** Metadata cache for cached response containers. */
+ @NonnullAfterInit private MetadataCache<ResolveEntityResponseContainer> responseCache;
+
+ /** Strategy used to locate the resolve entity context. */
+ @Nonnull
+ private Function<ProfileRequestContext, RelyingPartyResolveEntityContext> resolveEntityContextLookupStrategy;
+
+ /** Strategy used to locate the subcontext to hold the statement. */
+ @Nonnull private Function<ProfileRequestContext,EntityStatementContext> entityStatementContextLookupStrategy;
+
+ /** Strategy used to locate the lifetime for the cached response record. */
+ @Nonnull private Function<ProfileRequestContext,Duration> cachedResponseLifetimeLookupStrategy;
+
+ /** JWT used to build entity statement. */
+ @NonnullBeforeExec private SignedJWT jwt;
+
+ /** The resolve entity context to operate on. */
+ @NonnullBeforeExec private RelyingPartyResolveEntityContext resolveEntityContext;
+
+ /**
+ * Constructor.
+ */
+ public FormOutboundResolveEntityResponse() {
+ final Function<ProfileRequestContext,EntityStatementContext> escls =
+ new ChildContextLookup<>(EntityStatementContext.class).compose(
+ new OutboundMessageContextLookup());
+ assert escls != null;
+ entityStatementContextLookupStrategy = escls;
+ final Function<ProfileRequestContext, RelyingPartyResolveEntityContext> recls =
+ new ChildContextLookup<>(RelyingPartyResolveEntityContext.class).compose(
+ new InboundMessageContextLookup());
+ assert recls != null;
+ resolveEntityContextLookupStrategy = recls;
+ cachedResponseLifetimeLookupStrategy = new ResolveEntityCachedSuccessResponseLifetimeLookupFunction();
+ }
+
+ /**
+ * Set the strategy used to locate the subcontext to hold the statement
+ *
+ * @param strategy What to set.
+ */
+ public void setEntityStatementContextLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext,EntityStatementContext> strategy) {
+ checkSetterPreconditions();
+ entityStatementContextLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null!");
+ }
+
+ /**
+ * Set the strategy used to locate the resolve entity context
+ *
+ * @param strategy What to set.
+ */
+ public void setResolveEntityContextLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext,RelyingPartyResolveEntityContext> strategy) {
+ checkSetterPreconditions();
+ resolveEntityContextLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null!");
+ }
+
+ /**
+ * Set the metadata cache for cached response containers.
+ *
+ * @param cache What to set.
+ */
+ public void setResponseCache(@Nonnull final MetadataCache<ResolveEntityResponseContainer> cache) {
+ checkSetterPreconditions();
+ responseCache = Constraint.isNotNull(cache, "Response metadata cache cannot be null");
+ }
+
+ /**
+ * Set the strategy used to locate the lifetime for the cached response record.
+ *
+ * @param strategy What to set.
+ */
+ public void setCachedResponseLifetimeLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext, Duration> strategy) {
+ checkSetterPreconditions();
+ cachedResponseLifetimeLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+
+ if (responseCache == null) {
+ throw new ComponentInitializationException("Response metadata cache cannot be null");
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+ if (!super.doPreExecute(profileRequestContext)) {
+ return false;
+ }
+ resolveEntityContext = resolveEntityContextLookupStrategy.apply(profileRequestContext);
+ if (resolveEntityContext == null) {
+ log.error("{} Could not resolve resolve entity context", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return false;
+ }
+
+ return true;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+ final Response cachedResponse = resolveEntityContext.getCachedResponse();
+ if (cachedResponse != null) {
+ log.debug("{} Cached response found, storing in to the outbound message context", getLogPrefix());
+ profileRequestContext.ensureOutboundMessageContext().setMessage(cachedResponse);
+ return;
+ }
+ log.debug("{} No cached response found, resolving the response JWT from the context", getLogPrefix());
+ final EntityStatementContext entityStatementContext =
+ entityStatementContextLookupStrategy.apply(profileRequestContext);
+ if (entityStatementContext == null) {
+ log.error("{} Could not resolve entity statement context", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return;
+ }
+ if (entityStatementContext.getJWT() instanceof SignedJWT signedJwt) {
+ jwt = signedJwt;
+ } else {
+ log.error("{} No signed JWT found from the entity statement context", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return;
+ }
+
+ assert jwt != null;
+ final ResolveEntityResponse response = new ResolveEntityResponse(jwt);
+ final ResolveEntityResponseCriterion responseCriterion = new ResolveEntityResponseCriterion(response);
+ final ResolveEntityRequest validatedRequest = resolveEntityContext.getValidatedRequest();
+ final Duration lifetime = cachedResponseLifetimeLookupStrategy.apply(profileRequestContext);
+ if (lifetime == null) {
+ log.error("{} Could not resolve lifetime for the cached response record", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return;
+ }
+ final Instant expiration = Instant.now().plus(lifetime);
+ assert expiration != null;
+ final ResolveEntityContainerExpirationCriterion expirationCriterion =
+ new ResolveEntityContainerExpirationCriterion(expiration);
+ if (validatedRequest == null) {
+ log.error("{} No validated request found from the resolve entity context", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return;
+ }
+ final ResolveEntityRequestCriterion requestCriterion = new ResolveEntityRequestCriterion(validatedRequest);
+ final CriteriaSet criteria = new CriteriaSet(requestCriterion, responseCriterion, expirationCriterion);
+ try {
+ final List<ResolveEntityResponseContainer> result = responseCache.get(criteria);
+ if (result.size() != 1) {
+ log.error("{} Unexpected result (size={}) when storing response record into the metadata cache",
+ getLogPrefix(), result.size());
+ } else {
+ log.debug("{} Response stored into the cache", getLogPrefix());
+ }
+ } catch (final MetadataCacheException e) {
+ log.error("{} Could not store the response record into tht metadata cache", getLogPrefix(), e);
+ }
+ profileRequestContext.ensureOutboundMessageContext().setMessage(response);
+ }
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/LookupCachedResolveEntityRespomse.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/LookupCachedResolveEntityRespomse.java
new file mode 100644
index 00000000..de2ebfbc
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/LookupCachedResolveEntityRespomse.java
@@ -0,0 +1,151 @@
+/*
+ * 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.util.List;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.plugin.oidc.op.oidfed.messaging.impl.ResolveEntityRequest;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.ResolveEntityRequestCriterion;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.ResolveEntityResponseContainer;
+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.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.resolver.CriteriaSet;
+
+/**
+ * Lookup if a cached response already exists for the validated resolve entity API request. If yes, the response is
+ * stored into {@link RelyingPartyResolveEntityContext} and a corresponding event ID is published.
+ *
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#INVALID_PROFILE_CTX}
+ * @event {@link OidFederationEventIds#CACHED_RESOLVE_ENTITY_RESPONSE}
+ *
+ * @since 4.3.0
+ */
+public class LookupCachedResolveEntityRespomse extends AbstractProfileAction {
+
+ /** Class logger. */
+ @Nonnull private Logger log = LoggerFactory.getLogger(LookupCachedResolveEntityRespomse.class);
+
+ /** Strategy used to locate the resolve entity context. */
+ @Nonnull
+ private Function<ProfileRequestContext, RelyingPartyResolveEntityContext> resolveEntityContextLookupStrategy;
+
+ /** Metadata cache for cached response containers. */
+ @NonnullAfterInit private MetadataCache<ResolveEntityResponseContainer> responseCache;
+
+ /** Trust chain context to operate on. */
+ @NonnullBeforeExec private RelyingPartyResolveEntityContext resolveEntityContext;
+
+ /** Request message to operate on. */
+ @NonnullBeforeExec private ResolveEntityRequest validatedRequest;
+
+ /**
+ * Constructor.
+ */
+ public LookupCachedResolveEntityRespomse() {
+ final Function<ProfileRequestContext, RelyingPartyResolveEntityContext> recls =
+ new ChildContextLookup<>(RelyingPartyResolveEntityContext.class).compose(
+ new InboundMessageContextLookup());
+ assert recls != null;
+ resolveEntityContextLookupStrategy = recls;
+ }
+
+ /**
+ * Set the strategy used to locate the resolve entity context
+ *
+ * @param strategy What to set.
+ */
+ public void setResolveEntityContextLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext,RelyingPartyResolveEntityContext> strategy) {
+ checkSetterPreconditions();
+ resolveEntityContextLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null!");
+ }
+
+ /**
+ * Set the metadata cache for cached response containers.
+ *
+ * @param cache What to set.
+ */
+ public void setResponseCache(@Nonnull final MetadataCache<ResolveEntityResponseContainer> cache) {
+ checkSetterPreconditions();
+ responseCache = Constraint.isNotNull(cache, "Response metadata cache cannot be null");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+ if (responseCache == null) {
+ throw new ComponentInitializationException("Response metadata cache cannot be null");
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+ if (!super.doPreExecute(profileRequestContext)) {
+ return false;
+ }
+
+ resolveEntityContext = resolveEntityContextLookupStrategy.apply(profileRequestContext);
+ if (resolveEntityContext == null || resolveEntityContext.getValidatedRequest() == null) {
+ log.error("{} Could not resolve validated resolve entity request", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return false;
+ }
+ validatedRequest = resolveEntityContext.getValidatedRequest();
+
+ return true;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+ assert validatedRequest != null;
+ final ResolveEntityRequestCriterion requestCriterion = new ResolveEntityRequestCriterion(validatedRequest);
+ final CriteriaSet criteria = new CriteriaSet(requestCriterion);
+ try {
+ final List<ResolveEntityResponseContainer> result = responseCache.get(criteria);
+ if (result.size() != 1) {
+ log.debug("{} No cached response record found from the metadata cache", getLogPrefix(), result.size());
+ } else {
+ final ResolveEntityResponseContainer cachedResponse = result.get(0);
+ resolveEntityContext.setCachedResponse(cachedResponse.getResponse());
+ log.debug("{} Response found from the cache, publishing event {}", getLogPrefix(),
+ OidFederationEventIds.CACHED_RESOLVE_ENTITY_RESPONSE);
+ ActionSupport.buildEvent(profileRequestContext, OidFederationEventIds.CACHED_RESOLVE_ENTITY_RESPONSE);
+ return;
+ }
+ } catch (final MetadataCacheException e) {
+ log.error("{} Could not fetch response record from the metadata cache", getLogPrefix(), e);
+ }
+ }
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/OidFederationEventIds.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/OidFederationEventIds.java
new file mode 100644
index 00000000..a23864ce
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/OidFederationEventIds.java
@@ -0,0 +1,46 @@
+/*
+ * 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 javax.annotation.Nonnull;
+
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+
+/**
+ * OpenID Federation -specific constants to use for {@link org.opensaml.profile.action.ProfileAction}
+ * {@link org.opensaml.profile.context.EventContext}s.
+ */
+public class OidFederationEventIds {
+
+ /**
+ * ID of event returned if a flow wishes to indicate that another trust chain should be selected instead
+ */
+ @Nonnull @NotEmpty public static final String RESELECT_TRUST_CHAIN = "ReselectTrustChain";
+
+ /**
+ * ID of event returned if cached resolve entity response was found and set to the context.
+ */
+ @Nonnull @NotEmpty public static final String CACHED_RESOLVE_ENTITY_RESPONSE = "CachedResolveEntityResponseFound";
+
+ /**
+ * ID of event returned if the given trust anchor is invalid.
+ */
+ @Nonnull @NotEmpty public static final String INVALID_TRUST_ANCHOR = "InvalidTrustAnchor";
+
+ /**
+ * ID of event returned if the given subject is invalid.
+ */
+ @Nonnull @NotEmpty public static final String INVALID_SUBJECT = "InvalidSubject";
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/RelyingPartyResolveEntityContext.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/RelyingPartyResolveEntityContext.java
new file mode 100644
index 00000000..0a86ab99
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/RelyingPartyResolveEntityContext.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.profile.impl;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.BaseContext;
+
+import com.nimbusds.oauth2.sdk.Response;
+
+import net.shibboleth.idp.plugin.oidc.op.oidfed.messaging.impl.ResolveEntityRequest;
+
+/**
+ * Subcontext carrying information for resolve entity related to a relying party.
+ *
+ * @since 4.3.0
+ */
+public final class RelyingPartyResolveEntityContext extends BaseContext {
+
+ /** Validated (possibly modified) resolve entity request. */
+ @Nullable private ResolveEntityRequest validatedRequest;
+
+ /** Cached response message. */
+ @Nullable private Response cachedResponse;
+
+ /**
+ * Get the validated (possibly modified) resolve entity request.
+ *
+ * @return the validated request
+ */
+ @Nullable public ResolveEntityRequest getValidatedRequest() {
+ return validatedRequest;
+ }
+
+ /**
+ * Set the the validated (possibly modified) resolve entity request.
+ *
+ * @param request the validated request
+ * @return this context
+ */
+ @Nonnull public RelyingPartyResolveEntityContext setValidatedRequest(
+ @Nullable final ResolveEntityRequest request) {
+ validatedRequest = request;
+ return this;
+ }
+
+ /**
+ * Get the cached response message.
+ *
+ * @return the cached response
+ */
+ @Nullable public Response getCachedResponse() {
+ return cachedResponse;
+ }
+
+ /**
+ * Set the cached response message.
+ *
+ * @param response cached response
+ * @return this context
+ */
+ @Nonnull public RelyingPartyResolveEntityContext setCachedResponse(@Nullable final Response response) {
+ cachedResponse = response;
+ return this;
+ }
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/SelectTrustChain.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/SelectTrustChain.java
index cec81961..5cba50d4 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/SelectTrustChain.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/SelectTrustChain.java
@@ -137,8 +137,7 @@ public class SelectTrustChain extends AbstractProfileAction {
trustChainContext = trustChainContextLookupStrategy.apply(profileRequestContext);
if (trustChainContext == null || trustChainContext.getPolicyCompliantTrustChains() == null) {
- log.error("{} Unable to locate policy-compliant trust chains", getLogPrefix());
- ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
+ log.debug("{} Unable to locate policy-compliant trust chains, nothing to do", getLogPrefix());
return false;
}
return true;
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ValidateResolveEntityRequest.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ValidateResolveEntityRequest.java
new file mode 100644
index 00000000..02112173
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ValidateResolveEntityRequest.java
@@ -0,0 +1,185 @@
+/*
+ * 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.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.plugin.oidc.op.oidfed.messaging.impl.ResolveEntityRequest;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.LocalKeyContainer;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.SubjectEntityIDCriterion;
+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.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.primitive.StringSupport;
+import net.shibboleth.shared.resolver.CriteriaSet;
+
+/**
+ * Validates the resolve entity request against the profile configuration and stores the validated (possibly modified)
+ * request into {@link RelyingPartyResolveEntityContext#setValidatedRequest(ResolveEntityRequest)}.
+ *
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#INVALID_MSG_CTX}
+ *
+ * @since 4.3.0
+ */
+public class ValidateResolveEntityRequest extends AbstractProfileAction {
+
+ /** Class logger. */
+ @Nonnull private Logger log = LoggerFactory.getLogger(ValidateResolveEntityRequest.class);
+
+ /** Strategy used to create the resolve entity context. */
+ @Nonnull
+ private Function<ProfileRequestContext, RelyingPartyResolveEntityContext> resolveEntityContextCreationStrategy;
+
+ /** Cache containing local copies of trusted trust anchor keys. */
+ @NonnullAfterInit private MetadataCache<Map<String, LocalKeyContainer>> localTrustAnchorsCache;
+
+ /** Request message to operate on. */
+ @NonnullBeforeExec private ResolveEntityRequest requestMessage;
+
+ /** Trust chain context to operate on. */
+ @NonnullBeforeExec private RelyingPartyResolveEntityContext resolveEntityContext;
+
+ /**
+ * Constructor.
+ */
+ public ValidateResolveEntityRequest() {
+ final Function<ProfileRequestContext, RelyingPartyResolveEntityContext> reccs =
+ new ChildContextLookup<>(RelyingPartyResolveEntityContext.class, true).compose(
+ new InboundMessageContextLookup());
+ assert reccs != null;
+ resolveEntityContextCreationStrategy = reccs;
+ }
+
+ /**
+ * Set the strategy used to return or create the resolve entity context.
+ *
+ * @param strategy creation strategy
+ */
+ public void setResolveEntityContextCreationStrategy(
+ @Nonnull final Function<ProfileRequestContext, RelyingPartyResolveEntityContext> strategy) {
+ checkSetterPreconditions();
+ resolveEntityContextCreationStrategy = Constraint.isNotNull(strategy,
+ "RelyingPartyResolveEntityContext creation strategy cannot be null");
+ }
+
+ /**
+ * Set the strategy used to lookup the trust chain context.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setLocalTrustAnchorsCache(
+ @Nonnull final MetadataCache<Map<String, LocalKeyContainer>> cache) {
+ checkSetterPreconditions();
+ localTrustAnchorsCache =
+ Constraint.isNotNull(cache, "LocalTrustAnchorsCache cannot be null");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+ if (localTrustAnchorsCache == null) {
+ throw new ComponentInitializationException("LocalTrustAnchorsCache cannot be null");
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+ if (!super.doPreExecute(profileRequestContext)) {
+ return false;
+ }
+
+ requestMessage = Optional.ofNullable(profileRequestContext.getInboundMessageContext())
+ .map(messageContext -> messageContext.getMessage())
+ .filter(ResolveEntityRequest.class::isInstance)
+ .map(ResolveEntityRequest.class::cast)
+ .orElse(null);
+ if (requestMessage == null) {
+ log.error("{} Unable to fetch the request message to operate on", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return false;
+ }
+
+ resolveEntityContext = resolveEntityContextCreationStrategy.apply(profileRequestContext);
+ if (resolveEntityContext == null) {
+ log.error("{} Unable to create resolve entity context", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return false;
+ }
+ return true;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+ final List<String> validatedAnchors = requestMessage.getTrustAnchors().stream()
+ .filter(anchor -> isLocallyTrusted(anchor))
+ .toList();
+ if (validatedAnchors.isEmpty()) {
+ log.info("{} No locally trusted anchors left after filtering", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, OidFederationEventIds.INVALID_TRUST_ANCHOR);
+ return;
+ }
+ log.debug("{} The following trust anchors were validated: {}", getLogPrefix(), validatedAnchors);
+ resolveEntityContext.setValidatedRequest(
+ new ResolveEntityRequest(requestMessage.getEndpointURI(), requestMessage.getSubject(),
+ validatedAnchors, requestMessage.getEntityType()));
+ }
+
+ /**
+ * Verifies whether the given trust anchor candidate is locally trusted.
+ *
+ * @param candidate the trust anchor candidate
+ * @return true if locally trusted, false otherwise
+ */
+ protected boolean isLocallyTrusted(@Nullable final String candidate) {
+ if (StringSupport.trimOrNull(candidate) == null) {
+ return false;
+ }
+ assert candidate != null;
+ final SubjectEntityIDCriterion criterion = new SubjectEntityIDCriterion(candidate);
+ try {
+ final List<Map<String,LocalKeyContainer>> result = localTrustAnchorsCache.get(new CriteriaSet(criterion));
+ if (result == null || result.isEmpty() || result.get(0).isEmpty()) {
+ log.debug("{} No locally trusted keys found for {}", getLogPrefix(), candidate);
+ return false;
+ }
+ return result.get(0).containsKey(candidate);
+ } catch (final MetadataCacheException e) {
+ log.error("{} Could not fetch value for {} from the metadata cache", getLogPrefix(), candidate, e);
+ }
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ValidateSelectedTrustChain.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ValidateSelectedTrustChain.java
new file mode 100644
index 00000000..88e63c4a
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ValidateSelectedTrustChain.java
@@ -0,0 +1,224 @@
+/*
+ * 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.util.ArrayList;
+import java.util.List;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+
+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.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformation;
+
+import net.shibboleth.idp.plugin.oidc.op.oidfed.messaging.impl.ResolveEntityRequest;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.SubjectEntityIDCriterion;
+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.primitive.LoggerFactory;
+import net.shibboleth.shared.resolver.CriteriaSet;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.action.ActionSupport;
+
+/**
+ * Validates that the currenty selected trust chain meets the trust anchor requirements in the resolve entity request.
+ * If not and if other candidates remains, {@link OidFederationEventIds#RESELECT_TRUST_CHAIN} is published. If no other
+ * candidates are available, {@link OidFederationEventIds#INVALID_TRUST_ANCHOR} or
+ * {@link OidFederationEventIds#INVALID_SUBJECT} is published.
+ *
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#INVALID_MSG_CTX}
+ * @event {@link OidFederationEventIds#RESELECT_TRUST_CHAIN}
+ * @event {@link OidFederationEventIds#INVALID_TRUST_ANCHOR}
+ * @event {@link OidFederationEventIds#INVALID_SUBJECT}
+ *
+ * @since 4.3.0
+ */
+public class ValidateSelectedTrustChain extends AbstractProfileAction {
+
+ /** Class logger. */
+ @Nonnull private Logger log = LoggerFactory.getLogger(ValidateSelectedTrustChain.class);
+
+ /** Metadata cache for entity configurations. */
+ @NonnullAfterInit private MetadataCache<EntityStatement> entityConfigurationCache;
+
+ /** Strategy used to lookup the trust chain context. */
+ @Nonnull private Function<ProfileRequestContext, RelyingPartyTrustChainContext> trustChainContextLookupStrategy;
+
+ /** Strategy used to locate the resolve entity context. */
+ @Nonnull
+ private Function<ProfileRequestContext, RelyingPartyResolveEntityContext> resolveEntityContextLookupStrategy;
+
+ /** The validated request to operate on. */
+ @NonnullBeforeExec private ResolveEntityRequest validatedRequest;
+
+ /**
+ * Constructor.
+ */
+ public ValidateSelectedTrustChain() {
+ final Function<ProfileRequestContext, RelyingPartyTrustChainContext> tcls =
+ new ChildContextLookup<>(RelyingPartyTrustChainContext.class).compose(
+ new InboundMessageContextLookup());
+ assert tcls != null;
+ trustChainContextLookupStrategy = tcls;
+ final Function<ProfileRequestContext, RelyingPartyResolveEntityContext> recls =
+ new ChildContextLookup<>(RelyingPartyResolveEntityContext.class).compose(
+ new InboundMessageContextLookup());
+ assert recls != null;
+ resolveEntityContextLookupStrategy = recls;
+ }
+
+ /**
+ * Set the metadata cache for entity configurations.
+ *
+ * @param cache What to set.
+ */
+ public void setEntityConfigurationCache(@Nonnull final MetadataCache<EntityStatement> cache) {
+ checkSetterPreconditions();
+ entityConfigurationCache = Constraint.isNotNull(cache, "Entity configuration metadata cache cannot be null");
+ }
+
+ /**
+ * Set the strategy used to lookup the trust chain context.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setTrustChainContextLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext, RelyingPartyTrustChainContext> strategy) {
+ checkSetterPreconditions();
+ trustChainContextLookupStrategy =
+ Constraint.isNotNull(strategy, "TrustChainContextLookupStrategy cannot be null");
+ }
+
+ /**
+ * Set the strategy used to locate the resolve entity context
+ *
+ * @param strategy What to set.
+ */
+ public void setResolveEntityContextLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext,RelyingPartyResolveEntityContext> strategy) {
+ checkSetterPreconditions();
+ resolveEntityContextLookupStrategy = 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 metadata cache cannot be null");
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+ if (!super.doPreExecute(profileRequestContext)) {
+ return false;
+ }
+
+ final RelyingPartyResolveEntityContext resolveEntityContext =
+ resolveEntityContextLookupStrategy.apply(profileRequestContext);
+ if (resolveEntityContext == null || resolveEntityContext.getValidatedRequest() == null) {
+ log.error("{} Could not resolve validated request messaGE", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return false;
+ }
+ validatedRequest = resolveEntityContext.getValidatedRequest();
+
+ return true;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+ final RelyingPartyTrustChainContext trustChainContext =
+ trustChainContextLookupStrategy.apply(profileRequestContext);
+ final Pair<List<EntityStatement>, OIDCClientInformation> selectedTrustChain =
+ trustChainContext != null ? trustChainContext.getSelectedTrustChain() : null;
+ if (selectedTrustChain == null || selectedTrustChain.getFirst() == null) {
+ final List<Pair<List<EntityStatement>,OIDCClientInformation>> allChains =
+ trustChainContext != null ? trustChainContext.getPolicyCompliantTrustChains() : null;
+ if (allChains == null || allChains.isEmpty()) {
+ if (isSubjectValid(validatedRequest.getSubject())) {
+ log.debug("{} No trust chains were resolved, subject is valid", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, OidFederationEventIds.INVALID_TRUST_ANCHOR);
+ return;
+ } else {
+ log.debug("{} No trust chains were resolved, subject is not valid", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, OidFederationEventIds.INVALID_SUBJECT);
+ return;
+ }
+ } else {
+ log.debug("{} No trust chains left to choose from", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, OidFederationEventIds.INVALID_TRUST_ANCHOR);
+ return;
+ }
+ }
+
+ final List<String> trustAnchors = validatedRequest.getTrustAnchors();
+ final List<EntityStatement> candidateChain = selectedTrustChain.getFirst();
+ assert candidateChain != null;
+ final String candidateAnchor = candidateChain.get(candidateChain.size() - 1).getEntityID().getValue();
+ if (!trustAnchors.contains(candidateAnchor)) {
+ log.debug("{} Selected trust chain candidate has unrequested trust anchor {}", getLogPrefix(),
+ candidateAnchor);
+ assert trustChainContext != null;
+ final List<List<EntityStatement>> rejectedTrustChains = trustChainContext.getRejectedTrustChains();
+ if (rejectedTrustChains == null) {
+ trustChainContext.setRejectedTrustChains(List.of(selectedTrustChain.getFirst()));
+ } else {
+ final List<List<EntityStatement>> rejectedChains = new ArrayList<>(rejectedTrustChains);
+ rejectedChains.add(selectedTrustChain.getFirst());
+ trustChainContext.setRejectedTrustChains(CollectionSupport.copyToList(rejectedChains));
+ }
+ ActionSupport.buildEvent(profileRequestContext, OidFederationEventIds.RESELECT_TRUST_CHAIN);
+ return;
+ }
+ }
+
+ /**
+ * Checks if an entity configuration can be resolved for the given subject and it's thus valid for federation.
+ *
+ * @param subject the subject to be verified
+ * @return true if the given subject is valid, false otherwise.
+ */
+ protected boolean isSubjectValid(@Nonnull final String subject) {
+ final SubjectEntityIDCriterion subjectCriterion = new SubjectEntityIDCriterion(subject);
+ try {
+ final List<EntityStatement> result = entityConfigurationCache.get(new CriteriaSet(subjectCriterion));
+ if (result.size() == 1) {
+ return true;
+ }
+ } catch (final MetadataCacheException e) {
+ log.debug("{} Exception catched when resolving entty configuration", e);
+ }
+ return false;
+ }
+}
\ No newline at end of file
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 5bac9ce4..05fc2260 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
@@ -722,6 +722,27 @@
</property>
</bean>
+ <bean id="shibboleth.oidc.DefaultResolveEntityApiMappedErrors"
+ parent="shibboleth.oidc.DefaultApiMappedErrors"
+ class="org.springframework.beans.factory.config.MapFactoryBean">
+ <property name="sourceMap">
+ <map merge="true" value-type="com.nimbusds.oauth2.sdk.ErrorObject">
+ <entry>
+ <key>
+ <util:constant static-field="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.OidFederationEventIds.INVALID_TRUST_ANCHOR"/>
+ </key>
+ <bean class="com.nimbusds.oauth2.sdk.ErrorObject" c:_0="invalid_trust_anchor" c:_1="Trust anchor in the request is invalid" c:_2="404" />
+ </entry>
+ <entry>
+ <key>
+ <util:constant static-field="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.OidFederationEventIds.INVALID_SUBJECT"/>
+ </key>
+ <bean class="com.nimbusds.oauth2.sdk.ErrorObject" c:_0="invalid_subject" c:_1="Subject in the request is invalid" c:_2="404" />
+ </entry>
+ </map>
+ </property>
+ </bean>
+
<bean id="shibboleth.oidc.DefaultUnregisteredClientPolicyFilename" class="java.lang.String" factory-method="valueOf">
<constructor-arg value="%{idp.oidc.DefaultUnregisteredClientPolicyFile:%{idp.home}/conf/oidc-unregistered-client-policy.json}" />
</bean>
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/resolve-entity/resolve-entity-beans.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/resolve-entity/resolve-entity-beans.xml
new file mode 100644
index 00000000..57377fad
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/resolve-entity/resolve-entity-beans.xml
@@ -0,0 +1,198 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans" xmlns:c="http://www.springframework.org/schema/c"
+ xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p"
+ xmlns:util="http://www.springframework.org/schema/util" 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.oidc.profileId" class="java.lang.String"
+ c:_0="#{T(net.shibboleth.idp.plugin.oidc.op.oidfed.config.OIDFederationResolveEntityProfileConfiguration).PROFILE_ID}" />
+
+ <bean id="shibboleth.oidc.loggingId" class="java.lang.String" c:_0="%{idp.service.logging.oidfedresolve:OIDFED.ResolveEntity}" />
+
+ <util:constant id="shibboleth.metrics.ProfileCounter"
+ static-field="net.shibboleth.idp.plugin.oidc.op.oidfed.config.DefaultOIDFederationResolveEntityProfileConfiguration.PROFILE_COUNTER" />
+
+ <bean id="DecodeMessage" class="org.opensaml.profile.action.impl.DecodeMessage" scope="prototype">
+ <constructor-arg>
+ <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.decoding.impl.ResolveEntityRequestDecoder" scope="prototype"
+ p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier"
+ p:removeIpAddressFromEndpointUri="%{idp.oidc.logging.removeIpAddressFromProtocolMessage:false}"
+ p:customRequestParser="#{getObject('%{idp.oidc.requestParser.ResolveEntityRequest:}'.trim())}"/>
+ </constructor-arg>
+ </bean>
+
+ <bean id="InitializeOutboundMessageContext"
+ class="net.shibboleth.idp.plugin.oidc.op.profile.impl.InitializeOutboundResponseMessageContext"
+ scope="prototype" />
+
+ <bean id="shibboleth.ClientIDLookupStrategy" parent="shibboleth.Functions.Expression"
+ c:expression="new com.nimbusds.oauth2.sdk.id.ClientID(#input.getMessage().getSubject())" />
+
+ <bean id="InitializeRelyingPartyContext"
+ class="net.shibboleth.idp.plugin.oidc.op.profile.impl.InitializeRelyingPartyContext" scope="prototype"
+ p:clientIDLookupStrategy-ref="shibboleth.ClientIDLookupStrategy"
+ p:inbound="true" />
+
+ <bean id="shibboleth.oidfed.ResolveEntityResponseMetadataCache" parent="shibboleth.oidc.CacheBuilder">
+ <constructor-arg>
+ <bean p:cacheId="DefaultResolveEntityResponseMetadataCache" parent="shibboleth.oidfed.ResolveEntityResponseMetadataCacheBuilderSpec"
+ p:cleanupTaskInterval="PT30S"/>
+ </constructor-arg>
+ </bean>
+
+ <bean id="shibboleth.oidfed.ResolveEntityResponseMetadataCacheBuilderSpec"
+ class="net.shibboleth.oidc.metadata.cache.impl.DynamicMetadataCacheBuilderSpec"
+ p:minCacheDuration="%{idp.oidfed.resolveEntity.maxRefreshDelay:PT1S}"
+ p:maxCacheDuration="%{idp.oidfed.resolveEntity.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 parent="shibboleth.BiFunctions.Expression" c:expression="#input1"/>
+ </property>
+ <property name="fetchStrategy">
+ <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultResolveEntityResponseFetchingStrategy" />
+ </property>
+ </bean>
+
+ <bean id="ValidateRequest" class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.ValidateResolveEntityRequest"
+ scope="prototype"
+ p:localTrustAnchorsCache-ref="#{'%{idp.oidfed.resove-entity.LocalTrustAnchorsMetadataCache:shibboleth.oidfed.LocalTrustAnchorsMetadataCache}'.trim()}" />
+
+ <bean id="LookupCachedResolveEntityRespomse"
+ class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.LookupCachedResolveEntityRespomse"
+ scope="prototype"
+ p:responseCache-ref="shibboleth.oidfed.ResolveEntityResponseMetadataCache" />
+
+ <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()}"
+ p:clientIDLookupStrategy-ref="shibboleth.ClientIDLookupStrategy"
+ p:metadataPolicyMergingStrategy-ref="#{'%{idp.oidfed.authorize.TrustChainMetadataPolicyMergingStrategy:DefaultTrustChainMetadataPolicyMergingStrategy}'.trim()}"
+ p:metadataPolicyEnforcer-ref="#{'%{idp.oidfed.authorize.MetadataPolicyEnforcer:DefaultMetadataPolicyEnforcer}'.trim()}"
+ p:preSelectedTrustChainIdsLookupStrategy="#{getObject('shibboleth.oidfed.PreSelectedTrustChainIDsLookupStrategy') ?: getObject('shibboleth.oidfed.DefaultPreSelectedTrustChainIDsLookupStrategy')}"/>
+
+ <bean id="DefaultMetadataPolicyEnforcer"
+ class="net.shibboleth.oidc.metadata.policy.impl.DefaultMetadataPolicyEnforcer" />
+
+ <bean id="DefaultTrustChainMetadataPolicyMergingStrategy"
+ class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.navigate.DefaultTrustChainMetadataPolicyMergingStrategy"
+ p:localMetadataPolicyStrategy-ref="#{'%{idp.oidfed.resolve.LocalMetadataPolicyStrategy:DefaultLocalMetadataPolicyStrategy}'.trim()}"/>
+
+ <bean id="DefaultLocalMetadataPolicyStrategy"
+ parent="shibboleth.Functions.Constant">
+ <constructor-arg name="target">
+ <util:map/>
+ </constructor-arg>
+ </bean>
+
+ <bean id="SelectTrustChain" class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.SelectTrustChain"
+ scope="prototype">
+ <property name="activationCondition">
+ <bean parent="shibboleth.Conditions.Expression"
+ c:expression="#input.ensureInboundMessageContext().containsSubcontext(T(net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.RelyingPartyTrustChainContext))" />
+ </property>
+ </bean>
+
+ <bean id="ValidateSelectedTrustChain" class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.ValidateSelectedTrustChain"
+ scope="prototype"
+ p:entityConfigurationCache-ref="shibboleth.oidfed.EntityConfigurationMetadataCache"/>
+
+ <bean id="ResolveTrustMarks" class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.ResolveTrustMarks"
+ scope="prototype"
+ p:trustChainCache-ref="#{'%{idp.oidfed.authorize.TrustChainMetadataCache:shibboleth.oidfed.TrustChainMetadataCache}'.trim()}">
+ <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>
+ <property name="trustedTrustMarkIssuersLookupStrategy">
+ <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultTrustChainTrustedTrustMarkIssuersLookupStrategy"
+ p:objectMapper-ref="shibboleth.oidc.JSONObjectMapper" />
+ </property>
+ </bean>
+
+ <bean id="PopulateEntityStatementSignatureSigningParameters"
+ class="net.shibboleth.oidc.profile.impl.PopulateJWTSignatureSigningParameters" scope="prototype"
+ c:strategy-ref="shibboleth.MessageContextLookup.Outbound"
+ p:securityParametersContextLookupStrategy-ref="EntityStatementSecurityParametersContextLookupStrategy">
+ <property name="configurationLookupStrategy">
+ <bean lazy-init="true"
+ class="net.shibboleth.oidc.profile.config.navigate.JWTSignatureSigningConfigurationLookupFunction" />
+ </property>
+ <property name="signatureSigningParametersResolver">
+ <bean class="net.shibboleth.oidc.security.jose.impl.ClientInformationSignatureSigningParametersResolver">
+ <constructor-arg name="signatureAlgorithmLookupStrategy">
+ <bean parent="shibboleth.Functions.Constant" c:target="" />
+ </constructor-arg>
+ <constructor-arg name="defaultAlgorithmValue" value="%{idp.oidfed.entity.sigalg:RS256}" />
+ </bean>
+ </property>
+ </bean>
+
+ <bean id="EntityStatementSecurityParametersContextLookupStrategy" parent="shibboleth.Functions.Compose"
+ c:g-ref="shibboleth.oidc.ChildLookupOrCreate.JWTSecurityParameters"
+ c:f-ref="shibboleth.ChildLookup.RelyingParty" />
+
+ <bean id="EntityStatementSecurityParametersCreationViaMessageContextStrategy" parent="shibboleth.Functions.Compose">
+ <constructor-arg name="g" ref="EntityStatementSecurityParametersContextLookupStrategy" />
+ <constructor-arg name="f">
+ <bean parent="shibboleth.Functions.Expression" c:expression="#input.getParent()" />
+ </constructor-arg>
+ </bean>
+
+ <bean id="BuildEntityStatement"
+ class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.BuildResolveEntityResponse" scope="prototype"
+ p:identifierGeneratorLookupStrategy-ref="shibboleth.oidc.DefaultIdentifierGenerationStrategy"
+ p:objectMapper-ref="#{'%{idp.oidc.logging.objectMapper:shibboleth.oidc.JSONObjectMapper}'.trim()}">
+ <property name="subjectLookupStrategy">
+ <bean parent="shibboleth.Functions.Expression"
+ c:expression="#input.ensureInboundMessageContext().getMessage().getSubject()" />
+ </property>
+ </bean>
+
+ <bean id="SignEntityStatement" class="net.shibboleth.idp.profile.impl.WebFlowMessageHandlerAdaptor"
+ scope="prototype" c:executionDirection="OUTBOUND ">
+ <constructor-arg name="messageHandler">
+ <bean id="SignEntityStatementHandler"
+ class="net.shibboleth.oidc.security.impl.SignJWTHandler" scope="prototype" p:logName="Entity Statement"
+ p:securityParametersLookupStrategy-ref="EntityStatementSecurityParametersCreationViaMessageContextStrategy"
+ p:typeHeader="entity-statement+jwt">
+ <property name="claimsToSignLookupStrategy">
+ <bean
+ class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.JWTClaimsSetFromEntityStatementLookupFunction" />
+ </property>
+ <property name="jwtUpdateConsumer">
+ <bean
+ class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.EntityStatementUpdateStrategy" />
+ </property>
+ </bean>
+ </constructor-arg>
+ </bean>
+
+ <bean id="FormOutboundMessage"
+ class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.FormOutboundResolveEntityResponse" scope="prototype"
+ p:responseCache-ref="shibboleth.oidfed.ResolveEntityResponseMetadataCache" />
+
+ <bean id="BuildErrorResponseFromEvent"
+ class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.BuildResolveEntityErrorResponseFromEvent" scope="prototype"
+ p:httpServletResponseSupplier-ref="shibboleth.HttpServletResponseSupplier"
+ p:mappedErrors="#{getObject('shibboleth.oidfed.resolve-entity.MappedErrors') ?: getObject('shibboleth.oidc.DefaultResolveEntityApiMappedErrors')}"
+ p:responseCache-ref="shibboleth.oidfed.ResolveEntityResponseMetadataCache">
+ <property name="eventContextLookupStrategy">
+ <bean class="net.shibboleth.idp.profile.context.navigate.WebFlowCurrentEventLookupFunction" />
+ </property>
+ </bean>
+
+</beans>
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/resolve-entity/resolve-entity-flow.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/resolve-entity/resolve-entity-flow.xml
new file mode 100644
index 00000000..903e3fcc
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/resolve-entity/resolve-entity-flow.xml
@@ -0,0 +1,77 @@
+<flow xmlns="http://www.springframework.org/schema/webflow"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://www.springframework.org/schema/webflow http://www.springframework.org/schema/webflow/spring-webflow.xsd"
+ parent="oidc/abstract-api">
+
+ <action-state id="InitializeMandatoryContexts">
+ <evaluate expression="InitializeProfileRequestContext" />
+ <evaluate expression="PopulateMetricContext" />
+ <evaluate expression="FlowStartPopulateAuditContext" />
+ <evaluate expression="InitializeOutboundMessageContext" />
+ <evaluate expression="'proceed'" />
+
+ <transition on="proceed" to="DecodeMessage">
+ <set name="flowScope.transitionAfterDecode" value="'SelectConfiguration'" />
+ </transition>
+ </action-state>
+
+ <action-state id="SelectConfiguration">
+ <evaluate expression="InitializeRelyingPartyContext" />
+ <evaluate expression="SelectRelyingPartyConfiguration" />
+ <evaluate expression="SelectProfileConfiguration" />
+ <evaluate expression="PostLookupPopulateAuditContext" />
+ <evaluate expression="PopulateInboundInterceptContext" />
+ <evaluate expression="'proceed'" />
+
+ <transition on="proceed" to="CheckInboundInterceptContext" />
+ </action-state>
+
+ <decision-state id="CheckInboundInterceptContext">
+ <on-entry>
+ <set name="flowScope.skipOAuth2ClientAuth" value="true" />
+ </on-entry>
+ <if test="opensamlProfileRequestContext.ensureSubcontext(T(net.shibboleth.idp.profile.context.ProfileInterceptorContext)).getAvailableFlows().isEmpty()"
+ then="LookupCachedResponse" else="DoInboundInterceptSubflow" />
+ </decision-state>
+
+ <subflow-state id="DoInboundInterceptSubflow" subflow="intercept">
+ <input name="calledAsSubflow" value="true" />
+ <transition on="proceed" to="LookupCachedResponse" />
+ </subflow-state>
+
+ <action-state id="LookupCachedResponse">
+ <evaluate expression="ValidateRequest" />
+ <evaluate expression="LookupCachedResolveEntityRespomse" />
+ <evaluate expression="'proceed'" />
+ <transition on="CachedResolveEntityResponseFound" to="BuildResponseMessage" />
+ <transition on="proceed" to="ResolveTrustChains" />
+ </action-state>
+
+ <action-state id="ResolveTrustChains">
+ <evaluate expression="ResolveTrustChains" />
+ <evaluate expression="'proceed'" />
+ <transition on="proceed" to="SelectTrustChain" />
+ </action-state>
+
+ <action-state id="SelectTrustChain">
+ <evaluate expression="SelectTrustChain" />
+ <evaluate expression="ValidateSelectedTrustChain" />
+ <evaluate expression="ResolveTrustMarks" />
+ <evaluate expression="'proceed'" />
+ <transition on="ReselectTrustChain" to="SelectTrustChain" />
+ <transition on="proceed" to="BuildResponse" />
+ </action-state>
+
+
+ <action-state id="BuildResponse">
+ <evaluate expression="PopulateEntityStatementSignatureSigningParameters" />
+ <evaluate expression="BuildEntityStatement" />
+ <evaluate expression="SignEntityStatement" />
+ <evaluate expression="'proceed'" />
+
+ <transition on="proceed" to="BuildResponseMessage" />
+ </action-state>
+
+ <bean-import resource="resolve-entity-beans.xml" />
+
+</flow>
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml
index 0eefd026..3c10ef46 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml
@@ -109,7 +109,7 @@
<bean id="AbstractOIDFederationProfile" abstract="true"
p:securityConfiguration-ref="shibboleth.oidc.federation.DefaultSecurityConfiguration" />
- <bean id="OIDFED.Configuration" parent="AbstractOIDCProfile" lazy-init="true"
+ <bean id="OIDFED.Configuration" parent="AbstractOIDFederationProfile" lazy-init="true"
class="net.shibboleth.idp.plugin.oidc.op.oidfed.config.DefaultOIDFederationEntityConfigurationProfileConfiguration"
p:issuer-ref="shibboleth.oidc.issuer"
p:authorityHints="%{idp.oidfed.entity.authorityHints:https://example.org}" />
@@ -122,6 +122,9 @@
class="net.shibboleth.idp.plugin.oidc.op.oidfed.config.DefaultOIDFederationExplicitRegistrationProfileConfiguration"
p:mandatoryTrustMarks="%{idp.oidfed.explicitRegistration.mandatoryTrustMarks:}" />
+ <bean id="OIDFED.ResolveEntity" parent="AbstractOIDFederationProfile" lazy-init="true"
+ class="net.shibboleth.idp.plugin.oidc.op.oidfed.config.DefaultOIDFederationResolveEntityProfileConfiguration" />
+
<!-- Metadata-driven variants. -->
<bean id="AbstractMDDrivenOIDCProfile" parent="AbstractMDDrivenProfile" abstract="true">
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/ResolveEntityFlowTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/ResolveEntityFlowTest.java
new file mode 100644
index 00000000..bdb6fa75
--- /dev/null
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/ResolveEntityFlowTest.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.profile.flow.oidfed;
+
+import org.springframework.webflow.executor.FlowExecutionResult;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.nimbusds.jwt.SignedJWT;
+import com.nimbusds.oauth2.sdk.Response;
+
+import net.shibboleth.idp.plugin.oidc.op.oidfed.messaging.impl.ResolveEntityResponse;
+import net.shibboleth.oidc.profile.messaging.JSONErrorResponse;
+
+/**
+ * Flow tests for the OpenID federation resolve entity flow.
+ */
+public class ResolveEntityFlowTest extends AbstractFederationFlowTest {
+
+ public static final String FLOW_ID = "oidfed/resolve-entity";
+
+ public ResolveEntityFlowTest() {
+ super(FLOW_ID);
+ }
+
+ @Test
+ public void testInvalidMethod() throws Exception {
+ setJsonRequest("POST", "{}");
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ assertErrorCode(result, "invalid_request");
+ }
+
+ @Test
+ public void testInvalidSubject() throws Exception {
+ request.setMethod("GET");
+ request.setQueryString("sub=mockClientId&trust_anchors=" + anchorId + "&entity_type=openid_relying_party");
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ assertErrorCode(result, "invalid_subject");
+ }
+
+ @Test
+ public void testUntrustedAnchor() throws Exception {
+ request.setMethod("GET");
+ final String clientId = uniqueClientId();
+ configureMockHttpClient(clientId);
+ request.setQueryString("sub=" + clientId + "&trust_anchors=mockAnchors&entity_type=openid_relying_party");
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ assertErrorCode(result, "invalid_trust_anchor");
+ }
+
+ @Test
+ public void testWithTrustedTrustAnchor() throws Exception {
+ request.setMethod("GET");
+ final String clientId = uniqueClientId();
+ configureMockHttpClient(clientId);
+ request.setQueryString("sub=" + clientId + "&trust_anchors=" + anchorId + "&entity_type=openid_relying_party");
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ final ResolveEntityResponse parsedResponse =
+ parseSuccessResponse(result, ResolveEntityResponse.class);
+ final SignedJWT response = parsedResponse.getJWT();
+ Assert.assertEquals(response.getJWTClaimsSet().getSubject(), clientId);
+ }
+
+ protected JSONErrorResponse parseErrorResponse(final FlowExecutionResult result) {
+ final Response response = parseResponse(result);
+ Assert.assertTrue(response instanceof JSONErrorResponse);
+ return (JSONErrorResponse) response;
+ }
+
+}
diff --git a/idp-oidc-extension-impl/src/test/resources/net/shibboleth/idp/module/conf/relying-party.xml b/idp-oidc-extension-impl/src/test/resources/net/shibboleth/idp/module/conf/relying-party.xml
index 0d2ac430..faa7eee3 100644
--- a/idp-oidc-extension-impl/src/test/resources/net/shibboleth/idp/module/conf/relying-party.xml
+++ b/idp-oidc-extension-impl/src/test/resources/net/shibboleth/idp/module/conf/relying-party.xml
@@ -47,6 +47,7 @@
<bean parent="OAUTH2.Revocation" />
<bean parent="OAUTH2.PAR" />
<bean parent="OIDFED.Configuration" p:cachedEntityStatementLifetime="PT2S" />
+ <bean parent="OIDFED.ResolveEntity" />
</list>
</property>
</bean>
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list