[java-idp-plugin-oidc-op-oidfed] branch dev/CACHE-REFACTOR updated: Initial support for client authentication in the resolve-entity flow

Codeberg noreply at shibboleth.net
Thu Apr 16 12:40:38 UTC 2026


This is an automated email from the git hooks/post-receive script.

codeberg pushed a commit to branch dev/CACHE-REFACTOR
in repository java-idp-plugin-oidc-op-oidfed.

View the commit online:
https://codeberg.org/Shibboleth/java-idp-plugin-oidc-op-oidfed/commit/8c7d7dba1ccd0e40547e0db599767a4ba8e79c57

The following commit(s) were added to refs/heads/dev/CACHE-REFACTOR by this push:
     new 8c7d7db  Initial support for client authentication in the resolve-entity flow
8c7d7db is described below

commit 8c7d7dba1ccd0e40547e0db599767a4ba8e79c57
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Thu Apr 16 15:40:12 2026 +0300

    Initial support for client authentication in the resolve-entity flow
    
    - Support POST-method when client authentication is involved
      - Request decoder verifies the requirement
    - List of authentication flows configurable via 'idp.oidfed.resolve-entity.authn.flows', defaults to 'OAuth2Client'
      - The condition to make OAuth2Client resolving the metadata dynamically is 'idp.oidfed.resolve-entity.automaticRegistrationCondition', defaults to shibboleth.Conditions.TRUE
      - By default, private_key_jwt is the only enabled method, federation entity keys as the validation keys
        - inherited from shibboleth.oidfed.SignatureValidationConfiguration
      - Additional methods configurable for OIDFED.ResolveEntity via profile config option "tokenEndpointAuthMethods"
---
 ...ederationResolveEntityProfileConfiguration.java |   5 +-
 .../decoding/impl/ResolveEntityRequestDecoder.java |  24 +++-
 .../messaging/impl/ResolveEntityRequest.java       |  25 ++--
 ...ederationResolveEntityProfileConfiguration.java |  44 +++++-
 .../BuildResolveEntityErrorResponseFromEvent.java  |   4 +-
 .../profile/impl/ValidateResolveEntityRequest.java |   2 +-
 ...ormationFederationEntityCredentialResolver.java | 127 ++++++++++++++++++
 .../META-INF/net.shibboleth.idp/postconfig.xml     |  15 +++
 .../oidfed/resolve-entity/resolve-entity-beans.xml |  12 +-
 .../oidfed/resolve-entity/resolve-entity-flow.xml  |  50 +++++--
 .../idp/service/relying-party/postconfig.xml       |  33 ++++-
 .../profile/flow/oidfed/ResolveEntityFlowTest.java | 148 ++++++++++++++++++++-
 .../shibboleth/idp/module/conf/relying-party.xml   |   9 ++
 13 files changed, 465 insertions(+), 33 deletions(-)

diff --git a/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/config/OIDFederationResolveEntityProfileConfiguration.java b/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/config/OIDFederationResolveEntityProfileConfiguration.java
index 6111275..cacd25e 100644
--- a/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/config/OIDFederationResolveEntityProfileConfiguration.java
+++ b/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/config/OIDFederationResolveEntityProfileConfiguration.java
@@ -21,6 +21,8 @@ import javax.annotation.Nullable;
 
 import org.opensaml.profile.context.ProfileRequestContext;
 
+import net.shibboleth.oidc.profile.oauth2.config.OAuth2ClientAuthenticableClientProfileConfiguration;
+import net.shibboleth.oidc.profile.oauth2.config.OAuth2ClientAuthenticableProfileConfiguration;
 import net.shibboleth.profile.config.OverriddenIssuerProfileConfiguration;
 import net.shibboleth.shared.annotation.ConfigurationSetting;
 import net.shibboleth.shared.annotation.constraint.Positive;
@@ -29,7 +31,8 @@ import net.shibboleth.shared.annotation.constraint.Positive;
  * Profile configuration for an OpenID Federation Resolve Entity.
  */
 public interface OIDFederationResolveEntityProfileConfiguration extends OverriddenIssuerProfileConfiguration, 
-    OIDFederationProfileConfiguration, OIDFederationResponseCachingProfileConfiguration {
+    OIDFederationProfileConfiguration, OIDFederationResponseCachingProfileConfiguration,
+    OAuth2ClientAuthenticableProfileConfiguration, OAuth2ClientAuthenticableClientProfileConfiguration {
     
     /** OIDC base protocol URI. */
     public static final String PROTOCOL_URI = "https://openid.net/specs/openid-federation-1_0.html";
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/decoding/impl/ResolveEntityRequestDecoder.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/decoding/impl/ResolveEntityRequestDecoder.java
index ba15015..97f3f55 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/decoding/impl/ResolveEntityRequestDecoder.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/decoding/impl/ResolveEntityRequestDecoder.java
@@ -28,6 +28,8 @@ import org.opensaml.messaging.decoder.MessageDecodingException;
 import org.slf4j.Logger;
 
 import com.google.common.base.MoreObjects;
+import com.nimbusds.oauth2.sdk.ParseException;
+import com.nimbusds.oauth2.sdk.auth.ClientAuthentication;
 import com.nimbusds.oauth2.sdk.http.HTTPRequest;
 import com.nimbusds.oauth2.sdk.http.JakartaServletUtils;
 
@@ -50,8 +52,11 @@ public class ResolveEntityRequestDecoder extends BaseOAuth2RequestDecoder<Resolv
     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");
+        if (!"GET".equalsIgnoreCase(request.getMethod()) && !"POST".equalsIgnoreCase(request.getMethod())) {
+            throw new MessageDecodingException("This message decoder only supports the HTTP GET and POST methods");
+        }
+        if (!"application/x-www-form-urlencoded".equals(request.getContentType())) {
+            throw new MessageDecodingException("Invalid content type: " + request.getContentType());
         }
         try {
             final HTTPRequest httpRequest = JakartaServletUtils.createHTTPRequest(request);
@@ -60,6 +65,15 @@ public class ResolveEntityRequestDecoder extends BaseOAuth2RequestDecoder<Resolv
             if (uri == null) {
                 throw new MessageDecodingException("Could not parse request URI");
             }
+            final ClientAuthentication clientAuthentication = ClientAuthentication.parse(httpRequest);
+            if (clientAuthentication != null && !"POST".equalsIgnoreCase(request.getMethod())) {
+                throw new MessageDecodingException(
+                        "This message decoder requires use of POST method when client authentication is involved");
+            }
+            if (clientAuthentication == null && !"GET".equalsIgnoreCase(request.getMethod())) {
+                throw new MessageDecodingException(
+                        "This message decoder requires use of GET method when client authentication is not involved");
+            }
             final Map<String, List<String>> parameters = httpRequest.getQueryParameters();
             final String subject = Optional.ofNullable(parameters.get("sub"))
                     .filter(Objects::nonNull)
@@ -76,8 +90,9 @@ public class ResolveEntityRequestDecoder extends BaseOAuth2RequestDecoder<Resolv
             if (trustAnchors == null) {
                 throw new MessageDecodingException("No trust_anchor included in the request");
             }
-            return new ResolveEntityRequest(uri, subject, trustAnchors, parameters.get("entity_type"));
-        } catch (final IOException e) {
+            return new ResolveEntityRequest(uri, subject, trustAnchors, parameters.get("entity_type"),
+                    clientAuthentication);
+        } catch (final IOException | ParseException e) {
             log.error("Could not create HTTP request from the request", e);
             throw new MessageDecodingException(e);
         }
@@ -91,6 +106,7 @@ public class ResolveEntityRequestDecoder extends BaseOAuth2RequestDecoder<Resolv
                 .add("trustAnchors", message.getTrustAnchors())
                 .add("entityTypes", message.getEntityTypes())
                 .add("endpointURI", getEndpointURI(message))
+                .add("clientAuthentication", RequestUtil.getClientAuthenticationLog(message.getClientAuthentication()))
                 .toString();
     }
 
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/messaging/impl/ResolveEntityRequest.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/messaging/impl/ResolveEntityRequest.java
index 2c00b0f..10e9f2e 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/messaging/impl/ResolveEntityRequest.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/messaging/impl/ResolveEntityRequest.java
@@ -21,7 +21,8 @@ import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
 import com.google.common.base.MoreObjects;
-import com.nimbusds.oauth2.sdk.Request;
+import com.nimbusds.oauth2.sdk.AbstractOptionallyAuthenticatedRequest;
+import com.nimbusds.oauth2.sdk.auth.ClientAuthentication;
 import com.nimbusds.oauth2.sdk.http.HTTPRequest;
 
 import net.shibboleth.shared.annotation.constraint.NotEmpty;
@@ -31,10 +32,7 @@ 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;
+public class ResolveEntityRequest extends AbstractOptionallyAuthenticatedRequest {
 
     /** The requested subject. */
     @Nonnull @NotEmpty private final String subject;
@@ -53,12 +51,14 @@ public class ResolveEntityRequest implements Request {
      * @param sub subject
      * @param anchors trust anchors
      * @param types optional entity types
+     * @param clientAuthentication optional client authentication
      */
     public ResolveEntityRequest(@Nonnull final URI uri,
                   @Nonnull @NotEmpty final String sub,
                   @Nonnull @NotEmpty final List<String> anchors,
-                  @Nullable final List<String>  types) {
-        endpointUri = Constraint.isNotNull(uri, "Endpoint URI cannot be null");
+                  @Nullable final List<String>  types,
+                  @Nullable final ClientAuthentication clientAuthentication) {
+        super(Constraint.isNotNull(uri, "Endpoint URI cannot be null"), clientAuthentication);
         subject = Constraint.isNotNull(sub, "Subject cannot be empty");
         Constraint.isNotEmpty(anchors, "Trust anchors cannot be empty");
         trustAnchors = anchors;
@@ -94,7 +94,9 @@ public class ResolveEntityRequest implements Request {
 
     /** {@inheritDoc} */
     @Override @Nonnull public URI getEndpointURI() {
-        return endpointUri;
+        final URI result = super.getEndpointURI();
+        assert result != null;
+        return result;
     }
 
     /** {@inheritDoc} */
@@ -112,6 +114,7 @@ public class ResolveEntityRequest implements Request {
                 .add("trustAnchors", getTrustAnchors())
                 .add("entityTypes", getEntityTypes())
                 .add("endpointURI", getEndpointURI())
+                .add("clientAuthentication", getClientAuthentication())
                 .toString();
     }
 
@@ -128,8 +131,10 @@ public class ResolveEntityRequest implements Request {
             return false;
         }
         final ResolveEntityRequest other = (ResolveEntityRequest) obj;
-        return endpointUri.equals(other.endpointUri) && subject.equals(other.subject) &&
+        return getEndpointURI().equals(other.getEndpointURI()) && subject.equals(other.subject) &&
                 entityTypes.containsAll(other.entityTypes) && other.entityTypes.containsAll(entityTypes) &&
-                trustAnchors.containsAll(other.trustAnchors) && other.trustAnchors.containsAll(trustAnchors);
+                trustAnchors.containsAll(other.trustAnchors) && other.trustAnchors.containsAll(trustAnchors) &&
+                getClientAuthentication() == null ? other.getClientAuthentication() == null :
+                    getClientAuthentication().equals(other.getClientAuthentication());
     }
 }
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/config/impl/DefaultOIDFederationResolveEntityProfileConfiguration.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/config/impl/DefaultOIDFederationResolveEntityProfileConfiguration.java
index c6b729a..69a3114 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/config/impl/DefaultOIDFederationResolveEntityProfileConfiguration.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/config/impl/DefaultOIDFederationResolveEntityProfileConfiguration.java
@@ -23,6 +23,7 @@ import javax.annotation.Nullable;
 import org.opensaml.profile.context.ProfileRequestContext;
 
 import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.config.OIDFederationResolveEntityProfileConfiguration;
+import net.shibboleth.oidc.profile.oauth2.config.impl.AbstractOAuth2ClientAuthenticableProfileConfiguration;
 import net.shibboleth.shared.annotation.constraint.NotEmpty;
 import net.shibboleth.shared.annotation.constraint.Positive;
 import net.shibboleth.shared.logic.Constraint;
@@ -32,7 +33,7 @@ import net.shibboleth.shared.logic.FunctionSupport;
  * Implementation of a profile configuration for the OpenID Federation Resolve Entity.
  */
 public class DefaultOIDFederationResolveEntityProfileConfiguration
-    extends AbstractOIDFederationResponseCachingProfileConfiguration
+    extends AbstractOAuth2ClientAuthenticableProfileConfiguration
     implements OIDFederationResolveEntityProfileConfiguration {
 
     /** OIDC provider information profile counter name. */
@@ -41,6 +42,9 @@ public class DefaultOIDFederationResolveEntityProfileConfiguration
     /** 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;
 
@@ -59,6 +63,7 @@ public class DefaultOIDFederationResolveEntityProfileConfiguration
     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));
     }
     
@@ -86,6 +91,42 @@ public class DefaultOIDFederationResolveEntityProfileConfiguration
         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
@@ -121,4 +162,5 @@ public class DefaultOIDFederationResolveEntityProfileConfiguration
             @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-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/BuildResolveEntityErrorResponseFromEvent.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/BuildResolveEntityErrorResponseFromEvent.java
index 93b382d..2790250 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/BuildResolveEntityErrorResponseFromEvent.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/BuildResolveEntityErrorResponseFromEvent.java
@@ -240,10 +240,10 @@ public class BuildResolveEntityErrorResponseFromEvent extends AbstractProfileAct
         final RelyingPartyCachedMessageContext resolveEntityContext =
                 resolveEntityContextLookupStrategy.apply(profileRequestContext);
 
-        if (resolveEntityContext != null &&
+        final Duration cachedResponseLifetime = cachedResponseLifetimeLookupStrategy.apply(profileRequestContext);
+        if (resolveEntityContext != null && cachedResponseLifetime != null &&
                 resolveEntityContext.getValidatedRequest() instanceof ResolveEntityRequest resolveEntityRequest) {
             final NimbusResponseCriterion responseCriterion = new NimbusResponseCriterion(response);
-            final Duration cachedResponseLifetime = cachedResponseLifetimeLookupStrategy.apply(profileRequestContext);
             final Instant expiration = Instant.now().plus(cachedResponseLifetime);
             assert expiration != null;
             final ResponseContainerExpirationCriterion expirationCriterion =
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ValidateResolveEntityRequest.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ValidateResolveEntityRequest.java
index 1f80655..9f7e6bb 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ValidateResolveEntityRequest.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ValidateResolveEntityRequest.java
@@ -155,7 +155,7 @@ public class ValidateResolveEntityRequest extends AbstractProfileAction {
         log.debug("{} The following trust anchors were validated: {}", getLogPrefix(), validatedAnchors);
         cachedMessageContext.setValidatedRequest(
                 new ResolveEntityRequest(requestMessage.getEndpointURI(), requestMessage.getSubject(),
-                        validatedAnchors, requestMessage.getEntityTypes()));
+                        validatedAnchors, requestMessage.getEntityTypes(), requestMessage.getClientAuthentication()));
     }
 
     /**
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/security/credential/ClientInformationFederationEntityCredentialResolver.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/security/credential/ClientInformationFederationEntityCredentialResolver.java
new file mode 100644
index 0000000..652f23f
--- /dev/null
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/security/credential/ClientInformationFederationEntityCredentialResolver.java
@@ -0,0 +1,127 @@
+/*
+ * 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.security.credential;
+
+import java.util.List;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.security.credential.Credential;
+import org.slf4j.Logger;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformation;
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientMetadata;
+
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.EntityStatement;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.SubjectEntityStatementCriterion;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.util.EntityStatementHelper;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.support.ClientInformationExtensionSupport;
+import net.shibboleth.oidc.security.credential.JOSEObjectCredentialResolver;
+import net.shibboleth.oidc.security.credential.impl.BasicJOSEObjectCredentialResolver;
+import net.shibboleth.oidc.security.jose.criterion.ClientInformationCriterion;
+import net.shibboleth.shared.annotation.ParameterName;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.resolver.CriteriaSet;
+import net.shibboleth.shared.resolver.ResolverException;
+
+/**
+ * A {@link JOSEObjectCredentialResolver} that resolves credentials from the entity configuration payload. The entity
+ * configuration is fetched via client custom claim
+ * {@link ClientInformationExtensionSupport#KEY_VALIDATED_TRUST_CHAIN}.
+ */
+public class ClientInformationFederationEntityCredentialResolver extends BasicJOSEObjectCredentialResolver {
+    
+    /** Class logger. */
+    @Nonnull
+    private final Logger log = LoggerFactory.getLogger(ClientInformationFederationEntityCredentialResolver.class);
+
+    /** Resolver for fetching federation entity credentials from entity configuration. */
+    @Nonnull private final JOSEObjectCredentialResolver entityConfigurationCredentialResolver;
+
+    /** Object mapper used for deserializing jwks from the entity configuration payload. */
+    @Nonnull private final ObjectMapper objectMapper;
+
+    /**
+     * Constructor.
+     *
+     * @param resolver The resolver for fetching federation entity credentials from entity configuration.
+     * @param mapper The object mapper used for deserializing jwks from the entity configuration payload.
+     */
+    public ClientInformationFederationEntityCredentialResolver(@Nonnull
+            @ParameterName(name="entityConfigurationCredentialResolver") final JOSEObjectCredentialResolver resolver,
+            @Nonnull @ParameterName(name="objectMapper") final ObjectMapper mapper) {
+        entityConfigurationCredentialResolver = Constraint.isNotNull(resolver,
+                "EntityConfigurationCredentialResolver cannot be null");
+        objectMapper = Constraint.isNotNull(mapper, "ObjectMapper cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    @Nonnull protected Iterable<Credential> resolveFromSource(@Nullable final CriteriaSet criteriaSet)
+            throws ResolverException {
+        
+        Constraint.isNotNull(criteriaSet, "CriteriaSet was null");
+
+        if (criteriaSet != null) {
+            final ClientInformationCriterion clientCrit = criteriaSet.get(ClientInformationCriterion.class);
+            if (clientCrit != null) {
+                return resolveFromMetadata(criteriaSet, clientCrit.getOidcClientInformation());
+            }
+        }
+            
+        log.debug("Criteria did not contain a ClientInformationCriterion could not perform resolution");
+        return CollectionSupport.emptySet();
+    }
+    
+    /**
+     * Resolve the keyset from the entity configuration payload.
+     *
+     * @param criteriaSet the criteria set
+     * @param information the RP/Client information
+     * 
+     * @return a collection of credentials from the entity configuration key set (if any).
+     */
+    @Nonnull protected Iterable<Credential> resolveFromMetadata(@Nonnull final CriteriaSet criteriaSet, 
+            @Nonnull final OIDCClientInformation information) throws ResolverException {
+
+        final OIDCClientMetadata metadata = information.getOIDCMetadata();
+
+        if (metadata.getCustomField(ClientInformationExtensionSupport.KEY_VALIDATED_TRUST_CHAIN)
+                instanceof List<?> list) {
+            final List<String> serialized =
+                    list.stream().filter(String.class::isInstance).map(String.class::cast).toList();
+            assert serialized != null;
+            final List<EntityStatement<?>> trustChain =
+                    EntityStatementHelper.deserializeTrustChain(serialized, objectMapper);
+            if (trustChain != null) {
+                final EntityStatement<?> configuration = trustChain.get(0);
+                assert configuration != null;
+                final SubjectEntityStatementCriterion configurationCriterion =
+                        new SubjectEntityStatementCriterion(configuration);
+                log.debug("Returning credentials resolved via entity configuration credential resolver");
+                return entityConfigurationCredentialResolver.resolve(new CriteriaSet(configurationCriterion));
+            }
+        } else {
+            log.debug("Could not find the validated trust chain from the client metadata");
+        }
+        log.trace("Returning empty set of credentials");
+        return CollectionSupport.emptySet();
+    }
+    
+}
\ No newline at end of file
diff --git a/idp-oidfed-op-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml b/idp-oidfed-op-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
index fb85aa7..1820bdd 100644
--- a/idp-oidfed-op-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
+++ b/idp-oidfed-op-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
@@ -1075,6 +1075,9 @@
                 <entry key="#{T(net.shibboleth.oidc.profile.config.OIDCUserInfoConfiguration).PROFILE_ID}">
                     <ref bean="shibboleth.oidfed.userinfo.DefaultAutomaticRegistrationCondition"/>
                 </entry>
+                <entry key="#{T(net.shibboleth.idp.plugin.oidc.op.oidfed.profile.config.OIDFederationResolveEntityProfileConfiguration).PROFILE_ID}">
+                    <ref bean="shibboleth.oidfed.resolve-entity.DefaultAutomaticRegistrationCondition"/>
+                </entry>
             </util:map>
         </property>
     </bean>
@@ -1169,6 +1172,14 @@
         </constructor-arg>
     </bean>
 
+    <bean id="shibboleth.oidfed.resolve-entity.DefaultAutomaticRegistrationCondition" parent="shibboleth.Conditions.AND">
+        <constructor-arg>
+            <list>
+                <ref bean="%{idp.oidfed.resolve-entity.automaticRegistrationCondition:shibboleth.Conditions.TRUE}"/>
+            </list>
+        </constructor-arg>
+    </bean>
+
     <bean class="net.shibboleth.idp.plugin.oidc.op.security.jwt.claims.RequestObjectClaimsValidator">
         <constructor-arg>
             <bean id="shibboleth.oidfed.DefaultRequestObjectClaimsValidation"
@@ -1292,6 +1303,10 @@
         </property>
     </bean>
 
+    <!-- Property-based definition of login flows for the resolve-entity endpoint. -->    
+    <bean id="shibboleth.oidfed.resolver.PotentialFlows" class="org.springframework.beans.factory.config.ListFactoryBean"
+        p:sourceList="#{getObject('shibboleth.AuthenticationFlowDescriptorManager').getComponents().?[id matches 'authn/(' + '%{idp.oidfed.resolve-entity.authn.flows:OAuth2Client}'.trim() + ')']}" />
+
     <import resource="${idp.home}/conf/oidfed/oidfed-trustchain-resolver.xml"/>
 
 </beans>
\ No newline at end of file
diff --git a/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/resolve-entity/resolve-entity-beans.xml b/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/resolve-entity/resolve-entity-beans.xml
index 3263d56..a857621 100644
--- a/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/resolve-entity/resolve-entity-beans.xml
+++ b/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/resolve-entity/resolve-entity-beans.xml
@@ -12,6 +12,8 @@
 
     <bean id="shibboleth.oidc.loggingId" class="java.lang.String" c:_0="%{idp.service.logging.oidfedresolve:OIDFED.ResolveEntity}" />
 
+    <bean id="shibboleth.oidc.browserProfile" class="java.lang.Boolean" c:_0="false" />
+
     <util:constant id="shibboleth.metrics.ProfileCounter"
         static-field="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.config.impl.DefaultOIDFederationResolveEntityProfileConfiguration.PROFILE_COUNTER" />
 
@@ -28,14 +30,16 @@
         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="shibboleth.ClientIDLookupStrategy" parent="shibboleth.Functions.Expression" c:expression="#input.getMessage().getClientAuthentication() != null ? #input.getMessage().getClientAuthentication().getClientID() : 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="InitializeAuthenticationContext"
+        class="net.shibboleth.idp.saml.profile.impl.InitializeAuthenticationContext" scope="prototype" />
+
     <bean id="shibboleth.oidfed.ResolveEntityResponseMetadataCache" parent="shibboleth.oidc.CacheBuilder">
         <constructor-arg>
             <bean p:cacheId="DefaultResolveEntityResponseMetadataCache" parent="shibboleth.oidfed.ResolveEntityResponseMetadataCacheBuilderSpec"
@@ -78,7 +82,6 @@
     <bean id="ResolveTrustChains" class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.ResolveTrustChains"
         scope="prototype"
         p:trustChainCache-ref="#{'%{idp.oidfed.resolve-entity.TrustChainMetadataCache:shibboleth.oidfed.TrustChainMetadataCache}'.trim()}"
-        p:clientIDLookupStrategy-ref="shibboleth.ClientIDLookupStrategy"
         p:metadataPolicyMergingStrategy-ref="#{'%{idp.oidfed.resolve-entity.TrustChainMetadataPolicyMergingStrategy:DefaultTrustChainMetadataPolicyMergingStrategy}'.trim()}"
         p:metadataPolicyEnforcer-ref="#{'%{idp.oidfed.resolve-entity.MetadataPolicyEnforcer:DefaultMetadataPolicyEnforcer}'.trim()}"
         p:preSelectedTrustChainIdsLookupStrategy="#{getObject('shibboleth.oidfed.PreSelectedTrustChainIDsLookupStrategy') ?: getObject('shibboleth.oidfed.DefaultPreSelectedTrustChainIDsLookupStrategy')}"
@@ -88,6 +91,9 @@
         <property name="metadataLookupStrategy">
             <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.navigate.DefaultCombinedMetadataFromTrustChainLookupStrategy"/>
         </property>
+        <property name="clientIDLookupStrategy">
+            <bean parent="shibboleth.Functions.Expression" c:expression="new com.nimbusds.oauth2.sdk.id.ClientID(#input.getMessage().getSubject())" />
+        </property>
     </bean>
 
     <bean id="DefaultMetadataValidationCondition"
diff --git a/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/resolve-entity/resolve-entity-flow.xml b/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/resolve-entity/resolve-entity-flow.xml
index e1a17f0..fb65540 100644
--- a/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/resolve-entity/resolve-entity-flow.xml
+++ b/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/resolve-entity/resolve-entity-flow.xml
@@ -1,7 +1,7 @@
 <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">
+    parent="oidc/abstract, oidc/metadata-lookup">
 
     <action-state id="InitializeMandatoryContexts">
         <evaluate expression="InitializeProfileRequestContext" />
@@ -11,14 +11,23 @@
         <evaluate expression="'proceed'" />
         
         <transition on="proceed" to="DecodeMessage">
-            <set name="flowScope.transitionAfterDecode" value="'SelectConfiguration'" />
+            <set name="flowScope.transitionAfterDecode" value="'PostDecode'" />
          </transition>
     </action-state>
 
+    <action-state id="PostDecode">
+        <on-entry>
+            <set name="flowScope.skipOAuth2ClientAuth" value="opensamlProfileRequestContext.getInboundMessageContext().getMessage().getClientAuthentication() == null" />
+        </on-entry>
+        <evaluate expression="'proceed'"/>
+        <transition on="proceed" to="#{skipOAuth2ClientAuth ? 'SelectConfiguration' : 'DoMetadataLookup'}" />
+    </action-state>
+
     <action-state id="SelectConfiguration">
         <evaluate expression="InitializeRelyingPartyContext" />
         <evaluate expression="SelectRelyingPartyConfiguration" />
         <evaluate expression="SelectProfileConfiguration" />
+        <evaluate expression="CallInboundMessageHandler" />
         <evaluate expression="PostLookupPopulateAuditContext" />
         <evaluate expression="PopulateInboundInterceptContext" />
         <evaluate expression="'proceed'" />
@@ -27,19 +36,33 @@
     </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" />
+            then="#{skipOAuth2ClientAuth ? 'ResumeAfterAuthentication' : 'AuthenticationSetup'}" else="DoInboundInterceptSubflow" />
     </decision-state>
 
     <subflow-state id="DoInboundInterceptSubflow" subflow="intercept">
         <input name="calledAsSubflow" value="true" />
-        <transition on="proceed" to="LookupCachedResponse" />
+        <transition on="proceed" to="#{skipOAuth2ClientAuth ? 'ResumeAfterAuthentication' : 'AuthenticationSetup'}" />
+    </subflow-state>
+
+    <action-state id="AuthenticationSetup">
+        <evaluate expression="CallInboundMessageHandler" />
+        <evaluate expression="InitializeAuthenticationContext" />
+        <evaluate expression="'proceed'" />
+        <transition on="proceed" to="DoAuthenticationSubflow" />
+    </action-state>
+
+    <subflow-state id="DoAuthenticationSubflow" subflow="authn">
+        <input name="calledAsSubflow" value="true" />
+        <input name="bypassSessionActions" value="true" />
+        <input name="potentialFlows" value="getActiveFlow().getApplicationContext().getBean('shibboleth.oidfed.resolver.PotentialFlows')" />
+        <transition on="proceed" to="ResumeAfterAuthentication" />
+        <transition on="RestartAuthentication" to="AuthenticationSetup" />
     </subflow-state>
 
-    <action-state id="LookupCachedResponse">
+    <!-- Authentication subflow happens here. -->
+
+    <action-state id="ResumeAfterAuthentication">
         <evaluate expression="ValidateRequest" />
         <evaluate expression="LookupCachedResolveEntityResponse" />
         <evaluate expression="'proceed'" />
@@ -72,6 +95,17 @@
         <transition on="proceed" to="BuildResponseMessage" />
     </action-state>
 
+    <action-state id="HandleError">
+        <on-entry>
+            <evaluate
+                expression="opensamlProfileRequestContext.ensureSubcontext(T(net.shibboleth.idp.profile.context.SpringRequestContext)).setRequestContext(flowRequestContext)" />
+            <evaluate expression="LogEvent" />
+        </on-entry>
+        <evaluate expression="BuildErrorResponseFromEvent" />
+        <evaluate expression="'proceed'" />
+        <transition on="proceed" to="PopulateOutboundInterceptContext"/>
+    </action-state>
+
     <bean-import resource="resolve-entity-beans.xml" />
 
 </flow>
\ No newline at end of file
diff --git a/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml b/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml
index 2e2b53b..1a70414 100644
--- a/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml
+++ b/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml
@@ -53,7 +53,12 @@
         p:mandatoryTrustMarks="%{idp.oidfed.explicitRegistration.mandatoryTrustMarks:}" />
 
     <bean id="OIDFED.ResolveEntity" parent="AbstractOIDFederationProfile" lazy-init="true"
-        class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.config.impl.DefaultOIDFederationResolveEntityProfileConfiguration" />
+        class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.config.impl.DefaultOIDFederationResolveEntityProfileConfiguration"
+        p:issuer-ref="shibboleth.oidc.issuer"
+        p:tokenEndpointAuthMethods="%{idp.oidfed.resolve-entity.endpointAuthMethods:private_key_jwt}"
+        p:claimsValidator="#{getObject('DefaultJWTClaimsValidator')}"
+        p:useTargetedEndpointAsJWTAudience="%{idp.oidfed.resolve-entity.targetedEndpointAsJWTAudience:false}"
+        p:requireSingleJWTAudience="%{idp.oidfed.resolve-entity.requireSingleJWTAudience:true}"/>
 
     <bean id="shibboleth.oidfed.SigningConfiguration"
         parent="shibboleth.oidc.BasicSignatureSigningConfiguration"
@@ -86,6 +91,30 @@
         class="net.shibboleth.oidc.profile.config.CredentialsListFactory"
         c:_0="#{getObject('shibboleth.oidfed.SigningCredentials') ?: getObject('shibboleth.oidc.SigningCredentials')}" />
 
+    <bean id="shibboleth.oidfed.SignatureValidationConfiguration" 
+        parent="shibboleth.oidc.BasicSignatureValidationConfiguration">
+        <property name="signatureTrustEngine">
+            <bean class="net.shibboleth.oidc.security.impl.ExplicitKeySignedJWTTrustEngine"
+                c:JOSEObjectResolver-ref="defaultSignedJWTJOSEHeaderCredentialResolver">
+                <constructor-arg name="resolver">
+                    <bean id="defaultSignedJWTFedTrustedCredentialResolver"
+                        class="net.shibboleth.oidc.security.credential.impl.ChainingJOSEObjectCredentialResolver">
+                        <constructor-arg>
+                            <list>
+                                <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.security.credential.ClientInformationFederationEntityCredentialResolver"
+                                    c:objectMapper-ref="shibboleth.oidfed.JWTPayloadJSONObjectMapper">
+                                    <constructor-arg name="entityConfigurationCredentialResolver">
+                                        <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.security.credential.DefaultEntityConfigurationCredentialResolver" />
+                                    </constructor-arg>
+                                </bean>
+                            </list>
+                        </constructor-arg>
+                    </bean>
+                </constructor-arg>
+            </bean>
+        </property>
+    </bean>
+
     <bean id="shibboleth.oidfed.DefaultSecurityConfiguration"
         class="net.shibboleth.oidc.profile.config.JSONSecurityConfiguration" c:clockSkew="%{idp.policy.clockSkew:PT1M}">
         <constructor-arg name="idGenerator">
@@ -100,7 +129,7 @@
             <ref bean="#{'%{idp.oidfed.signing.config:shibboleth.oidfed.SigningConfiguration}'.trim()}" />
         </property>
         <property name="jwtSignatureValidationConfiguration">
-            <ref bean="#{'%{idp.oidfed.validation.config:shibboleth.oidc.SignatureValidationConfiguration}'.trim()}" />
+            <ref bean="#{'%{idp.oidfed.validation.config:shibboleth.oidfed.SignatureValidationConfiguration}'.trim()}" />
         </property>
     </bean>
 
diff --git a/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/ResolveEntityFlowTest.java b/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/ResolveEntityFlowTest.java
index bd8a31d..f8c9c49 100644
--- a/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/ResolveEntityFlowTest.java
+++ b/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/ResolveEntityFlowTest.java
@@ -18,9 +18,13 @@ import java.io.IOException;
 import java.net.URISyntaxException;
 import java.time.Instant;
 import java.util.Date;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
+import org.opensaml.storage.StorageService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.webflow.executor.FlowExecutionResult;
 import org.testng.Assert;
 import org.testng.annotations.Test;
@@ -31,6 +35,7 @@ import com.nimbusds.jose.jwk.JWKSet;
 import com.nimbusds.jwt.JWTClaimsSet;
 import com.nimbusds.jwt.SignedJWT;
 import com.nimbusds.oauth2.sdk.Response;
+import com.nimbusds.oauth2.sdk.Scope;
 
 import net.shibboleth.idp.plugin.oidc.op.oidfed.TrustChainTestUtil;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.messaging.impl.ResolveEntityResponse;
@@ -45,7 +50,11 @@ import net.minidev.json.JSONObject;
 public class ResolveEntityFlowTest extends AbstractFederationFlowTest {
     
     public static final String FLOW_ID = "oidfed/resolve-entity";
-    
+
+    @Autowired
+    @Qualifier("shibboleth.StorageService")
+    StorageService storageService;
+
     public ResolveEntityFlowTest() {
         super(FLOW_ID);
     }
@@ -57,9 +66,18 @@ public class ResolveEntityFlowTest extends AbstractFederationFlowTest {
         assertErrorCode(result, "invalid_request");
     }
 
+    @Test
+    public void testNoContentType() throws Exception {
+        request.setMethod("GET");
+        request.setQueryString("sub=mockClientId&trust_anchor=" + anchorId + "&entity_type=openid_relying_party");
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        assertErrorCode(result, "invalid_request");
+    }
+
     @Test
     public void testInvalidSubject() throws Exception {
         request.setMethod("GET");
+        request.setContentType("application/x-www-form-urlencoded");
         request.setQueryString("sub=mockClientId&trust_anchor=" + anchorId + "&entity_type=openid_relying_party");
         final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
         assertErrorCode(result, "invalid_subject");
@@ -70,6 +88,7 @@ public class ResolveEntityFlowTest extends AbstractFederationFlowTest {
         request.setMethod("GET");
         final String clientId = uniqueClientId();
         rpConfigureMockHttpClient(clientId);
+        request.setContentType("application/x-www-form-urlencoded");
         request.setQueryString("sub=" + clientId + "&trust_anchor=mockAnchors&entity_type=openid_relying_party");
         final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
         assertErrorCode(result, "invalid_trust_anchor");
@@ -80,6 +99,7 @@ public class ResolveEntityFlowTest extends AbstractFederationFlowTest {
         request.setMethod("GET");
         final String clientId = uniqueClientId();
         rpConfigureMockHttpClient(clientId);
+        request.setContentType("application/x-www-form-urlencoded");
         request.setQueryString("sub=" + clientId + "&trust_anchor=" + anchorId + "&entity_type=openid_relying_party");
         final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
         final ResolveEntityResponse parsedResponse =
@@ -96,6 +116,7 @@ public class ResolveEntityFlowTest extends AbstractFederationFlowTest {
         request.setMethod("GET");
         final String clientId = uniqueClientId();
         rpConfigureMockHttpClient(clientId, initializeNewJwk("RSA", 2048, "mockNewLeafKey"));
+        request.setContentType("application/x-www-form-urlencoded");
         request.setQueryString("sub=" + clientId + "&trust_anchor=" + anchorId + "&entity_type=openid_relying_party");
         final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
         assertErrorCode(result, "invalid_request");
@@ -107,6 +128,7 @@ public class ResolveEntityFlowTest extends AbstractFederationFlowTest {
         request.setMethod("GET");
         final String clientId = uniqueClientId();
         rpConfigureMockHttpClient(clientId, new JSONObject(Map.of("response_types", "invalid")));
+        request.setContentType("application/x-www-form-urlencoded");
         request.setQueryString("sub=" + clientId + "&trust_anchor=" + anchorId + "&entity_type=openid_relying_party");
         final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
         assertErrorCode(result, "invalid_metadata");
@@ -117,6 +139,7 @@ public class ResolveEntityFlowTest extends AbstractFederationFlowTest {
         request.setMethod("GET");
         final String entityId = uniqueClientId();
         opConfigureMockHttpClient(entityId);
+        request.setContentType("application/x-www-form-urlencoded");
         request.setQueryString("sub=" + entityId + "&trust_anchor=" + anchorId + "&entity_type=openid_provider");
         final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
         final ResolveEntityResponse parsedResponse =
@@ -133,6 +156,7 @@ public class ResolveEntityFlowTest extends AbstractFederationFlowTest {
         request.setMethod("GET");
         final String entityId = uniqueClientId();
         opConfigureMockHttpClient(entityId, new JSONObject(Map.of("issuer", List.of("unexpected", "values"))));
+        request.setContentType("application/x-www-form-urlencoded");
         request.setQueryString("sub=" + entityId + "&trust_anchor=" + anchorId + "&entity_type=openid_provider");
         final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
         assertErrorCode(result, "invalid_metadata");
@@ -158,6 +182,7 @@ public class ResolveEntityFlowTest extends AbstractFederationFlowTest {
         } catch (UnsupportedOperationException | IOException | URISyntaxException e) {
             Assert.fail("Could not initialize mock HTTP client", e);
         }
+        request.setContentType("application/x-www-form-urlencoded");
         request.setQueryString("sub=" + entityId + "&trust_anchor=" + anchorId + "&entity_type=openid_provider");
         final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
         final ResolveEntityResponse parsedResponse =
@@ -169,6 +194,127 @@ public class ResolveEntityFlowTest extends AbstractFederationFlowTest {
         Assert.assertNull(response.getJWTClaimsSet().getClaim("authority_hints"));
     }
 
+    @Test
+    public void testRPWithTrustedTrustAnchor_jwtAuth_successWithLeafKey() throws Exception {
+        request.setMethod("POST");
+        final String requestingClientId = uniqueClientId();
+        final String clientId = uniqueClientId();
+        final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
+                .issuer(requestingClientId)
+                .subject(requestingClientId)
+                .audience(issuer)
+                .expirationTime(Date.from(Instant.now().plusSeconds(600)))
+                .jwtID(idGenerator.generateIdentifier())
+                .build();
+
+        final SignedJWT jwt = createPrivateKeyJWT(claimsSet, leafKey.toRSAKey().toRSAPrivateKey());
+        final Map<String, String> requestParams = new HashMap<>();
+        requestParams.put("sub", clientId);
+        requestParams.put("trust_anchor", anchorId);
+        requestParams.put("entity_type", "openid_relying_party");
+        populateClientAssertionParams(requestParams, jwt);
+        rpConfigureMockHttpClient(requestingClientId);
+        rpConfigureMockHttpClient(clientId);
+        request.setContentType("application/x-www-form-urlencoded");
+        setHttpFormRequest(request, "POST", requestParams);
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        final ResolveEntityResponse parsedResponse =
+                parseSuccessResponse(result, ResolveEntityResponse.class);
+        final SignedJWT response = parsedResponse.getJWT();
+        Assert.assertEquals(response.getHeader().getType(), new JOSEObjectType("resolve-response+jwt"));
+        Assert.assertEquals(response.getJWTClaimsSet().getSubject(), clientId);
+        Assert.assertNotNull(response.getJWTClaimsSet().getClaim("metadata"));
+        Assert.assertNull(response.getJWTClaimsSet().getClaim("authority_hints"));
+    }
+
+    @Test
+    public void testRPWithTrustedTrustAnchor_basicAuth_successForLocalClient() throws Exception {
+        request.setMethod("POST");
+        final String registeredClientId = "localResolveEntityClient";
+        final String secret = "mockClientSecret";
+        storeMetadata(storageService, registeredClientId, secret, Scope.parse("openid"), redirectUri);
+        final String clientId = uniqueClientId();
+        final Map<String, String> requestParams = new HashMap<>();
+        requestParams.put("sub", clientId);
+        requestParams.put("trust_anchor", anchorId);
+        requestParams.put("entity_type", "openid_relying_party");
+        setBasicAuth(registeredClientId, secret);
+
+        rpConfigureMockHttpClient(clientId);
+        request.setContentType("application/x-www-form-urlencoded");
+        setHttpFormRequest(request, "POST", requestParams);
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        final ResolveEntityResponse parsedResponse =
+                parseSuccessResponse(result, ResolveEntityResponse.class);
+        final SignedJWT response = parsedResponse.getJWT();
+        Assert.assertEquals(response.getHeader().getType(), new JOSEObjectType("resolve-response+jwt"));
+        Assert.assertEquals(response.getJWTClaimsSet().getSubject(), clientId);
+        Assert.assertNotNull(response.getJWTClaimsSet().getClaim("metadata"));
+        Assert.assertNull(response.getJWTClaimsSet().getClaim("authority_hints"));
+    }
+
+    @Test
+    public void testRPWithTrustedTrustAnchor_basicAuth_failForLocalClientWithDefaultConfig() throws Exception {
+        request.setMethod("POST");
+        final String registeredClientId = "localDefaultClient";
+        final String secret = "mockClientSecret";
+        storeMetadata(storageService, registeredClientId, secret, Scope.parse("openid"), redirectUri);
+        final String clientId = uniqueClientId();
+        final Map<String, String> requestParams = new HashMap<>();
+        requestParams.put("sub", clientId);
+        requestParams.put("trust_anchor", anchorId);
+        requestParams.put("entity_type", "openid_relying_party");
+        setBasicAuth(registeredClientId, secret);
+
+        rpConfigureMockHttpClient(clientId);
+        request.setContentType("application/x-www-form-urlencoded");
+        setHttpFormRequest(request, "POST", requestParams);
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        assertErrorCode(result, "unauthorized_client");
+    }
+
+    @Test
+    public void testRPWithTrustedTrustAnchor_basicAuth_failWithGet() throws Exception {
+        request.setMethod("GET");
+        final String registeredClientId = "localResolveEntityClient";
+        final String secret = "mockClientSecret";
+        storeMetadata(storageService, registeredClientId, secret, Scope.parse("openid"), redirectUri);
+        final String clientId = uniqueClientId();
+        request.setContentType("application/x-www-form-urlencoded");
+        request.setQueryString("sub=" + clientId + "&trust_anchor=" + anchorId + "&entity_type=openid_relying_party");
+        setBasicAuth(registeredClientId, secret);
+
+        rpConfigureMockHttpClient(clientId);
+        request.setContentType("application/x-www-form-urlencoded");
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        assertErrorCode(result, "invalid_request");
+    }
+
+    @Test
+    public void testRPWithTrustedTrustAnchor_jwtAuth_failWithRpfKey() throws Exception {
+        request.setMethod("POST");
+        final String clientId = uniqueClientId();
+        final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
+                .issuer(clientId)
+                .subject(clientId)
+                .audience(issuer)
+                .expirationTime(Date.from(Instant.now().plusSeconds(600)))
+                .jwtID(idGenerator.generateIdentifier())
+                .build();
+
+        final SignedJWT jwt = createPrivateKeyJWT(claimsSet, rpKey.toRSAKey().toRSAPrivateKey());
+        final Map<String, String> requestParams = new HashMap<>();
+        requestParams.put("sub", clientId);
+        requestParams.put("trust_anchor", anchorId);
+        requestParams.put("entity_type", "openid_relying_party");
+        populateClientAssertionParams(requestParams, jwt);
+        rpConfigureMockHttpClient(clientId);
+        request.setContentType("application/x-www-form-urlencoded");
+        setHttpFormRequest(request, "POST", requestParams);
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        assertErrorCode(result, "invalid_client");
+    }
+
     protected JSONErrorResponse parseErrorResponse(final FlowExecutionResult result) {
         final Response response = parseResponse(result);
         Assert.assertTrue(response instanceof JSONErrorResponse);
diff --git a/idp-oidfed-op-impl/src/test/resources/net/shibboleth/idp/module/conf/relying-party.xml b/idp-oidfed-op-impl/src/test/resources/net/shibboleth/idp/module/conf/relying-party.xml
index 60ce13d..c3148c8 100644
--- a/idp-oidfed-op-impl/src/test/resources/net/shibboleth/idp/module/conf/relying-party.xml
+++ b/idp-oidfed-op-impl/src/test/resources/net/shibboleth/idp/module/conf/relying-party.xml
@@ -74,6 +74,15 @@
                     <bean parent="OAUTH2.Token.MDDriven" p:tokenEndpointAuthMethods="client_secret_basic,client_secret_post,client_secret_jwt,private_key_jwt,none"/>
                     <bean parent="OAUTH2.PAR.MDDriven" p:tokenEndpointAuthMethods="private_key_jwt,none"/>
                     <ref bean="OIDC.UserInfo.MDDriven" />
+                    <!-- Enabled for federation-authenticated clients -->
+                    <bean parent="OIDFED.ResolveEntity" />
+                </list>
+            </property>
+        </bean>
+        <bean parent="RelyingPartyByName" c:relyingPartyIds="localResolveEntityClient">
+            <property name="profileConfigurations">
+                <list>
+                    <bean parent="OIDFED.ResolveEntity" p:tokenEndpointAuthMethods="client_secret_basic"/>
                 </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