[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