[java-oidfed-common] branch main updated: Imported 'resolve-entity' flow from the OP's OIDFED plugin
Codeberg
noreply at shibboleth.net
Tue Sep 22 15:43:19 UTC 2026
This is an automated email from the git hooks/post-receive script.
codeberg pushed a commit to branch main
in repository java-oidfed-common.
View the commit online:
https://codeberg.org/Shibboleth/java-oidfed-common/commit/b95418629127f77d9bca13b13e9e039e2ec4c95a
The following commit(s) were added to refs/heads/main by this push:
new b954186 Imported 'resolve-entity' flow from the OP's OIDFED plugin
b954186 is described below
commit b95418629127f77d9bca13b13e9e039e2ec4c95a
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Tue Sep 22 18:42:08 2026 +0300
Imported 'resolve-entity' flow from the OP's OIDFED plugin
- Changed the features to not require OP's codebase
- Similar set of flow tests
---
.../context/RelyingPartyConfigurationSupport.java | 61 +++
.../context/logic/TrustAnchorIdPredicate.java | 98 +++++
.../navigate/TrustAnchorIdLookupFunction.java | 94 +++++
oidfed-common-conf-impl/pom.xml | 5 +
.../META-INF/net.shibboleth.idp/postconfig.xml | 69 +++-
.../oidfed/resolve-entity/resolve-entity-beans.xml | 361 ++++++++++++++++
.../oidfed/resolve-entity/resolve-entity-flow.xml | 213 ++++++++++
.../idp/service/relying-party/postconfig.xml | 81 +++-
.../oidfed/flow/AbstractFederationFlowTest.java | 1 -
.../oidfed/flow/ResolveEntityFlowTest.java | 455 +++++++++++++++++++++
.../src/test/resources/credentials/htpasswd.txt | 2 +
.../oauth2client-authn-config.xml} | 33 +-
.../shibboleth/idp/module/conf/relying-party.xml | 16 +
.../decoding/impl/ResolveEntityRequestDecoder.java | 110 +++++
.../BuildResolveEntityErrorResponseFromEvent.java | 269 ++++++++++++
.../profile/impl/BuildResolveEntityResponse.java | 204 +++++++++
.../impl/FormOutboundResolveEntityResponse.java | 228 +++++++++++
.../impl/LookupCachedResolveEntityResponse.java | 157 +++++++
.../ValidateResolveEntityProfileConfiguration.java | 231 +++++++++++
.../profile/impl/ValidateResolveEntityRequest.java | 186 +++++++++
.../profile/impl/ValidateSelectedTrustChain.java | 231 +++++++++++
.../navigate/DefaultEntityTypesLookupFunction.java | 46 +++
...ormationFederationEntityCredentialResolver.java | 128 ------
...ustChainFederationEntityCredentialResolver.java | 124 ++++++
.../oidfed/conf/oidfed/oidfed.properties | 17 +
25 files changed, 3270 insertions(+), 150 deletions(-)
diff --git a/oidfed-common-api/src/main/java/net/shibboleth/oidfed/profile/context/RelyingPartyConfigurationSupport.java b/oidfed-common-api/src/main/java/net/shibboleth/oidfed/profile/context/RelyingPartyConfigurationSupport.java
new file mode 100644
index 0000000..7fc7431
--- /dev/null
+++ b/oidfed-common-api/src/main/java/net/shibboleth/oidfed/profile/context/RelyingPartyConfigurationSupport.java
@@ -0,0 +1,61 @@
+/*
+ * 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.oidfed.profile.context;
+
+import java.util.Collection;
+
+import javax.annotation.Nonnull;
+
+import net.shibboleth.oidfed.profile.context.logic.TrustAnchorIdPredicate;
+import net.shibboleth.profile.relyingparty.BasicRelyingPartyConfiguration;
+import net.shibboleth.profile.relyingparty.RelyingPartyConfiguration;
+import net.shibboleth.shared.logic.Constraint;
+
+/**
+ * Support functions for building {@link RelyingPartyConfiguration} objects with activation conditions.
+ */
+public class RelyingPartyConfigurationSupport {
+
+ /**
+ * A shorthand method for constructing a {@link BasicRelyingPartyConfiguration} with an activation condition
+ * based on one or more trust anchor IDs.
+ *
+ * <p>If a single ID is supplied, then the ID is also set as the identifier for the configuration.</p>
+ *
+ * @param trustAnchorIds the trust anchors for which the configuration should be active
+ *
+ * @return a default-constructed configuration with the appropriate condition set
+ */
+ @Nonnull
+ public static BasicRelyingPartyConfiguration byTrustAnchor(@Nonnull final Collection<String> trustAnchorIds) {
+
+ Constraint.isNotNull(trustAnchorIds, "Trust Anchor ID list cannot be null");
+
+ final BasicRelyingPartyConfiguration config = new BasicRelyingPartyConfiguration();
+ config.setActivationCondition(new TrustAnchorIdPredicate(trustAnchorIds));
+
+ final StringBuffer name = new StringBuffer("TrustAnchorIDs[");
+ for (final String taId: trustAnchorIds) {
+ name.append(taId).append(',');
+
+ }
+ name.append(']');
+ final String id = name.toString();
+ assert id != null;
+ config.setId(id);
+ return config;
+ }
+
+}
diff --git a/oidfed-common-api/src/main/java/net/shibboleth/oidfed/profile/context/logic/TrustAnchorIdPredicate.java b/oidfed-common-api/src/main/java/net/shibboleth/oidfed/profile/context/logic/TrustAnchorIdPredicate.java
new file mode 100644
index 0000000..4ff2a99
--- /dev/null
+++ b/oidfed-common-api/src/main/java/net/shibboleth/oidfed/profile/context/logic/TrustAnchorIdPredicate.java
@@ -0,0 +1,98 @@
+/*
+ * 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.oidfed.profile.context.logic;
+
+import java.util.Collection;
+import java.util.function.Predicate;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import net.shibboleth.oidfed.profile.context.navigate.TrustAnchorIdLookupFunction;
+
+import net.shibboleth.shared.annotation.ParameterName;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.logic.StrategyIndirectedPredicate;
+import net.shibboleth.shared.primitive.StringSupport;
+
+/**
+ * Predicate that evaluates a {@link ProfileRequestContext} by looking for a trust anchor ID that matches one of a
+ * designated set, or a generic predicate.
+ */
+public class TrustAnchorIdPredicate extends StrategyIndirectedPredicate<ProfileRequestContext,String> {
+
+ /**
+ * Constructor.
+ *
+ * @param candidates hardwired set of values to check against
+ */
+ public TrustAnchorIdPredicate(@Nonnull @ParameterName(name="candidates") final Collection<String> candidates) {
+ super(new TrustAnchorIdLookupFunction(), StringSupport.normalizeStringCollection(candidates));
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param candidate a single value to check against
+ */
+ public TrustAnchorIdPredicate(@Nonnull @NotEmpty @ParameterName(name="candidate") final String candidate) {
+ this(CollectionSupport.singleton(candidate));
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param pred generalized predicate
+ */
+ public TrustAnchorIdPredicate(@Nonnull @ParameterName(name="pred") final Predicate<String> pred) {
+ super(new TrustAnchorIdLookupFunction(), pred);
+ }
+
+ /**
+ * Workaround for Spring type conversion ambiguities.
+ *
+ * @param candidates hardwired set of values to check against
+ *
+ * @return the predicate
+ */
+ @Nonnull public static TrustAnchorIdPredicate fromCandidates(@Nonnull final Collection<String> candidates) {
+ return new TrustAnchorIdPredicate(candidates);
+ }
+
+ /**
+ * Workaround for Spring type conversion ambiguities.
+ *
+ * @param candidate a single value to check against
+ *
+ * @return the predicate
+ */
+ @Nonnull public static TrustAnchorIdPredicate fromCandidate(@Nonnull @NotEmpty final String candidate) {
+ return new TrustAnchorIdPredicate(candidate);
+ }
+
+ /**
+ * Workaround for Spring type conversion ambiguities.
+ *
+ * @param pred generalized predicate
+ *
+ * @return the predicate
+ */
+ @Nonnull public static TrustAnchorIdPredicate fromPredicate(@Nonnull final Predicate<String> pred) {
+ return new TrustAnchorIdPredicate(pred);
+ }
+
+}
diff --git a/oidfed-common-api/src/main/java/net/shibboleth/oidfed/profile/context/navigate/TrustAnchorIdLookupFunction.java b/oidfed-common-api/src/main/java/net/shibboleth/oidfed/profile/context/navigate/TrustAnchorIdLookupFunction.java
new file mode 100644
index 0000000..8faef10
--- /dev/null
+++ b/oidfed-common-api/src/main/java/net/shibboleth/oidfed/profile/context/navigate/TrustAnchorIdLookupFunction.java
@@ -0,0 +1,94 @@
+/*
+ * 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.oidfed.profile.context.navigate;
+
+import java.util.Optional;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.ThreadSafe;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
+
+import net.shibboleth.oidc.metadata.context.OIDCMetadataContext;
+import net.shibboleth.oidfed.profile.context.RelyingPartyTrustChainContext;
+import net.shibboleth.oidfed.support.ClientInformationExtensionSupport;
+import net.shibboleth.shared.logic.Constraint;
+
+/**
+ * A function that primarily returns {@link ClientInformationExtensionSupport#KEY_VALIDATED_TRUST_ANCHOR} if found from
+ * the client information resolved via {@link OIDCMetadataContext} and secondarily takes the subject of the last entity
+ * statement in the {@link RelyingPartyTrustChainContext#getSelectedTrustChain()} that is expected to be the anchor of
+ * the selected trust chain.
+ *
+ * <p>If a specific setting is unavailable, a null value is returned.</p>
+ */
+ at ThreadSafe
+public class TrustAnchorIdLookupFunction implements Function<ProfileRequestContext, String> {
+
+ /** Strategy used to lookup the OIDC metadata context. */
+ @Nonnull private Function<ProfileRequestContext, OIDCMetadataContext> oidcMetadataContextLookupStrategy;
+
+ /** Strategy used to lookup the trust chain context. */
+ @Nonnull private Function<ProfileRequestContext, RelyingPartyTrustChainContext> trustChainContextLookupStrategy;
+
+ /**
+ * Constructor.
+ */
+ public TrustAnchorIdLookupFunction() {
+ final Function<ProfileRequestContext,OIDCMetadataContext> omcls =
+ new ChildContextLookup<>(OIDCMetadataContext.class).compose(
+ new InboundMessageContextLookup());
+ assert omcls != null;
+ oidcMetadataContextLookupStrategy = omcls;
+ final Function<ProfileRequestContext, RelyingPartyTrustChainContext> tcls =
+ new ChildContextLookup<>(RelyingPartyTrustChainContext.class).compose(
+ new InboundMessageContextLookup());
+ assert tcls != null;
+ trustChainContextLookupStrategy = tcls;
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param oidcMetadataStrategy strategy used to lookup the OIDC metadata context
+ * @param trustChainStrategy strategy used to lookup the trust chain context
+ */
+ public TrustAnchorIdLookupFunction(
+ @Nonnull final Function<ProfileRequestContext, OIDCMetadataContext> oidcMetadataStrategy,
+ @Nonnull final Function<ProfileRequestContext, RelyingPartyTrustChainContext> trustChainStrategy) {
+ oidcMetadataContextLookupStrategy = Constraint.isNotNull(oidcMetadataStrategy,
+ "OIDC metadata context lookup strategy cannot be null");
+ trustChainContextLookupStrategy = Constraint.isNotNull(trustChainStrategy,
+ "Trust chain context lookup strategy cannot be null");
+ }
+
+ /** {@inheritDoc} */
+ @Nullable public String apply(@Nullable final ProfileRequestContext profileRequestContext) {
+ return Optional.ofNullable(oidcMetadataContextLookupStrategy.apply(profileRequestContext))
+ .map(oidcContext -> oidcContext.getClientInformation())
+ .map(clientInfo -> clientInfo != null ?
+ ClientInformationExtensionSupport.parseValidatedTrustAnchor(clientInfo) : null)
+ .orElse(Optional.ofNullable(trustChainContextLookupStrategy.apply(profileRequestContext))
+ .map(trustChainContext -> trustChainContext.getSelectedTrustChain())
+ .map(verifiedChain -> verifiedChain.getTrustChain())
+ .map(trustChain -> trustChain.get(trustChain.size() - 1))
+ .map(anchorStatement -> anchorStatement.getSubject())
+ .orElse(null));
+ }
+}
diff --git a/oidfed-common-conf-impl/pom.xml b/oidfed-common-conf-impl/pom.xml
index cc3d193..847a518 100644
--- a/oidfed-common-conf-impl/pom.xml
+++ b/oidfed-common-conf-impl/pom.xml
@@ -70,6 +70,11 @@
<artifactId>oidc-common-profile-impl</artifactId>
<scope>provided</scope>
</dependency>
+ <dependency>
+ <groupId>${oidc-common.groupId}</groupId>
+ <artifactId>oidc-common-conf-impl</artifactId>
+ <scope>provided</scope>
+ </dependency>
<dependency>
<groupId>${idp.groupId}</groupId>
<artifactId>idp-admin-api</artifactId>
diff --git a/oidfed-common-conf-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml b/oidfed-common-conf-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
index d0e5188..42bd010 100644
--- a/oidfed-common-conf-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
+++ b/oidfed-common-conf-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
@@ -322,7 +322,7 @@
p:defaultSupportedOperators-ref="#{'%{idp.oidfed.MetadataPolicyOperators:shibboleth.oidfed.StandardMetadataPolicyOperators}'.trim()}">
<property name="supportedOperators">
<util:map>
- <entry key="net.shibboleth.oidfed.messaging.impl.ResolveEntityRequest" value-ref="#{'%{idp.oidfed.resolve-entity.MetadataPolicyOperators:shibboleth.oidfed.StandardMetadataPolicyOperators}'.trim()}" />
+ <entry key="net.shibboleth.oidfed.messaging.impl.ResolveEntityRequest" value-ref="#{'%{idp.oidfed.resolveEntity.MetadataPolicyOperators:shibboleth.oidfed.StandardMetadataPolicyOperators}'.trim()}" />
<entry key="net.shibboleth.oidfed.messaging.impl.ExplicitClientRegistrationRequest" value-ref="#{'%{idp.oidfed.register.MetadataPolicyOperators:shibboleth.oidfed.StandardMetadataPolicyOperators}'.trim()}" />
</util:map>
</property>
@@ -1118,6 +1118,73 @@
class="net.shibboleth.oidfed.profile.impl.BiConsumerEntityConfigurationMetadataDecorator"
abstract="true" />
+ <bean id="shibboleth.oidfed.DefaultApiMappedErrors"
+ class="org.springframework.beans.factory.config.MapFactoryBean">
+ <property name="sourceMap">
+ <map value-type="com.nimbusds.oauth2.sdk.ErrorObject">
+ <entry key="#{T(net.shibboleth.idp.authn.AuthnEventIds).AUTHN_EXCEPTION}"
+ value="#{T(com.nimbusds.oauth2.sdk.OAuth2Error).INVALID_CLIENT}" />
+ <entry key="#{T(net.shibboleth.idp.authn.AuthnEventIds).SUBJECT_C14N_ERROR}"
+ value="#{T(com.nimbusds.oauth2.sdk.OAuth2Error).INVALID_CLIENT}" />
+ <entry key="#{T(net.shibboleth.idp.authn.AuthnEventIds).UNKNOWN_USERNAME}"
+ value="#{T(com.nimbusds.oauth2.sdk.OAuth2Error).INVALID_CLIENT}" />
+ <entry key="#{T(net.shibboleth.idp.authn.AuthnEventIds).NO_CREDENTIALS}"
+ value="#{T(com.nimbusds.oauth2.sdk.OAuth2Error).INVALID_CLIENT}" />
+ <entry key="#{T(net.shibboleth.idp.authn.AuthnEventIds).INVALID_CREDENTIALS}"
+ value="#{T(com.nimbusds.oauth2.sdk.OAuth2Error).INVALID_CLIENT}" />
+ <entry key="#{T(net.shibboleth.idp.authn.AuthnEventIds).ACCOUNT_LOCKED}"
+ value="#{T(com.nimbusds.oauth2.sdk.OAuth2Error).INVALID_CLIENT}" />
+ <entry key="#{T(net.shibboleth.idp.authn.AuthnEventIds).ACCOUNT_ERROR}"
+ value="#{T(com.nimbusds.oauth2.sdk.OAuth2Error).INVALID_CLIENT}" />
+ <entry key="#{T(net.shibboleth.idp.authn.AuthnEventIds).RESELECT_FLOW}"
+ value="#{T(com.nimbusds.oauth2.sdk.OAuth2Error).INVALID_CLIENT}" />
+ <entry key="#{T(net.shibboleth.idp.authn.AuthnEventIds).NO_POTENTIAL_FLOW}"
+ value="#{T(com.nimbusds.oauth2.sdk.OAuth2Error).INVALID_CLIENT}" />
+ <entry key="#{T(net.shibboleth.idp.profile.IdPEventIds).INVALID_PROFILE_CONFIG}"
+ value="#{T(com.nimbusds.oauth2.sdk.OAuth2Error).UNAUTHORIZED_CLIENT}" />
+ <entry key="#{T(org.opensaml.profile.action.EventIds).ACCESS_DENIED}"
+ value="#{T(com.nimbusds.oauth2.sdk.OAuth2Error).ACCESS_DENIED}" />
+ </map>
+ </property>
+ </bean>
+
+ <bean id="shibboleth.oidc.DefaultResolveEntityApiMappedErrors"
+ parent="shibboleth.oidfed.DefaultApiMappedErrors"
+ class="org.springframework.beans.factory.config.MapFactoryBean">
+ <property name="sourceMap">
+ <map merge="true" value-type="com.nimbusds.oauth2.sdk.ErrorObject">
+ <entry>
+ <key>
+ <util:constant static-field="org.opensaml.profile.action.EventIds.INVALID_MSG_CTX"/>
+ </key>
+ <bean class="com.nimbusds.oauth2.sdk.ErrorObject" c:_0="server_error" c:_1="Internal server error" c:_2="500" />
+ </entry>
+ <entry>
+ <key>
+ <util:constant static-field="net.shibboleth.oidfed.profile.OidFederationEventIds.INVALID_TRUST_ANCHOR"/>
+ </key>
+ <bean class="com.nimbusds.oauth2.sdk.ErrorObject" c:_0="invalid_trust_anchor" c:_1="Trust anchor in the request is invalid" c:_2="404" />
+ </entry>
+ <entry>
+ <key>
+ <util:constant static-field="net.shibboleth.oidfed.profile.OidFederationEventIds.INVALID_SUBJECT"/>
+ </key>
+ <bean class="com.nimbusds.oauth2.sdk.ErrorObject" c:_0="invalid_subject" c:_1="Subject in the request is invalid" c:_2="404" />
+ </entry>
+ <entry>
+ <key>
+ <util:constant static-field="net.shibboleth.oidfed.profile.OidFederationEventIds.INVALID_METADATA"/>
+ </key>
+ <bean class="com.nimbusds.oauth2.sdk.ErrorObject" c:_0="invalid_metadata" c:_1="Metadata is invalid or not found for the requested entity types" c:_2="400" />
+ </entry>
+ </map>
+ </property>
+ </bean>
+
+ <!-- Property-based definition of login flows for the resolve-entity endpoint. -->
+ <bean id="shibboleth.oidfed.resolve-entity.PotentialFlows" class="org.springframework.beans.factory.config.ListFactoryBean"
+ p:sourceList="#{getObject('shibboleth.AuthenticationFlowDescriptorManager').getComponents().?[id matches 'authn/(' + '%{idp.oidfed.resolveEntity.authn.flows:OAuth2Client}'.trim() + ')']}" />
+
<import resource="${idp.home}/conf/oidfed/oidfed-trustchain-resolver.xml"/>
</beans>
\ No newline at end of file
diff --git a/oidfed-common-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/resolve-entity/resolve-entity-beans.xml b/oidfed-common-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/resolve-entity/resolve-entity-beans.xml
new file mode 100644
index 0000000..5c34d90
--- /dev/null
+++ b/oidfed-common-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/resolve-entity/resolve-entity-beans.xml
@@ -0,0 +1,361 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans" xmlns:c="http://www.springframework.org/schema/c"
+ xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p"
+ xmlns:util="http://www.springframework.org/schema/util" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
+ http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
+ http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"
+ default-init-method="initialize" default-destroy-method="destroy">
+
+ <bean id="shibboleth.oidfed.profileId" class="java.lang.String"
+ c:_0="#{T(net.shibboleth.oidfed.profile.config.OIDFederationResolveEntityProfileConfiguration).PROFILE_ID}" />
+
+ <bean id="shibboleth.oidfed.loggingId" class="java.lang.String" c:_0="%{idp.service.logging.oidfedresolve:OIDFED.ResolveEntity}" />
+
+ <bean id="shibboleth.oidfed.browserProfile" class="java.lang.Boolean" c:_0="false" />
+
+ <alias alias="shibboleth.oidfed.trust-chain-resolver.UseResolverApiCondition"
+ name="%{idp.oidfed.resolveEntity.trustchain.resolver.useResolverApiCondition:shibboleth.Conditions.FALSE}" />
+
+ <alias alias="shibboleth.oidfed.trust-chain-resolver.FallbackToLocalResolutionCondition"
+ name="%{idp.oidfed.resolveEntity.trustchain.resolver.fallbackToLocalCondition:shibboleth.Conditions.FALSE}" />
+
+ <alias alias="ResolveTrustChainsCondition"
+ name="%{idp.oidfed.resolveEntity.resolveTrustChainsCondition:DefaultResolveTrustChainsCondition}" />
+
+ <bean id="DefaultResolveTrustChainsCondition" parent="shibboleth.Conditions.Expression"
+ c:expression="#input.getInboundMessageContext().getMessage().getClientAuthentication() instanceof T(com.nimbusds.oauth2.sdk.auth.PrivateKeyJWT)" />
+
+ <bean id="TrustChainCandidatesExist" parent="shibboleth.Conditions.Expression"
+ c:expression="#input.ensureInboundMessageContext().containsSubcontext(T(net.shibboleth.oidfed.profile.context.RelyingPartyTrustChainContext)) and #input.ensureInboundMessageContext().ensureSubcontext(T(net.shibboleth.oidfed.profile.context.RelyingPartyTrustChainContext)).getPolicyCompliantTrustChains() != null and #input.ensureInboundMessageContext().ensureSubcontext(T(net.shibboleth.oidfed.profile.context.RelyingPartyTrustChainContext)).getPolicyCompliantTrustChains().size() > 0"/>
+
+ <bean id="InitializeProfileRequestContext"
+ class="net.shibboleth.idp.profile.impl.InitializeProfileRequestContext" scope="prototype"
+ p:profileId-ref="shibboleth.oidfed.profileId"
+ p:loggingId-ref="shibboleth.oidfed.loggingId"
+ p:browserProfile-ref="shibboleth.oidfed.browserProfile" />
+
+ <bean id="CallInboundMessageHandler"
+ class="net.shibboleth.idp.profile.impl.WebFlowMessageHandlerAdaptor" scope="prototype"
+ c:executionDirection="INBOUND">
+ <constructor-arg name="messageHandler">
+ <bean class="org.opensaml.messaging.handler.impl.FunctionMessageHandler" scope="prototype">
+ <property name="functionLookupStrategy">
+ <bean class="net.shibboleth.oidc.profile.config.navigate.MessageHandlerLookupFunction" />
+ </property>
+ </bean>
+ </constructor-arg>
+ <property name="errorEvent">
+ <util:constant static-field="org.opensaml.profile.action.EventIds.MESSAGE_PROC_ERROR" />
+ </property>
+ </bean>
+
+ <bean id="PopulateMetricContext"
+ class="org.opensaml.profile.action.impl.PopulateMetricContext" scope="prototype"
+ p:counterName="#{getObject('shibboleth.metrics.ProfileCounter')}"
+ p:metricStrategy="#{getObject('shibboleth.metrics.MetricStrategy')}" />
+
+ <util:constant id="shibboleth.metrics.ProfileCounter"
+ static-field="net.shibboleth.oidfed.profile.config.impl.DefaultOIDFederationResolveEntityProfileConfiguration.PROFILE_COUNTER" />
+
+ <bean id="DecodeMessage" class="org.opensaml.profile.action.impl.DecodeMessage" scope="prototype">
+ <constructor-arg>
+ <bean class="net.shibboleth.oidfed.decoding.impl.ResolveEntityRequestDecoder" scope="prototype"
+ p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier" />
+ <!-- TODO: support custom parsing? p:customRequestParser="#{getObject('%{idp.oidfed.requestParser.ResolveEntityRequest:}'.trim())}"/>-->
+ </constructor-arg>
+ </bean>
+
+ <bean id="InitializeAuthenticationContext"
+ class="net.shibboleth.idp.saml.profile.impl.InitializeAuthenticationContext" scope="prototype" />
+
+ <bean id="shibboleth.oidfed.ResolveEntityResponseMetadataCache" parent="shibboleth.oidfed.CacheBuilder">
+ <constructor-arg>
+ <bean p:cacheId="DefaultResolveEntityResponseMetadataCache" parent="shibboleth.oidfed.ResolveEntityResponseMetadataCacheBuilderSpec"
+ p:cleanupTaskInterval="PT30S"/>
+ </constructor-arg>
+ </bean>
+
+ <bean id="SelectRelyingPartyConfiguration"
+ class="net.shibboleth.idp.profile.impl.SelectRelyingPartyConfiguration" scope="prototype"
+ p:relyingPartyConfigurationResolver-ref="shibboleth.RelyingPartyResolverService" />
+
+ <bean id="SelectProfileConfiguration" class="net.shibboleth.idp.profile.impl.SelectProfileConfiguration"
+ scope="prototype" />
+
+ <bean id="PopulateInboundInterceptContext"
+ class="net.shibboleth.idp.profile.interceptor.impl.PopulateProfileInterceptorContext" scope="prototype"
+ p:availableFlows="#{@'shibboleth.ProfileInterceptorFlowDescriptorManager'.getComponents()}"
+ p:loggingLabel="inbound">
+ <property name="activeFlowsLookupStrategy">
+ <bean class="net.shibboleth.idp.profile.config.navigate.InboundFlowsLookupFunction" />
+ </property>
+ </bean>
+
+ <bean id="ValidateResolveEntityProfileConfiguration"
+ class="net.shibboleth.oidfed.profile.impl.ValidateResolveEntityProfileConfiguration"
+ scope="prototype">
+ <property name="subjectEntityIdLookupStrategy">
+ <bean parent="shibboleth.Functions.Expression"
+ c:expression="#input.getInboundMessageContext()?.getMessage().getClientAuthentication().getClientID()?.getValue()" />
+ </property>
+ </bean>
+
+ <bean id="shibboleth.oidfed.trust-chain-resolver.PreSelectedTrustChainIDsLookupStrategy"
+ parent="shibboleth.Functions.Expression" c:expression="#null" />
+
+ <bean id="shibboleth.oidfed.ResolveEntityResponseMetadataCacheBuilderSpec"
+ class="net.shibboleth.oidc.metadata.cache.impl.DynamicMetadataCacheBuilderSpec"
+ p:minCacheDuration="%{idp.oidfed.cache.resolveEntity.minRefreshDelay:PT1S}"
+ p:maxCacheDuration="%{idp.oidfed.cache.resolveEntity.maxRefreshDelay:PT30S}">
+ <property name="criteriaToIdentifierStrategy">
+ <bean parent="shibboleth.Functions.Expression"
+ c:expression="#input?.get(T(net.shibboleth.oidfed.metadata.cache.local.ResolveEntityRequestCriterion))?.getRequest().toString()"/>
+ </property>
+ <property name="identifierExtractionStrategy">
+ <bean parent="shibboleth.Functions.Expression"
+ c:expression="#input?.getRequest()?.toString()"/>
+ </property>
+ <property name="metadataExpirationTimeStrategy">
+ <bean class="net.shibboleth.oidfed.metadata.cache.local.DefaultNimbusResponseContainerExpirationTimeStrategy"/>
+ </property>
+ <property name="metadataFilterStrategy">
+ <bean parent="shibboleth.BiFunctions.Expression" c:expression="#input1"/>
+ </property>
+ <property name="fetchStrategy">
+ <bean class="net.shibboleth.oidfed.metadata.cache.local.DefaultResolveEntityResponseFetchingStrategy" />
+ </property>
+ </bean>
+
+ <bean id="ValidateRequest" class="net.shibboleth.oidfed.profile.impl.ValidateResolveEntityRequest"
+ scope="prototype"
+ p:localTrustAnchorsCache-ref="#{'%{idp.oidfed.resolveEntity.LocalTrustAnchorsMetadataCache:shibboleth.oidfed.LocalTrustAnchorsMetadataCache}'.trim()}" />
+
+ <bean id="LookupCachedResolveEntityResponse"
+ class="net.shibboleth.oidfed.profile.impl.LookupCachedResolveEntityResponse"
+ scope="prototype"
+ p:responseCache-ref="shibboleth.oidfed.ResolveEntityResponseMetadataCache" />
+
+ <bean id="shibboleth.oidfed.trust-chain-resolver.EntityIDLookupStrategy"
+ parent="shibboleth.Functions.Expression"
+ c:expression="'true'.equals(#input.getSubcontext(T(net.shibboleth.idp.authn.context.AuthenticationContext))?.getAuthenticationStateMap()?.get('idp.resolve-entity.resolveForAuthentication')) ? #input.getInboundMessageContext()?.getMessage().getClientAuthentication()?.getClientID()?.getValue() : #input.getInboundMessageContext()?.getMessage()?.getSubject()"/>
+
+ <bean id="DefaultMetadataPolicyEnforcer"
+ class="net.shibboleth.oidfed.metadata.policy.impl.DefaultFederationMetadataPolicyEnforcer"
+ p:metadataPolicyOperators-ref="#{'%{idp.oidfed.resolveEntity.MetadataPolicyOperators:shibboleth.oidfed.StandardMetadataPolicyOperators}'.trim()}"/>
+
+ <bean id="DefaultTrustChainMetadataPolicyMergingStrategy"
+ class="net.shibboleth.oidfed.profile.navigate.DefaultTrustChainMetadataPolicyMergingStrategy"
+ p:metadataPolicyMergingStrategy-ref="#{'%{idp.oidfed.resolveEntity.MetadataPolicyMergingStrategy:MetadataPolicyMergingStrategy}'.trim()}"
+ p:localMetadataPolicyStrategy-ref="#{'%{idp.oidfed.resolveEntity.LocalMetadataPolicyStrategy:DefaultLocalMetadataPolicyStrategy}'.trim()}"/>
+
+ <bean id="MetadataPolicyMergingStrategy"
+ class="net.shibboleth.oidfed.metadata.policy.impl.DefaultFederationMetadataPolicyMergingStrategy"
+ p:metadataPolicyOperators-ref="#{'%{idp.oidfed.resolveEntity.MetadataPolicyOperators:shibboleth.oidfed.StandardMetadataPolicyOperators}'.trim()}"/>
+
+ <bean id="DefaultLocalMetadataPolicyStrategy"
+ parent="shibboleth.Functions.Constant">
+ <constructor-arg name="target">
+ <util:map/>
+ </constructor-arg>
+ </bean>
+
+ <bean id="SelectTrustChain" class="net.shibboleth.oidfed.profile.impl.SelectTrustChain"
+ scope="prototype">
+ <property name="activationCondition">
+ <bean parent="shibboleth.Conditions.Expression"
+ c:expression="#input.ensureInboundMessageContext().containsSubcontext(T(net.shibboleth.oidfed.profile.context.RelyingPartyTrustChainContext))" />
+ </property>
+ </bean>
+
+ <bean id="ValidateSelectedTrustChain" class="net.shibboleth.oidfed.profile.impl.ValidateSelectedTrustChain"
+ scope="prototype"
+ p:entityConfigurationCache-ref="shibboleth.oidfed.EntityConfigurationMetadataCache"/>
+
+ <bean id="ResolveTrustMarks" class="net.shibboleth.oidfed.profile.impl.ResolveTrustMarks"
+ scope="prototype"
+ p:trustChainCache-ref="#{'%{idp.oidfed.resolveEntity.TrustChainMetadataCache:shibboleth.oidfed.TrustChainMetadataCache}'.trim()}"
+ p:trustMarkClaimsValidationLookupStrategy="#{getObject('shibboleth.oidfed.TrustMarkClaimsValidationLookupStrategy') ?: getObject('DefaultTrustMarkClaimsValidationLookupStrategy')}"
+ p:delegatedTrustMarkClaimsValidationLookupStrategy="#{getObject('shibboleth.oidfed.DelegatedTrustMarkClaimsValidationLookupStrategy') ?: getObject('DefaultDelegatedTrustMarkClaimsValidationLookupStrategy')}"
+ p:trustEngine-ref="shibboleth.oidfed.DefaultEntityConfigurationTrustEngine"
+ p:delegationTrustEngine-ref="shibboleth.oidfed.DefaultDelegatedTrustMarkTrustEngine">
+ <property name="trustChainTrustMarksParsingStrategy">
+ <bean class="net.shibboleth.oidfed.profile.navigate.DefaultTrustChainTrustMarksParsingStrategy"
+ p:objectMapper-ref="shibboleth.oidfed.JWTPayloadJSONObjectMapper"/>
+ </property>
+ <property name="trustedTrustMarkIssuersLookupStrategy">
+ <bean class="net.shibboleth.oidfed.profile.navigate.DefaultTrustChainTrustedTrustMarkIssuersLookupStrategy"/>
+ </property>
+ <property name="trustedTrustMarkOwnersLookupStrategy">
+ <bean class="net.shibboleth.oidfed.profile.navigate.DefaultTrustChainTrustedTrustMarkOwnersLookupStrategy"/>
+ </property>
+ </bean>
+
+ <bean id="ValidateTrustMarks" class="net.shibboleth.oidfed.profile.impl.ValidateTrustMarks"
+ scope="prototype"
+ p:trustMarkStatusCache-ref="#{'%{idp.oidfed.resolveEntity.TrustMarkStatusMetadataCache:shibboleth.oidfed.TrustMarkStatusMetadataCache}'.trim()}">
+ </bean>
+
+ <bean id="PopulateResolveResponseSignatureSigningParameters"
+ class="net.shibboleth.oidc.profile.impl.PopulateJWTSignatureSigningParameters" scope="prototype"
+ c:strategy-ref="shibboleth.MessageContextLookup.Outbound"
+ p:securityParametersContextLookupStrategy-ref="ResolveResponseSecurityParametersContextLookupStrategy">
+ <property name="configurationLookupStrategy">
+ <bean lazy-init="true"
+ class="net.shibboleth.oidc.profile.config.navigate.JWTSignatureSigningConfigurationLookupFunction" />
+ </property>
+ <property name="signatureSigningParametersResolver">
+ <bean class="net.shibboleth.oidc.security.jose.impl.ClientInformationSignatureSigningParametersResolver">
+ <constructor-arg name="signatureAlgorithmLookupStrategy">
+ <bean parent="shibboleth.Functions.Constant" c:target="" />
+ </constructor-arg>
+ <constructor-arg name="defaultAlgorithmValue" value="%{idp.oidfed.resolveEntity.sigalg:RS256}" />
+ </bean>
+ </property>
+ </bean>
+
+ <bean id="ResolveResponseSecurityParametersContextLookupStrategy" parent="shibboleth.Functions.Compose"
+ c:g-ref="shibboleth.oidfed.ChildLookupOrCreate.JWTSecurityParameters"
+ c:f-ref="shibboleth.ChildLookup.RelyingParty" />
+
+ <bean id="shibboleth.oidfed.ChildLookupOrCreate.JWTSecurityParameters"
+ class="org.opensaml.messaging.context.navigate.ChildContextLookup"
+ c:type="#{ T(net.shibboleth.oidc.security.jose.context.SecurityParametersContext) }"
+ c:createContext="true" />
+
+ <bean id="ResolveResponseSecurityParametersCreationViaMessageContextStrategy" parent="shibboleth.Functions.Compose">
+ <constructor-arg name="g" ref="ResolveResponseSecurityParametersContextLookupStrategy" />
+ <constructor-arg name="f">
+ <bean parent="shibboleth.Functions.Expression" c:expression="#input.getParent()" />
+ </constructor-arg>
+ </bean>
+
+ <bean id="BuildResolveResponse"
+ class="net.shibboleth.oidfed.profile.impl.BuildResolveEntityResponse" scope="prototype"
+ p:identifierGeneratorLookupStrategy-ref="shibboleth.oidfed.DefaultIdentifierGenerationStrategy"
+ p:objectMapper-ref="#{'%{idp.oidfed.logging.objectMapper:shibboleth.oidfed.JWTPayloadJSONObjectMapper}'.trim()}">
+ <property name="subjectLookupStrategy">
+ <bean parent="shibboleth.Functions.Expression"
+ c:expression="#input.ensureInboundMessageContext().getMessage().getSubject()" />
+ </property>
+ </bean>
+
+ <bean id="SignResolveResponse" class="net.shibboleth.idp.profile.impl.WebFlowMessageHandlerAdaptor"
+ scope="prototype" c:executionDirection="OUTBOUND ">
+ <constructor-arg name="messageHandler">
+ <bean id="SignResolveResponseHandler"
+ class="net.shibboleth.oidc.security.impl.SignJWTHandler" scope="prototype" p:logName="Resolve Response"
+ p:securityParametersLookupStrategy-ref="ResolveResponseSecurityParametersCreationViaMessageContextStrategy"
+ p:typeHeader="resolve-response+jwt">
+ <property name="claimsToSignLookupStrategy">
+ <bean
+ class="net.shibboleth.oidfed.profile.impl.JWTClaimsSetFromEntityStatementLookupFunction" />
+ </property>
+ <property name="jwtUpdateConsumer">
+ <bean
+ class="net.shibboleth.oidfed.profile.impl.EntityStatementUpdateStrategy" />
+ </property>
+ </bean>
+ </constructor-arg>
+ </bean>
+
+ <bean id="FormOutboundMessage"
+ class="net.shibboleth.oidfed.profile.impl.FormOutboundResolveEntityResponse" scope="prototype"
+ p:responseCache-ref="shibboleth.oidfed.ResolveEntityResponseMetadataCache" />
+
+ <bean id="BuildErrorResponseFromEvent"
+ class="net.shibboleth.oidfed.profile.impl.BuildResolveEntityErrorResponseFromEvent" scope="prototype"
+ p:httpServletResponseSupplier-ref="shibboleth.HttpServletResponseSupplier"
+ p:mappedErrors="#{getObject('shibboleth.oidfed.resolve-entity.MappedErrors') ?: getObject('shibboleth.oidc.DefaultResolveEntityApiMappedErrors')}"
+ p:responseCache-ref="shibboleth.oidfed.ResolveEntityResponseMetadataCache">
+ <property name="eventContextLookupStrategy">
+ <bean class="net.shibboleth.idp.profile.context.navigate.WebFlowCurrentEventLookupFunction" />
+ </property>
+ </bean>
+
+ <bean id="LogEvent" class="org.opensaml.profile.action.impl.LogEvent" scope="prototype"
+ p:suppressedEvents="#{getObject('shibboleth.SuppressedEvents') ?: getObject('shibboleth.DefaultSuppressedEvents')}">
+ <property name="eventContextLookupStrategy">
+ <bean class="net.shibboleth.idp.profile.context.navigate.WebFlowCurrentEventLookupFunction" />
+ </property>
+ </bean>
+
+ <bean id="FlowStartPopulateAuditContext" parent="shibboleth.AbstractPopulateAuditContext"
+ p:fieldExtractors="#{getObject('shibboleth.FlowStartAuditExtractors') ?: getObject('shibboleth.DefaultFlowStartAuditExtractors')}" />
+
+ <bean id="PostDecodePopulateAuditContext" parent="shibboleth.AbstractPopulateAuditContext"
+ p:fieldExtractors="#{getObject('shibboleth.oidfed.resolve-entity-PostDecodeAuditExtractors') ?: getObject('shibboleth.oidfed.resolve-entity.DefaultPostDecodeAuditExtractors')}" />
+
+ <bean id="PostLookupPopulateAuditContext" parent="shibboleth.AbstractPopulateAuditContext"
+ p:fieldExtractors="#{getObject('shibboleth.oidfed.resolve-entity.PostLookupAuditExtractors') ?: getObject('shibboleth.oidfed.resolve-entity.DefaultPostLookupAuditExtractors')}" />
+
+ <bean id="PostResponsePopulateAuditContext" parent="shibboleth.AbstractPopulateAuditContext"
+ p:fieldExtractors="#{getObject('shibboleth.oidfed.resolve-entity.PostResponseAuditExtractors') ?: getObject('shibboleth.oidfed.resolve-entity.DefaultPostResponseAuditExtractors')}" />
+
+ <bean id="shibboleth.oidfed.resolve-entity.DefaultPostDecodeAuditExtractors"
+ class="org.springframework.beans.factory.config.MapFactoryBean">
+ <property name="sourceMap">
+ <map/>
+ </property>
+ </bean>
+
+ <bean id="shibboleth.oidfed.resolve-entity.DefaultPostLookupAuditExtractors"
+ class="org.springframework.beans.factory.config.MapFactoryBean">
+ <property name="sourceMap">
+ <map/>
+ </property>
+ </bean>
+
+ <bean id="shibboleth.oidfed.resolve-entity.DefaultPostResponseAuditExtractors"
+ class="org.springframework.beans.factory.config.MapFactoryBean">
+ <property name="sourceMap">
+ <map/>
+ </property>
+ </bean>
+
+ <bean id="PopulateOutboundInterceptContext"
+ class="net.shibboleth.idp.profile.interceptor.impl.PopulateProfileInterceptorContext" scope="prototype"
+ p:availableFlows="#{@'shibboleth.ProfileInterceptorFlowDescriptorManager'.getComponents()}"
+ p:loggingLabel="outbound">
+ <property name="activeFlowsLookupStrategy">
+ <bean class="net.shibboleth.idp.profile.config.navigate.OutboundFlowsLookupFunction" />
+ </property>
+ </bean>
+
+ <bean id="CallOutboundMessageHandler"
+ class="net.shibboleth.idp.profile.impl.WebFlowMessageHandlerAdaptor" scope="prototype"
+ c:executionDirection="OUTBOUND">
+ <constructor-arg name="messageHandler">
+ <bean class="org.opensaml.messaging.handler.impl.FunctionMessageHandler" scope="prototype">
+ <property name="functionLookupStrategy">
+ <bean class="net.shibboleth.oidc.profile.config.navigate.MessageHandlerLookupFunction" />
+ </property>
+ </bean>
+ </constructor-arg>
+ <property name="errorEvent">
+ <util:constant static-field="org.opensaml.profile.action.EventIds.MESSAGE_PROC_ERROR" />
+ </property>
+ </bean>
+
+ <bean id="WriteAuditLog" class="net.shibboleth.idp.profile.audit.impl.WriteAuditLog" scope="prototype"
+ p:activationCondition-ref="shibboleth.ProfileAuditingCondition"
+ p:formattingMap-ref="shibboleth.AuditFormattingMap"
+ p:dateTimeFormat="#{getObject('shibboleth.AuditDateTimeFormat')}"
+ p:useDefaultTimeZone="#{getObject('shibboleth.AuditDefaultTimeZone') ?: false}"
+ p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier" />
+
+ <bean id="EncodeMessage" class="org.opensaml.profile.action.impl.EncodeMessage" scope="prototype"
+ p:messageEncoderFactory-ref="oidc.messageEncoderFactory"
+ p:httpServletResponseSupplier-ref="shibboleth.HttpServletResponseSupplier" />
+
+ <bean id="oidc.messageEncoderFactory"
+ class="net.shibboleth.oidc.profile.encoding.impl.OIDCResponseEncoderFactory"
+ p:messageEncoder-ref="oidc.nimbusEncoder" scope="prototype" />
+
+ <bean id="oidc.nimbusEncoder" class="net.shibboleth.oidc.profile.encoding.impl.SimpleNimbusResponseEncoder"
+ scope="prototype" p:httpServletResponseSupplier-ref="shibboleth.HttpServletResponseSupplier" init-method=""/>
+
+ <bean id="RecordResponseComplete" class="net.shibboleth.idp.profile.impl.RecordResponseComplete"
+ scope="prototype" />
+
+</beans>
diff --git a/oidfed-common-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/resolve-entity/resolve-entity-flow.xml b/oidfed-common-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/resolve-entity/resolve-entity-flow.xml
new file mode 100644
index 0000000..06d81be
--- /dev/null
+++ b/oidfed-common-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/resolve-entity/resolve-entity-flow.xml
@@ -0,0 +1,213 @@
+<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="oidfed/resolve-trust-chains">
+
+ <action-state id="InitializeProfileRequestContext">
+ <evaluate expression="InitializeProfileRequestContext" />
+ <evaluate expression="PopulateMetricContext" />
+ <evaluate expression="'proceed'" />
+ <transition on="proceed" to="InitializeMandatoryContexts" />
+ </action-state>
+
+ <action-state id="InitializeMandatoryContexts">
+ <on-entry>
+ <evaluate expression="opensamlProfileRequestContext.ensureInboundMessageContext()"/>
+ <evaluate expression="opensamlProfileRequestContext.ensureOutboundMessageContext()"/>
+ <evaluate expression="opensamlProfileRequestContext.ensureSubcontext(T(net.shibboleth.profile.context.RelyingPartyContext))" />
+ </on-entry>
+ <evaluate expression="FlowStartPopulateAuditContext" />
+ <evaluate expression="DecodeMessage" />
+ <evaluate expression="PostDecodePopulateAuditContext" />
+ <evaluate expression="'proceed'" />
+ <transition on="proceed" to="CheckIfContainsAuthentication" />
+ </action-state>
+
+ <decision-state id="CheckIfContainsAuthentication">
+ <on-entry>
+ <set name="flowScope.skipOAuth2ClientAuth" value="opensamlProfileRequestContext.getInboundMessageContext().getMessage().getClientAuthentication() == null" />
+ </on-entry>
+ <if test="flowScope.skipOAuth2ClientAuth"
+ then="SelectConfiguration" else="CheckIfResolveTrustChains" />
+ </decision-state>
+
+ <decision-state id="CheckIfResolveTrustChains">
+ <on-entry>
+ <evaluate expression="InitializeAuthenticationContext" />
+ <evaluate expression="opensamlProfileRequestContext.getSubcontext(T(net.shibboleth.idp.authn.context.AuthenticationContext)).getAuthenticationStateMap().put('idp.resolve-entity.resolveForAuthentication', 'true')"/>
+ <set name="conversationScope.automaticallyRegistered" value="false" />
+ <evaluate expression="opensamlProfileRequestContext.ensureSubcontext(T(net.shibboleth.profile.context.RelyingPartyContext)).setRelyingPartyId(opensamlProfileRequestContext.getInboundMessageContext()?.getMessage().getClientAuthentication().getClientID()?.getValue())" />
+ <evaluate expression="opensamlProfileRequestContext.ensureSubcontext(T(net.shibboleth.profile.context.RelyingPartyContext)).setVerified(true)" />
+ </on-entry>
+ <if test="ResolveTrustChainsCondition.test(opensamlProfileRequestContext)"
+ then="InitializeResolveTrustChains" else="SelectConfiguration" />
+ </decision-state>
+
+ <action-state id="InitializeResolveTrustChains">
+ <evaluate expression="'proceed'" />
+ <transition on="proceed" to="ChooseResolutionMethod">
+ <set name="flowScope.transitionAfterTrustChainResolution" value="'CheckIfContinueTrustChainResolution'" />
+ <set name="flowScope.transitionOnNoTrustChainsResolved" value="'NoTrustChainsResolved'" />
+ <set name="flowScope.transitionForReselectTrustChain" value="'SelectFederationConfiguration'" />
+ </transition>
+ </action-state>
+
+ <decision-state id="CheckIfContinueTrustChainResolution">
+ <if test="TrustChainCandidatesExist.test(opensamlProfileRequestContext)"
+ then="SelectFederationConfiguration" else="SelectConfiguration" />
+ </decision-state>
+
+ <action-state id="SelectFederationConfiguration">
+ <evaluate expression="SelectTrustChain" />
+ <evaluate expression="ResolveTrustMarks" />
+ <evaluate expression="SelectRelyingPartyConfiguration" />
+ <evaluate expression="SelectProfileConfiguration" />
+ <evaluate expression="ValidateTrustMarks" />
+ <evaluate expression="ValidateResolveEntityProfileConfiguration" />
+ <evaluate expression="'proceed'" />
+ <transition on="ReselectTrustChain" to="#{transitionForReselectTrustChain}" />
+ <transition on="proceed" to="SelectConfiguration">
+ <set name="conversationScope.automaticallyRegistered" value="opensamlProfileRequestContext.ensureInboundMessageContext().containsSubcontext(T(net.shibboleth.oidc.metadata.context.OIDCMetadataContext))" />
+ </transition>
+ </action-state>
+
+ <action-state id="SelectConfiguration">
+ <on-entry>
+ <evaluate expression="opensamlProfileRequestContext.getSubcontext(T(net.shibboleth.idp.authn.context.AuthenticationContext))?.getAuthenticationStateMap()?.put('idp.resolve-entity.resolveForAuthentication', 'false')"/>
+ </on-entry>
+ <evaluate expression="SelectRelyingPartyConfiguration" />
+ <evaluate expression="SelectProfileConfiguration" />
+ <evaluate expression="CallInboundMessageHandler" />
+ <evaluate expression="PostLookupPopulateAuditContext" />
+ <evaluate expression="PopulateInboundInterceptContext" />
+ <evaluate expression="'proceed'" />
+
+ <transition on="proceed" to="CheckInboundInterceptContext" />
+ </action-state>
+
+ <decision-state id="CheckInboundInterceptContext">
+ <on-entry>
+ <evaluate expression="CallInboundMessageHandler" />
+ </on-entry>
+ <if test="opensamlProfileRequestContext.ensureSubcontext(T(net.shibboleth.idp.profile.context.ProfileInterceptorContext)).getAvailableFlows().isEmpty()"
+ then="#{skipOAuth2ClientAuth ? 'ResumeAfterAuthentication' : 'DoAuthenticationSubflow'}" else="DoInboundInterceptSubflow" />
+ </decision-state>
+
+ <subflow-state id="DoInboundInterceptSubflow" subflow="intercept">
+ <input name="calledAsSubflow" value="true" />
+ <transition on="proceed" to="#{skipOAuth2ClientAuth ? 'ResumeAfterAuthentication' : 'DoAuthenticationSubflow'}" />
+ </subflow-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.resolve-entity.PotentialFlows')" />
+ <transition on="proceed" to="ResumeAfterAuthentication" />
+ <transition on="RestartAuthentication" to="DoAuthenticationSubflow" />
+ </subflow-state>
+
+ <!-- Authentication subflow happens here. -->
+
+ <action-state id="ResumeAfterAuthentication">
+ <evaluate expression="ValidateRequest" />
+ <evaluate expression="LookupCachedResolveEntityResponse" />
+ <evaluate expression="'proceed'" />
+ <transition on="CachedResponseFound" to="BuildResponseMessage" />
+ <transition on="proceed" to="ResolveSubjectTrustChains" />
+ </action-state>
+
+ <action-state id="ResolveSubjectTrustChains">
+ <evaluate expression="ResolveTrustChains" />
+ <evaluate expression="'proceed'" />
+ <transition on="proceed" to="SelectSubjectTrustChain" />
+ </action-state>
+
+ <action-state id="SelectSubjectTrustChain">
+ <evaluate expression="SelectTrustChain" />
+ <evaluate expression="ValidateSelectedTrustChain" />
+ <evaluate expression="ResolveTrustMarks" />
+ <evaluate expression="ValidateTrustMarks" />
+ <evaluate expression="'proceed'" />
+ <transition on="ReselectTrustChain" to="SelectSubjectTrustChain" />
+ <transition on="proceed" to="BuildResponse" />
+ </action-state>
+
+ <action-state id="BuildResponse">
+ <evaluate expression="PopulateResolveResponseSignatureSigningParameters" />
+ <evaluate expression="BuildResolveResponse" />
+ <evaluate expression="SignResolveResponse" />
+ <evaluate expression="'proceed'" />
+
+ <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>
+
+ <action-state id="BuildResponseMessage">
+ <evaluate expression="FormOutboundMessage" />
+ <evaluate expression="'proceed'" />
+ <transition on="proceed" to="PopulateOutboundInterceptContext" />
+ </action-state>
+
+ <action-state id="PopulateOutboundInterceptContext">
+ <evaluate expression="PopulateOutboundInterceptContext" />
+ <evaluate expression="'proceed'" />
+ <transition on="proceed" to="CheckOutboundInterceptContext" />
+ </action-state>
+
+ <decision-state id="CheckOutboundInterceptContext">
+ <on-entry>
+ <evaluate expression="flowRequestContext.getFlowScope().get('transitionAfterOutboundIntercept') != null ? flowRequestContext.getFlowScope().get('transitionAfterOutboundIntercept') : 'CommitResponse'" result="flowScope.postOutboundInterceptTransition"/>
+ <evaluate expression="PopulateOutboundInterceptContext" />
+ </on-entry>
+ <if test="opensamlProfileRequestContext.ensureSubcontext(T(net.shibboleth.idp.profile.context.ProfileInterceptorContext)).getAvailableFlows().isEmpty()"
+ then="#{postOutboundInterceptTransition}" else="DoOutboundInterceptSubflow" />
+ </decision-state>
+
+ <subflow-state id="DoOutboundInterceptSubflow" subflow="intercept">
+ <input name="calledAsSubflow" value="true" />
+ <transition on="proceed" to="#{postOutboundInterceptTransition}" />
+ <transition to="HandleError" />
+ </subflow-state>
+
+ <!-- Passthrough state if an exception is thrown. -->
+ <action-state id="LogRuntimeException">
+ <on-entry>
+ <evaluate
+ expression="T(org.slf4j.LoggerFactory).getLogger('net.shibboleth.idp.plugin.oidc.op.profile').error('Uncaught runtime exception', flowExecutionException.getCause())" />
+ </on-entry>
+ <evaluate expression="'RuntimeException'" />
+ <transition to="HandleError" />
+ </action-state>
+
+ <!-- end state -->
+ <end-state id="CommitResponse">
+ <on-entry>
+ <evaluate expression="CallOutboundMessageHandler" />
+ <evaluate expression="EncodeMessage" />
+ <evaluate expression="PostResponsePopulateAuditContext" />
+ <evaluate expression="WriteAuditLog" />
+ <evaluate expression="RecordResponseComplete" />
+ </on-entry>
+ </end-state>
+
+ <!-- all unhandled non proceed results are turned into errors -->
+ <global-transitions>
+ <transition on-exception="java.lang.RuntimeException" to="LogRuntimeException" />
+ <transition on="#{!'proceed'.equals(currentEvent.id)}" to="HandleError" />
+ </global-transitions>
+
+ <bean-import resource="resolve-entity-beans.xml" />
+
+</flow>
\ No newline at end of file
diff --git a/oidfed-common-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml b/oidfed-common-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml
index d947aa6..428d517 100644
--- a/oidfed-common-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml
+++ b/oidfed-common-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml
@@ -19,6 +19,29 @@
p:authorityHints="%{idp.oidfed.entityConfiguration.authorityHints:https://example.org}"
p:optionalClaimsLookupStrategies-ref="shibboleth.oidfed.EntityConfigurationClaimsLookupStrategies" />
+ <bean id="OIDFED.ResolveEntity" parent="AbstractOIDFederationProfile" lazy-init="true"
+ class="net.shibboleth.oidfed.profile.config.impl.DefaultOIDFederationResolveEntityProfileConfiguration"
+ p:issuer-ref="shibboleth.oidfed.entityId"
+ p:tokenEndpointAuthMethods="%{idp.oidfed.resolveEntity.endpointAuthMethods:private_key_jwt}"
+ p:useTargetedEndpointAsJWTAudience="%{idp.oidfed.resolveEntity.targetedEndpointAsJWTAudience:true}"
+ p:requireSingleJWTAudience="%{idp.oidfed.resolveEntity.requireSingleJWTAudience:true}">
+ <property name="claimsValidator">
+ <bean class="net.shibboleth.oidc.security.jwt.claims.impl.ChainingJWTClaimsValidator">
+ <property name="claimValidators">
+ <util:list value-type="net.shibboleth.oidc.jwt.claims.ClaimsValidator">
+ <ref bean="OIDFedClientAuthenticationExpiryClaimsValidator" />
+ <ref bean="OIDFedClientAuthenticationNotBeforeClaimsValidator" />
+ <ref bean="OIDFedClientAuthenticationIssuedAtClaimsValidator" />
+ <ref bean="OIDFedClientAuthenticationIssuerClaimsValidator" />
+ <ref bean="OIDFedClientAuthenticationSubjectClaimsValidator" />
+ <ref bean="OIDFedClientAuthenticationDefaultAuthenticationAudienceClaimsValidator" />
+ <ref bean="OIDFedClientAuthenticationJWTIdentifierClaimsValidator" />
+ </util:list>
+ </property>
+ </bean>
+ </property>
+ </bean>
+
<bean id="shibboleth.oidfed.SigningConfiguration"
parent="shibboleth.oidfed.BasicSignatureSigningConfiguration"
p:signingCredentials-ref="shibboleth.oidfed.SigningCredentialsFactory">
@@ -62,8 +85,7 @@
class="net.shibboleth.oidc.security.credential.impl.ChainingJOSEObjectCredentialResolver">
<constructor-arg>
<list>
- <bean class="net.shibboleth.oidfed.security.credential.ClientInformationFederationEntityCredentialResolver"
- c:objectMapper-ref="shibboleth.oidfed.JWTPayloadJSONObjectMapper">
+ <bean class="net.shibboleth.oidfed.security.credential.SelectedTrustChainFederationEntityCredentialResolver">
<constructor-arg name="entityConfigurationCredentialResolver">
<bean class="net.shibboleth.oidfed.security.credential.DefaultEntityConfigurationCredentialResolver" />
</constructor-arg>
@@ -112,6 +134,61 @@
class="net.shibboleth.oidfed.profile.navigate.DefaultEntityConfigurationTrustMarksLookupStrategy"
p:trustMarkLookupStrategies-ref="shibboleth.oidfed.DefaultTrustMarkLookupStrategies"/>
+ <bean id="OIDFedClientAuthenticationExpiryClaimsValidator"
+ class="net.shibboleth.oidc.security.jwt.claims.impl.ExpiryClaimsValidator"
+ p:clockSkew="%{idp.policy.clockSkew:PT1M}" />
+
+ <bean id="OIDFedClientAuthenticationNotBeforeClaimsValidator"
+ class="net.shibboleth.oidc.security.jwt.claims.impl.NotBeforeClaimsValidator"
+ p:clockSkew="%{idp.policy.clockSkew:PT1M}" />
+
+ <bean id="OIDFedClientAuthenticationIssuedAtClaimsValidator"
+ class="net.shibboleth.oidc.security.jwt.claims.impl.IssuedAtClaimsValidator"
+ p:clockSkew="%{idp.policy.clockSkew:PT1M}"
+ p:messageLifetime="%{idp.policy.messageLifetime:PT1M}"
+ p:requiredRule="false" />
+
+ <bean id="OIDFedClientAuthenticationIssuerClaimsValidator"
+ class="net.shibboleth.oidc.security.jwt.claims.impl.ExactMatchClaimsValidator"
+ p:claimName="iss">
+ <property name="valueToMatchLookupStrategy">
+ <bean class="net.shibboleth.shared.logic.BiFunctionSupport"
+ factory-method="forFunctionOfFirstArg"
+ c:_0-ref="shibboleth.RelyingPartyIdLookup.Simple" />
+ </property>
+ </bean>
+
+ <bean id="OIDFedClientAuthenticationSubjectClaimsValidator"
+ class="net.shibboleth.oidc.security.jwt.claims.impl.ExactMatchClaimsValidator"
+ p:claimName="sub">
+ <property name="valueToMatchLookupStrategy">
+ <bean class="net.shibboleth.shared.logic.BiFunctionSupport"
+ factory-method="forFunctionOfFirstArg"
+ c:_0-ref="shibboleth.RelyingPartyIdLookup.Simple" />
+ </property>
+ </bean>
+
+ <bean id="OIDFedClientAuthenticationDefaultAuthenticationAudienceClaimsValidator"
+ class="net.shibboleth.oidc.security.jwt.claims.impl.JWTAuthenticationAudienceClaimsValidator">
+ <property name="audienceLookupStrategy">
+ <bean parent="shibboleth.BiFunctions.Expression"
+ c:expression="#custom.get().getRequestURL().toString()"
+ p:customObject-ref="shibboleth.HttpServletRequestSupplier" />
+ </property>
+ <property name="extraAudienceValidationCondition">
+ <bean class="net.shibboleth.oidc.profile.config.navigate.RequireSingleJWTAudienceLookupFunction"/>
+ </property>
+ </bean>
+
+ <bean id="OIDFedClientAuthenticationJWTIdentifierClaimsValidator"
+ class="net.shibboleth.oidc.security.jwt.claims.impl.JWTIdentifierClaimsValidator"
+ p:clockSkew="%{idp.policy.clockSkew:PT1M}"
+ p:replayCache-ref="shibboleth.ReplayCache" />
+
+
+ <bean id="RelyingPartyByTrustAnchor" abstract="true" parent="RelyingParty"
+ class="net.shibboleth.oidfed.profile.context.RelyingPartyConfigurationSupport" factory-method="byTrustAnchor" />
+
<import resource="${idp.home}/conf/oidfed/oidfed-entity-configuration-claims.xml"/>
</beans>
diff --git a/oidfed-common-conf-impl/src/test/java/net/shibboleth/oidfed/flow/AbstractFederationFlowTest.java b/oidfed-common-conf-impl/src/test/java/net/shibboleth/oidfed/flow/AbstractFederationFlowTest.java
index 38b2ed1..3cf3410 100644
--- a/oidfed-common-conf-impl/src/test/java/net/shibboleth/oidfed/flow/AbstractFederationFlowTest.java
+++ b/oidfed-common-conf-impl/src/test/java/net/shibboleth/oidfed/flow/AbstractFederationFlowTest.java
@@ -723,7 +723,6 @@ public class AbstractFederationFlowTest extends AbstractFlowTest {
protected ErrorResponse parseErrorResponse(final FlowExecutionResult result, final String message) {
final Response response = parseResponse(result);
- Assert.assertFalse(response.indicatesSuccess(), message);
Assert.assertTrue(response instanceof ErrorResponse, message);
return (ErrorResponse) response;
}
diff --git a/oidfed-common-conf-impl/src/test/java/net/shibboleth/oidfed/flow/ResolveEntityFlowTest.java b/oidfed-common-conf-impl/src/test/java/net/shibboleth/oidfed/flow/ResolveEntityFlowTest.java
new file mode 100644
index 0000000..ef70d12
--- /dev/null
+++ b/oidfed-common-conf-impl/src/test/java/net/shibboleth/oidfed/flow/ResolveEntityFlowTest.java
@@ -0,0 +1,455 @@
+/*
+ * 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.oidfed.flow;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.security.interfaces.ECPrivateKey;
+import java.security.interfaces.RSAPrivateKey;
+import java.time.Instant;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.commons.codec.binary.Base64;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.storage.StorageService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.webflow.executor.FlowExecutionResult;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JOSEObjectType;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.JWSHeader;
+import com.nimbusds.jose.crypto.ECDSASigner;
+import com.nimbusds.jose.crypto.RSASSASigner;
+import com.nimbusds.jose.jwk.JWKSet;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.SignedJWT;
+import com.nimbusds.oauth2.sdk.ErrorResponse;
+import com.nimbusds.oauth2.sdk.Response;
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientMetadata;
+
+import net.shibboleth.oidfed.messaging.impl.ResolveEntityResponse;
+import net.shibboleth.oidfed.metadata.EntityStatement;
+import net.shibboleth.oidfed.metadata.impl.SubordinateStatementImpl;
+import net.shibboleth.oidfed.testing.FederationJwtSupport;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.minidev.json.JSONObject;
+
+/**
+ * Flow tests for the OpenID federation resolve entity flow.
+ */
+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);
+ }
+
+ @Test
+ public void testInvalidMethod() throws Exception {
+ setJsonRequest("POST", "{}");
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ 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");
+ }
+
+ @Test
+ public void testUntrustedAnchor() throws Exception {
+ 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");
+ }
+
+ @Test
+ public void testRPWithTrustedTrustAnchor() throws Exception {
+ 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 =
+ 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_validTrustMark() throws Exception {
+ request.setMethod("GET");
+ final String clientId = uniqueClientId();
+ final String trustMark = FederationJwtSupport.trustMark(JWSAlgorithm.RS256, trustMarkIssuerKey, trustMarkIssuerId,
+ clientId, "https://example.org/email-allowing-trust-mark", Instant.now().plusSeconds(300)).serialize();
+ final OIDCClientMetadata metadata = new OIDCClientMetadata();
+ metadata.setRedirectionURI(new URI(redirectUri));
+ metadata.setJWKSet(new JWKSet(rpKey.toPublicJWK()));
+ final String rpEntityConfiguration = rpEntityConfiguration(clientId, metadata, List.of(Map.of(
+ "trust_mark_type", "https://example.org/email-allowing-trust-mark",
+ "trust_mark", trustMark)), leafKey);
+ rpConfigureMockHttpClient(clientId, rpEntityConfiguration);
+ try {
+ mapResponse(entityConfigurationUrl(trustMarkIssuerId),
+ mockResponse(trustMarkIssuerConfiguration(trustMarkIssuerId)));
+ mapResponse(subordinateStatementUrl(anchorFetchEndpoint, trustMarkIssuerId),
+ mockResponse(subordinateStatement(trustMarkIssuerId,
+ Map.of("federation_entity", CollectionSupport.emptyMap()), trustMarkIssuerKey)));
+ mapResponse(trustMarkStatusEndpoint, mockResponse(200, "application/trust-mark-status-response+jwt",
+ trustMarkStatusResponse(trustMarkIssuerId, trustMark, "active", trustMarkIssuerKey)));
+ } catch (UnsupportedOperationException | IOException e) {
+ Assert.fail("Could not initialize mock HTTP client", e);
+ }
+ 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 =
+ 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"));
+ final Map<String,Object> trustMarks = response.getJWTClaimsSet().getJSONObjectClaim("trust_marks");
+ Assert.assertNotNull(trustMarks, "Could not find trust marks for client " + clientId);
+ Assert.assertEquals(trustMarks.size(), 1);
+ Assert.assertEquals(trustMarks.get("https://example.org/email-allowing-trust-mark"), trustMark);
+ }
+
+ @Test
+ public void testRPWithTrustedTrustAnchor_subordinateKeyNotMatchingEntityConfiguration() throws Exception {
+ 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");
+ assertErrorDescriptionContains(result, "NoTrustChainsResolved", "");
+ }
+
+ @Test
+ public void testRPWithTrustedTrustAnchorInvalidMetadata() throws Exception {
+ 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");
+ }
+
+ @Test
+ public void testOPWithTrustedTrustAnchor() throws Exception {
+ 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 =
+ parseSuccessResponse(result, ResolveEntityResponse.class);
+ final SignedJWT response = parsedResponse.getJWT();
+ Assert.assertEquals(response.getHeader().getType(), new JOSEObjectType("resolve-response+jwt"));
+ Assert.assertEquals(response.getJWTClaimsSet().getSubject(), entityId);
+ Assert.assertNotNull(response.getJWTClaimsSet().getClaim("metadata"));
+ Assert.assertNull(response.getJWTClaimsSet().getClaim("authority_hints"));
+ }
+
+ @Test
+ public void testOPWithTrustedTrustAnchorInvalidMetadata() throws Exception {
+ 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");
+ }
+
+ @Test
+ public void testOPWithTrustedTrustAnchor_emptyMetadataPolicyCrit() throws Exception {
+ request.setMethod("GET");
+ final String entityId = uniqueClientId();
+ final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder().issuer(anchorId).subject(entityId)
+ .issueTime(Date.from(Instant.now()))
+ .expirationTime(Date.from(Instant.now().plusSeconds(300)))
+ .claim("jwks", new JWKSet(leafKey).toJSONObject(true))
+ .claim("metadata", Map.of("openid_provider", emptyOpMetadata(entityId).toJSONObject()))
+ .build();
+ final ObjectMapper objectMapper = payloadObjectMapper;
+ assert objectMapper != null;
+ final SignedJWT jwt = FederationJwtSupport.entityStatement(JWSAlgorithm.RS256, trustedAnchorKey, claimsSet);
+ assert jwt != null;
+ final EntityStatement<?> subordinateStatement = SubordinateStatementImpl.parse(jwt, objectMapper);
+ try {
+ mapResponse(entityConfigurationUrl(entityId), mockResponse(opEntityConfiguration(entityId)));
+ mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+ mapResponse(subordinateStatementUrl(anchorFetchEndpoint, entityId),
+ mockResponse(subordinateStatement.getJwt().serialize()));
+ } 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 =
+ parseSuccessResponse(result, ResolveEntityResponse.class);
+ final SignedJWT response = parsedResponse.getJWT();
+ Assert.assertEquals(response.getHeader().getType(), new JOSEObjectType("resolve-response+jwt"));
+ Assert.assertEquals(response.getJWTClaimsSet().getSubject(), entityId);
+ Assert.assertNotNull(response.getJWTClaimsSet().getClaim("metadata"));
+ 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";
+ 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";
+ 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";
+ 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 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, 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(requestingClientId);
+ 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 Response parseResponse(final FlowExecutionResult result) {
+ assertFlowExecutionOutcome(result.getOutcome(), END_STATE_ID);
+ final ProfileRequestContext prc = retrieveProfileRequestContext(result);
+ Assert.assertNotNull(prc);
+ assert prc != null;
+ Assert.assertNotNull(prc.ensureOutboundMessageContext());
+ final Object responseMessage = prc.ensureOutboundMessageContext().getMessage();
+ Assert.assertNotNull(responseMessage);
+ Assert.assertTrue(responseMessage instanceof Response);
+ return (Response) responseMessage;
+ }
+
+ protected <AResponseType extends Response> AResponseType parseSuccessResponse(final FlowExecutionResult result,
+ final Class<AResponseType> clazz) {
+ final Response response = parseResponse(result);
+ if (response.indicatesSuccess()) {
+ Assert.assertTrue(clazz.isInstance(response));
+ return clazz.cast(response);
+ }
+ return null;
+ }
+
+ protected void assertErrorCode(final FlowExecutionResult result, final String errorCode) {
+ assertErrorCode(result, errorCode, null);
+ }
+
+ protected void assertErrorCode(final FlowExecutionResult result, final String errorCode,
+ final String message) {
+ final Response response = parseResponse(result);
+ Assert.assertTrue(response instanceof ErrorResponse);
+ final ErrorResponse errorResponse = (ErrorResponse) response;
+ Assert.assertEquals(errorResponse.getErrorObject().getCode(), errorCode, message);
+ }
+
+ protected void setJsonRequest(final String method, final String body) {
+ setRequest(method, body, "application/json");
+ }
+
+ protected void setHttpFormRequest(final String method, final Map<String, String> parameters) {
+ setHttpFormRequest(request, method, parameters);
+ }
+
+ protected static void setHttpFormRequest(final MockHttpServletRequest request, final String method, final Map<String, String> parameters) {
+ setRequest(request, method, "", "application/x-www-form-urlencoded");
+ request.setParameters(parameters);
+ }
+
+ protected void setBasicAuth(final String username, final String password) {
+ request.removeHeader("Authorization");
+ request.addHeader("Authorization",
+ "Basic " + new String(Base64.encodeBase64(new String(username + ":" + password).getBytes())));
+ }
+
+ protected void setRequest(final String method, final String body, final String contentType) {
+ setRequest(request, method, body, contentType);
+ }
+
+ protected static void setRequest(final MockHttpServletRequest request, final String method, final String body, final String contentType) {
+ request.setMethod(method);
+ request.setContentType(contentType);
+ request.setContent(body.getBytes());
+ }
+
+ protected static SignedJWT createPrivateKeyJWT(final JWTClaimsSet claimsSet, final RSAPrivateKey rsaPrivateKey)
+ throws JOSEException {
+ return createPrivateKeyJWT(claimsSet, rsaPrivateKey, JWSAlgorithm.RS256);
+ }
+
+ protected static SignedJWT createPrivateKeyJWT(final JWTClaimsSet claimsSet, final RSAPrivateKey rsaPrivateKey,
+ final JWSAlgorithm algorithm) throws JOSEException {
+ final SignedJWT jwt = new SignedJWT(new JWSHeader(algorithm), claimsSet);
+ final RSASSASigner signer = new RSASSASigner(rsaPrivateKey);
+ jwt.sign(signer);
+ return jwt;
+ }
+
+ protected static SignedJWT createPrivateKeyJWT(final JWTClaimsSet claimsSet, final ECPrivateKey ecPrivateKey,
+ final JWSAlgorithm algorithm) throws JOSEException {
+ final SignedJWT jwt = new SignedJWT(new JWSHeader(algorithm), claimsSet);
+ final ECDSASigner signer = new ECDSASigner(ecPrivateKey);
+ jwt.sign(signer);
+ return jwt;
+ }
+
+}
diff --git a/oidfed-common-conf-impl/src/test/resources/credentials/htpasswd.txt b/oidfed-common-conf-impl/src/test/resources/credentials/htpasswd.txt
new file mode 100644
index 0000000..882b85e
--- /dev/null
+++ b/oidfed-common-conf-impl/src/test/resources/credentials/htpasswd.txt
@@ -0,0 +1,2 @@
+localDefaultClient:$apr1$2dj74pi3$XKh7CjYBTUxzeMbhRSxlr1
+localResolveEntityClient:$apr1$503cv9z7$4ogERgiLlrAALMa4xDaKg/
diff --git a/oidfed-common-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/relying-party.xml b/oidfed-common-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/authn/oauth2client-authn-config.xml
similarity index 57%
copy from oidfed-common-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/relying-party.xml
copy to oidfed-common-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/authn/oauth2client-authn-config.xml
index 9ab8124..032594b 100644
--- a/oidfed-common-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/relying-party.xml
+++ b/oidfed-common-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/authn/oauth2client-authn-config.xml
@@ -11,23 +11,20 @@
default-init-method="initialize"
default-destroy-method="destroy">
-
- <bean id="shibboleth.UnverifiedRelyingParty" parent="RelyingParty">
- <property name="profileConfigurations">
- <list>
- <bean parent="OIDFED.Configuration" p:cachedSuccessResponseLifetime="PT2S" />
- </list>
- </property>
- </bean>
-
- <bean id="shibboleth.DefaultRelyingParty" parent="RelyingParty.MDDriven">
- <property name="profileConfigurations">
- <list>
- </list>
- </property>
- </bean>
-
- <util:list id="shibboleth.RelyingPartyOverrides">
+
+ <!-- Ordered list of CredentialValidators to apply to a request. -->
+ <util:list id="shibboleth.authn.OAuth2Client.Validators">
+ <ref bean="shibboleth.OIDCClientInfoValidator" />
+ <ref bean="shibboleth.JWTValidator" />
+ <bean parent="shibboleth.HTPasswdValidator">
+ <property name="resource">
+ <bean class="net.shibboleth.shared.spring.resource.ConditionalResource">
+ <constructor-arg>
+ <bean class="org.springframework.core.io.ClassPathResource" c:path="/credentials/htpasswd.txt" />
+ </constructor-arg>
+ </bean>
+ </property>
+ </bean>
</util:list>
-
+
</beans>
diff --git a/oidfed-common-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/relying-party.xml b/oidfed-common-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/relying-party.xml
index 9ab8124..4ca29ed 100644
--- a/oidfed-common-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/relying-party.xml
+++ b/oidfed-common-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/relying-party.xml
@@ -16,6 +16,7 @@
<property name="profileConfigurations">
<list>
<bean parent="OIDFED.Configuration" p:cachedSuccessResponseLifetime="PT2S" />
+ <bean parent="OIDFED.ResolveEntity" />
</list>
</property>
</bean>
@@ -28,6 +29,21 @@
</bean>
<util:list id="shibboleth.RelyingPartyOverrides">
+ <bean parent="RelyingPartyByTrustAnchor" c:trustAnchorIds="https://trust-anchor.federation.local">
+ <property name="profileConfigurations">
+ <list>
+ <!-- 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>
</util:list>
</beans>
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/decoding/impl/ResolveEntityRequestDecoder.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/decoding/impl/ResolveEntityRequestDecoder.java
new file mode 100644
index 0000000..ea07356
--- /dev/null
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/decoding/impl/ResolveEntityRequestDecoder.java
@@ -0,0 +1,110 @@
+/*
+ * 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.oidfed.decoding.impl;
+
+import java.io.IOException;
+import java.net.URI;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.messaging.decoder.MessageDecodingException;
+import org.opensaml.messaging.decoder.servlet.AbstractHttpServletRequestMessageDecoder;
+import org.slf4j.Logger;
+
+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;
+
+import jakarta.servlet.http.HttpServletRequest;
+import net.shibboleth.oidfed.messaging.impl.ResolveEntityRequest;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * Message decoder decoding OpenID Federation Resolve Entity request {@link ResolveEntityRequest}.
+ */
+public class ResolveEntityRequestDecoder extends AbstractHttpServletRequestMessageDecoder {
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(ResolveEntityRequestDecoder.class);
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doDecode() throws MessageDecodingException {
+ final HttpServletRequest request = getHttpServletRequest();
+ assert request != null;
+ 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);
+ //TODO: protocol logging once base-class moved to oidc-common
+ //getProtocolMessageLogger().trace("Inbound request {}", RequestUtil.toString(httpRequest, null));
+ final URI uri = httpRequest.getURI();
+ if (uri == null) {
+ throw new MessageDecodingException("Could not parse request URI");
+ }
+ final 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;
+ if ("GET".equalsIgnoreCase(request.getMethod())) {
+ parameters = httpRequest.getQueryStringParameters();
+ } else {
+ parameters = httpRequest.getBodyAsFormParameters();
+ }
+ final String subject = Optional.ofNullable(parameters.get("sub"))
+ .filter(Objects::nonNull)
+ .filter(list -> list.size() == 1)
+ .map(list -> list.get(0))
+ .orElse(null);
+ if (subject == null) {
+ throw new MessageDecodingException("No single sub value in the request");
+ }
+ final List<String> trustAnchors = Optional.ofNullable(parameters.get("trust_anchor"))
+ .filter(Objects::nonNull)
+ .filter(list -> list.size() > 0)
+ .orElse(null);
+ if (trustAnchors == null) {
+ throw new MessageDecodingException("No trust_anchor included in the request");
+ }
+ final ResolveEntityRequest requestMessage =
+ new ResolveEntityRequest(uri, subject, trustAnchors, parameters.get("entity_type"),
+ clientAuthentication);
+ log.debug("Successfully built request message object {}", requestMessage);
+ final MessageContext messageContext = new MessageContext();
+ messageContext.setMessage(requestMessage);
+ setMessageContext(messageContext);
+ } catch (final IOException | ParseException e) {
+ log.error("Could not create HTTP request from the request", e);
+ throw new MessageDecodingException(e);
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/BuildResolveEntityErrorResponseFromEvent.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/BuildResolveEntityErrorResponseFromEvent.java
new file mode 100644
index 0000000..20f6113
--- /dev/null
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/BuildResolveEntityErrorResponseFromEvent.java
@@ -0,0 +1,269 @@
+/*
+ * 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.oidfed.profile.impl;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.context.EventContext;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.CurrentOrPreviousEventLookup;
+import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
+import org.slf4j.Logger;
+
+import com.nimbusds.oauth2.sdk.ErrorObject;
+import com.nimbusds.oauth2.sdk.ErrorResponse;
+import com.nimbusds.oauth2.sdk.http.HTTPResponse;
+
+import net.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.oidc.metadata.cache.MetadataCache;
+import net.shibboleth.oidc.metadata.cache.MetadataCacheException;
+import net.shibboleth.oidc.profile.messaging.JSONErrorResponse;
+import net.shibboleth.oidfed.messaging.impl.ResolveEntityRequest;
+import net.shibboleth.oidfed.metadata.cache.ResponseContainerExpirationCriterion;
+import net.shibboleth.oidfed.metadata.cache.local.NimbusResponseCriterion;
+import net.shibboleth.oidfed.metadata.cache.local.ResolveEntityRequestCriterion;
+import net.shibboleth.oidfed.metadata.cache.local.ResolveEntityResponseContainer;
+import net.shibboleth.oidfed.profile.config.navigate.CachedErrorResponseLifetimeLookupFunction;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.resolver.CriteriaSet;
+
+/**
+ * This action reads an event from the configured {@link EventContext} lookup strategy, constructs a JSON error response
+ * message and attaches it as the outbound message. If {@link RelyingPartyCachedMessageContext} is found, it's exploited
+ * for storing the response message in the configured {@link #responseCache}.
+ */
+public class BuildResolveEntityErrorResponseFromEvent extends AbstractProfileAction {
+
+ /** Default value for the error code in the error response messages. */
+ public static final String DEFAULT_ERROR_CODE = "invalid_request";
+
+ /** Default value for the HTTP response status code in the HTTP responses. */
+ public static final int DEFAULT_HTTP_STATUS_CODE = HTTPResponse.SC_BAD_REQUEST;
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(BuildResolveEntityErrorResponseFromEvent.class);
+
+ /** Strategy function for access to {@link EventContext} to check. */
+ @Nonnull
+ private Function<ProfileRequestContext, EventContext> eventContextLookupStrategy;
+
+ /** Map of eventIds to pre-configured error objects. */
+ private Map<String, ErrorObject> mappedErrors;
+
+ /** The status code for unmapped events. */
+ private int defaultStatusCode;
+
+ /** The code for unmapped events. */
+ private String defaultCode;
+
+ /** Metadata cache for cached response containers. */
+ @NonnullAfterInit private MetadataCache<ResolveEntityResponseContainer> responseCache;
+
+ /** Strategy used to locate the lifetime for the cached response record. */
+ @Nonnull private Function<ProfileRequestContext,Duration> cachedResponseLifetimeLookupStrategy;
+
+ /** Strategy used to locate the resolve entity context. */
+ @Nonnull
+ private Function<ProfileRequestContext, RelyingPartyCachedMessageContext> resolveEntityContextLookupStrategy;
+
+ /**
+ * Constructor.
+ */
+ public BuildResolveEntityErrorResponseFromEvent() {
+ eventContextLookupStrategy = new CurrentOrPreviousEventLookup();
+ mappedErrors = new HashMap<>();
+ defaultStatusCode = DEFAULT_HTTP_STATUS_CODE;
+ defaultCode = DEFAULT_ERROR_CODE;
+ cachedResponseLifetimeLookupStrategy = new CachedErrorResponseLifetimeLookupFunction();
+ final Function<ProfileRequestContext, RelyingPartyCachedMessageContext> recls =
+ new ChildContextLookup<>(RelyingPartyCachedMessageContext.class).compose(
+ new InboundMessageContextLookup());
+ assert recls != null;
+ resolveEntityContextLookupStrategy = recls;
+
+ }
+
+ /**
+ * Set the status code for unmapped events.
+ *
+ * @param code The default status code for unmapped events.
+ */
+ public void setDefaultStatusCode(final int code) {
+ defaultStatusCode = code;
+ }
+
+ /**
+ * Set the code for unmapped events.
+ *
+ * @param code The default status code for unmapped events.
+ */
+ public void setDefaultCode(@Nonnull final String code) {
+ defaultCode = Constraint.isNotNull(code, "Default code cannot be null");
+ }
+
+ /**
+ * Set lookup strategy for {@link EventContext} to check.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setEventContextLookupStrategy(@Nonnull final Function<ProfileRequestContext, EventContext> strategy) {
+ ifInitializedThrowUnmodifiabledComponentException();
+
+ eventContextLookupStrategy = Constraint.isNotNull(strategy, "EventContext lookup strategy cannot be null");
+ }
+
+ /**
+ * Set map of eventIds to pre-configured error objects.
+ *
+ * @param errors map of eventIds to pre-configured error objects.
+ */
+ public void setMappedErrors(@Nonnull final Map<String, ErrorObject> errors) {
+ ifInitializedThrowUnmodifiabledComponentException();
+
+ mappedErrors = Constraint.isNotNull(errors, "Mapped errors cannot be null");
+ }
+
+ /**
+ * Set the metadata cache for cached response containers.
+ *
+ * @param cache What to set.
+ */
+ public void setResponseCache(@Nonnull final MetadataCache<ResolveEntityResponseContainer> cache) {
+ checkSetterPreconditions();
+ responseCache = Constraint.isNotNull(cache, "Response metadata cache cannot be null");
+ }
+
+ /**
+ * Set the strategy used to locate the lifetime for the cached response record.
+ *
+ * @param strategy What to set.
+ */
+ public void setCachedResponseLifetimeLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext, Duration> strategy) {
+ checkSetterPreconditions();
+ cachedResponseLifetimeLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null");
+ }
+
+ /**
+ * Set the strategy used to locate the resolve entity context
+ *
+ * @param strategy What to set.
+ */
+ public void setResolveEntityContextLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext,RelyingPartyCachedMessageContext> strategy) {
+ checkSetterPreconditions();
+ resolveEntityContextLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null!");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+
+ if (responseCache == null) {
+ throw new ComponentInitializationException("Response metadata cache cannot be null");
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+ if (!super.doPreExecute(profileRequestContext)) {
+ return false;
+ }
+
+ if (profileRequestContext.getOutboundMessageContext() == null) {
+ log.debug("{} No outbound message context initialized, nothing to do", getLogPrefix());
+ return false;
+ }
+
+ return true;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+ final EventContext eventCtx = eventContextLookupStrategy.apply(profileRequestContext);
+ if (eventCtx == null || eventCtx.getEvent() == null) {
+ log.error("{} No event to be included in the response, nothing to do", getLogPrefix());
+ return;
+ }
+ assert eventCtx != null;
+ final Object event = eventCtx.getEvent();
+ assert event != null;
+ final String eventValue = event.toString();
+ final ErrorObject error;
+ if (mappedErrors.containsKey(eventValue)) {
+ log.debug("{} Found mapped event for {}", getLogPrefix(), eventValue);
+ error = mappedErrors.get(eventValue);
+ } else {
+ log.debug("{} No mapped event found for {}, creating general {}", getLogPrefix(), eventValue, defaultCode);
+ error = new ErrorObject(defaultCode, eventValue, defaultStatusCode);
+ }
+ assert error != null;
+ final ErrorResponse errorResponse = buildErrorResponse(error, profileRequestContext);
+ if (errorResponse != null) {
+ profileRequestContext.ensureOutboundMessageContext().setMessage(errorResponse);
+ log.debug("{} ErrorResponse successfully set as the outbound message", getLogPrefix());
+ } else {
+ log.debug("{} Error response not formed", getLogPrefix());
+ }
+ }
+
+ protected JSONErrorResponse buildErrorResponse(@Nonnull final ErrorObject error,
+ @Nonnull final ProfileRequestContext profileRequestContext) {
+ final JSONErrorResponse response = new JSONErrorResponse(error);
+ final RelyingPartyCachedMessageContext resolveEntityContext =
+ resolveEntityContextLookupStrategy.apply(profileRequestContext);
+
+ final Duration cachedResponseLifetime = cachedResponseLifetimeLookupStrategy.apply(profileRequestContext);
+ if (resolveEntityContext != null && cachedResponseLifetime != null &&
+ resolveEntityContext.getValidatedRequest() instanceof ResolveEntityRequest resolveEntityRequest) {
+ final NimbusResponseCriterion responseCriterion = new NimbusResponseCriterion(response);
+ final Instant expiration = Instant.now().plus(cachedResponseLifetime);
+ assert expiration != null;
+ final ResponseContainerExpirationCriterion expirationCriterion =
+ new ResponseContainerExpirationCriterion(expiration);
+ final ResolveEntityRequestCriterion requestCriterion =
+ new ResolveEntityRequestCriterion(resolveEntityRequest);
+ final CriteriaSet criteria = new CriteriaSet(requestCriterion, responseCriterion, expirationCriterion);
+ try {
+ final List<ResolveEntityResponseContainer> result = responseCache.get(criteria);
+ if (result.size() != 1) {
+ log.error("{} Unexpected result (size={}) when storing response record into the metadata cache",
+ getLogPrefix(), result.size());
+ } else {
+ log.debug("{} Response stored into the cache", getLogPrefix());
+ }
+ } catch (final MetadataCacheException e) {
+ log.error("{} Could not store the response record into the metadata cache", getLogPrefix(), e);
+ }
+ }
+
+ return response;
+ }
+}
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/BuildResolveEntityResponse.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/BuildResolveEntityResponse.java
new file mode 100644
index 0000000..b5c45e6
--- /dev/null
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/BuildResolveEntityResponse.java
@@ -0,0 +1,204 @@
+/*
+ * 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.oidfed.profile.impl;
+
+import java.time.Instant;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
+import org.slf4j.Logger;
+
+import com.nimbusds.jwt.JWTClaimsSet;
+
+import net.shibboleth.oidfed.metadata.EntityStatement;
+import net.shibboleth.oidfed.metadata.TrustMark;
+import net.shibboleth.oidfed.metadata.payload.claim.Metadata;
+import net.shibboleth.oidfed.profile.OidFederationEventIds;
+import net.shibboleth.oidfed.profile.context.RelyingPartyTrustChainContext;
+import net.shibboleth.oidfed.profile.context.VerifiedTrustChain;
+import net.shibboleth.oidfed.profile.navigate.DefaultEntityTypesLookupFunction;
+import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.collection.Pair;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * An action that uses the information from {@link RelyingPartyTrustChainContext} for creating a new JWT to be used for
+ * creating a response to OpenID Federation Resolve Entity API.
+ */
+public class BuildResolveEntityResponse extends AbstractBuildEntityStatementAction {
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(BuildResolveEntityResponse.class);
+
+ /** Strategy used to lookup the trust chain context. */
+ @Nonnull private Function<ProfileRequestContext, RelyingPartyTrustChainContext> trustChainContextLookupStrategy;
+
+ /** Strategy used to lookup the entity types included in the response metadata. */
+ @Nonnull private Function<ProfileRequestContext, List<String>> entityTypesLookupStrategy;
+
+ /** Trust chain context to operate on. */
+ @NonnullBeforeExec private RelyingPartyTrustChainContext trustChainContext;
+
+ /** Constructor. */
+ public BuildResolveEntityResponse() {
+ final Function<ProfileRequestContext, RelyingPartyTrustChainContext> tcls =
+ new ChildContextLookup<>(RelyingPartyTrustChainContext.class).compose(
+ new InboundMessageContextLookup());
+ assert tcls != null;
+ trustChainContextLookupStrategy = tcls;
+ entityTypesLookupStrategy = new DefaultEntityTypesLookupFunction();
+ }
+
+ /**
+ * Set the strategy used to lookup the trust chain context.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setTrustChainContextLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext, RelyingPartyTrustChainContext> strategy) {
+ checkSetterPreconditions();
+ trustChainContextLookupStrategy =
+ Constraint.isNotNull(strategy, "TrustChainContextLookupStrategy cannot be null");
+ }
+
+ /**
+ * Set the strategy used to lookup the entity types included in the response metadata.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setEntityTypesLookupStrategy(@Nonnull final Function<ProfileRequestContext, List<String>> strategy) {
+ checkSetterPreconditions();
+ entityTypesLookupStrategy = Constraint.isNotNull(strategy, "EntityTypesLookupStrategy cannot be null");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+ if (!super.doPreExecute(profileRequestContext)) {
+ return false;
+ }
+
+ trustChainContext = trustChainContextLookupStrategy.apply(profileRequestContext);
+ if (trustChainContext == null) {
+ log.error("{} Unable to locate trust chain context", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
+ return false;
+ }
+
+ return true;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected boolean populateClaimsSetBuilder(@Nonnull final JWTClaimsSet.Builder builder,
+ @Nonnull final ProfileRequestContext profileRequestContext) {
+ final VerifiedTrustChain selectedTrustChain = trustChainContext.getSelectedTrustChain();
+ if (selectedTrustChain == null) {
+ log.debug("{} No selected trust chain found form the context", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, OidFederationEventIds.INVALID_METADATA);
+ return false;
+ }
+ final Metadata metadata = selectedTrustChain.getMetadata();
+ final List<String> entityTypes = entityTypesLookupStrategy.apply(profileRequestContext);
+ log.trace("{} The following entity types were requested: {}", getLogPrefix(), entityTypes);
+ if (entityTypes != null && !entityTypes.isEmpty()) {
+ final Map<String,Object> filteredMetadata = metadata.getAllClaims().entrySet()
+ .stream()
+ .filter(entry -> entityTypes.contains(entry.getKey()))
+ .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
+ if (filteredMetadata.isEmpty()) {
+ log.warn("{} No metadata for entity types {} found for the selected trust chain", getLogPrefix(),
+ entityTypes);
+ ActionSupport.buildEvent(profileRequestContext, OidFederationEventIds.INVALID_METADATA);
+ return false;
+ }
+ builder.claim("metadata", filteredMetadata);
+ } else {
+ builder.claim("metadata", metadata);
+ }
+
+ final List<EntityStatement<?>> trustChain = selectedTrustChain.getTrustChain();
+ builder.claim("trust_chain", trustChain.stream()
+ .map(statement -> statement.getJwt().serialize())
+ .toList());
+
+ final Instant expirationTime = resolveTrustChainExpiration(trustChain);
+ if (expirationTime == null) {
+ log.error("{} Coud not resolve expiration time from the selected trust chain context", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
+ return false;
+ }
+ builder.expirationTime(Date.from(expirationTime));
+
+ final String entityId = trustChain.get(0).getSubject();
+ assert entityId != null;
+ final Map<String, String> trustMarks = buildTrustMarks(entityId, trustChainContext.getVerifiedTrustMarks());
+ if (trustMarks != null && !trustMarks.isEmpty()) {
+ builder.claim("trust_marks", trustMarks);
+ }
+ return true;
+ }
+
+ /**
+ * Builds value for the trust_marks claim in the resolve entity response entity statement.
+ *
+ * @param entityId the subject entity ID
+ * @param trustMarks trust marks for the selected trust chain
+ * @return map of trust marks, keyed with trust mark IDs
+ */
+ @Nullable private Map<String, String> buildTrustMarks(@Nonnull final String entityId,
+ @Nullable final Map<String, List<TrustMark>> trustMarks) {
+ return Optional.ofNullable(trustMarks)
+ .map(marks -> marks.get(entityId))
+ .filter(Objects::nonNull)
+ .map(list -> list.stream()
+ .map(trustMark -> new Pair<String, String>(
+ trustMark.getParsedPayload().getTrustMarkType(), trustMark.getJwt().serialize()))
+ .filter(Objects::nonNull)
+ .collect(Collectors.toMap(pair -> pair.getFirst(), pair -> pair.getSecond())))
+ .orElse(null);
+ }
+
+ /**
+ * Resolve expiration time for the given trust chain.
+ *
+ * @param trustChain trust chain
+ * @return expiration time
+ */
+ @Nullable private Instant resolveTrustChainExpiration(@Nonnull final List<EntityStatement<?>> trustChain) {
+ Instant metadataExpiration = null;
+ for (final EntityStatement<?> statement : trustChain) {
+ final Instant statementExpiration = statement.getParsedPayload().getExpiration();
+ metadataExpiration = metadataExpiration == null ? statementExpiration :
+ statementExpiration.isBefore(metadataExpiration) ? statementExpiration : metadataExpiration;
+ }
+ return metadataExpiration;
+ }
+
+}
\ No newline at end of file
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/FormOutboundResolveEntityResponse.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/FormOutboundResolveEntityResponse.java
new file mode 100644
index 0000000..5309dfd
--- /dev/null
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/FormOutboundResolveEntityResponse.java
@@ -0,0 +1,228 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.oidfed.profile.impl;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.util.List;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
+import org.opensaml.profile.context.navigate.OutboundMessageContextLookup;
+import org.slf4j.Logger;
+
+import com.nimbusds.jwt.SignedJWT;
+import com.nimbusds.oauth2.sdk.Response;
+
+import net.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.oidc.metadata.cache.MetadataCache;
+import net.shibboleth.oidc.metadata.cache.MetadataCacheException;
+import net.shibboleth.oidfed.messaging.impl.ResolveEntityRequest;
+import net.shibboleth.oidfed.messaging.impl.ResolveEntityResponse;
+import net.shibboleth.oidfed.metadata.cache.ResponseContainerExpirationCriterion;
+import net.shibboleth.oidfed.metadata.cache.local.NimbusResponseCriterion;
+import net.shibboleth.oidfed.metadata.cache.local.ResolveEntityRequestCriterion;
+import net.shibboleth.oidfed.metadata.cache.resolver.ResolveEntityResponseContainer;
+import net.shibboleth.oidfed.profile.config.navigate.CachedSuccessResponseLifetimeLookupFunction;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.resolver.CriteriaSet;
+
+/**
+ * This action builds a response for the OpenID federation resolve entity request. The response contains an
+ * {@link SignedJWT}. The response is put in the {@link #responseCache} using the lifetime resolved via
+ * {@link #cachedResponseLifetimeLookupStrategy}.
+ */
+public class FormOutboundResolveEntityResponse extends AbstractProfileAction {
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(FormOutboundResolveEntityResponse.class);
+
+ /** Metadata cache for cached response containers. */
+ @NonnullAfterInit private MetadataCache<ResolveEntityResponseContainer> responseCache;
+
+ /** Strategy used to locate the resolve entity context. */
+ @Nonnull
+ private Function<ProfileRequestContext, RelyingPartyCachedMessageContext> cachedMessageContextLookupStrategy;
+
+ /** Strategy used to locate the subcontext to hold the statement. */
+ @Nonnull private Function<ProfileRequestContext,EntityStatementContext> entityStatementContextLookupStrategy;
+
+ /** Strategy used to locate the lifetime for the cached response record. */
+ @Nonnull private Function<ProfileRequestContext,Duration> cachedResponseLifetimeLookupStrategy;
+
+ /** JWT used to build entity statement. */
+ @NonnullBeforeExec private SignedJWT jwt;
+
+ /** The resolve entity context to operate on. */
+ @NonnullBeforeExec private RelyingPartyCachedMessageContext cachedMessageContext;
+
+ /**
+ * Constructor.
+ */
+ public FormOutboundResolveEntityResponse() {
+ final Function<ProfileRequestContext,EntityStatementContext> escls =
+ new ChildContextLookup<>(EntityStatementContext.class).compose(
+ new OutboundMessageContextLookup());
+ assert escls != null;
+ entityStatementContextLookupStrategy = escls;
+ final Function<ProfileRequestContext, RelyingPartyCachedMessageContext> recls =
+ new ChildContextLookup<>(RelyingPartyCachedMessageContext.class).compose(
+ new InboundMessageContextLookup());
+ assert recls != null;
+ cachedMessageContextLookupStrategy = recls;
+ cachedResponseLifetimeLookupStrategy = new CachedSuccessResponseLifetimeLookupFunction();
+ }
+
+ /**
+ * Set the strategy used to locate the subcontext to hold the statement
+ *
+ * @param strategy What to set.
+ */
+ public void setEntityStatementContextLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext,EntityStatementContext> strategy) {
+ checkSetterPreconditions();
+ entityStatementContextLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null!");
+ }
+
+ /**
+ * Set the strategy used to locate the cached message context
+ *
+ * @param strategy What to set.
+ */
+ public void setCachedMessageContextLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext,RelyingPartyCachedMessageContext> strategy) {
+ checkSetterPreconditions();
+ cachedMessageContextLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null!");
+ }
+
+ /**
+ * Set the metadata cache for cached response containers.
+ *
+ * @param cache What to set.
+ */
+ public void setResponseCache(@Nonnull final MetadataCache<ResolveEntityResponseContainer> cache) {
+ checkSetterPreconditions();
+ responseCache = Constraint.isNotNull(cache, "Response metadata cache cannot be null");
+ }
+
+ /**
+ * Set the strategy used to locate the lifetime for the cached response record.
+ *
+ * @param strategy What to set.
+ */
+ public void setCachedResponseLifetimeLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext, Duration> strategy) {
+ checkSetterPreconditions();
+ cachedResponseLifetimeLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+
+ if (responseCache == null) {
+ throw new ComponentInitializationException("Response metadata cache cannot be null");
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+ if (!super.doPreExecute(profileRequestContext)) {
+ return false;
+ }
+ cachedMessageContext = cachedMessageContextLookupStrategy.apply(profileRequestContext);
+ if (cachedMessageContext == null) {
+ log.error("{} Could not resolve cached message context", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return false;
+ }
+
+ return true;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+ final Response cachedResponse = cachedMessageContext.getCachedResponse();
+ if (cachedResponse != null) {
+ log.debug("{} Cached response found, storing in to the outbound message context", getLogPrefix());
+ profileRequestContext.ensureOutboundMessageContext().setMessage(cachedResponse);
+ return;
+ }
+ log.debug("{} No cached response found, resolving the response JWT from the context", getLogPrefix());
+ final EntityStatementContext entityStatementContext =
+ entityStatementContextLookupStrategy.apply(profileRequestContext);
+ if (entityStatementContext == null) {
+ log.error("{} Could not resolve entity statement context", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return;
+ }
+ if (entityStatementContext.getJWT() instanceof SignedJWT signedJwt) {
+ jwt = signedJwt;
+ } else {
+ log.error("{} No signed JWT found from the entity statement context", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return;
+ }
+
+ assert jwt != null;
+ final ResolveEntityResponse response = new ResolveEntityResponse(jwt);
+ final NimbusResponseCriterion responseCriterion = new NimbusResponseCriterion(response);
+ final Duration lifetime = cachedResponseLifetimeLookupStrategy.apply(profileRequestContext);
+ if (lifetime == null) {
+ log.error("{} Could not resolve lifetime for the cached response record", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return;
+ }
+ final Instant expiration = Instant.now().plus(lifetime);
+ assert expiration != null;
+ final ResponseContainerExpirationCriterion expirationCriterion =
+ new ResponseContainerExpirationCriterion(expiration);
+ if (cachedMessageContext.getValidatedRequest() instanceof ResolveEntityRequest validatedRequest) {
+ final ResolveEntityRequestCriterion requestCriterion = new ResolveEntityRequestCriterion(validatedRequest);
+ final CriteriaSet criteria = new CriteriaSet(requestCriterion, responseCriterion, expirationCriterion);
+ try {
+ final List<ResolveEntityResponseContainer> result = responseCache.get(criteria);
+ if (result.size() != 1) {
+ log.error("{} Unexpected result (size={}) when storing response record into the metadata cache",
+ getLogPrefix(), result.size());
+ } else {
+ log.debug("{} Response stored into the cache", getLogPrefix());
+ }
+ } catch (final MetadataCacheException e) {
+ log.error("{} Could not store the response record into tht metadata cache", getLogPrefix(), e);
+ }
+ } else {
+ log.error("{} No validated request found from the resolve entity context", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return;
+ }
+
+ profileRequestContext.ensureOutboundMessageContext().setMessage(response);
+ }
+}
\ No newline at end of file
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/LookupCachedResolveEntityResponse.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/LookupCachedResolveEntityResponse.java
new file mode 100644
index 0000000..f52c867
--- /dev/null
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/LookupCachedResolveEntityResponse.java
@@ -0,0 +1,157 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.oidfed.profile.impl;
+
+import java.util.List;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.oidc.metadata.cache.MetadataCache;
+import net.shibboleth.oidc.metadata.cache.MetadataCacheException;
+import net.shibboleth.oidfed.messaging.impl.ResolveEntityRequest;
+import net.shibboleth.oidfed.metadata.cache.local.ResolveEntityRequestCriterion;
+import net.shibboleth.oidfed.metadata.cache.local.ResolveEntityResponseContainer;
+import net.shibboleth.oidfed.profile.OidFederationEventIds;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.resolver.CriteriaSet;
+
+/**
+ * Lookup if a cached response already exists for the validated resolve entity API request. If yes, the response is
+ * stored into {@link RelyingPartyCachedMessageContext} and a corresponding event ID is published.
+ *
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#INVALID_PROFILE_CTX}
+ * @event {@link OidFederationEventIds#CACHED_RESPONSE_FOUND}
+ */
+public class LookupCachedResolveEntityResponse extends AbstractProfileAction {
+
+ /** Class logger. */
+ @Nonnull private Logger log = LoggerFactory.getLogger(LookupCachedResolveEntityResponse.class);
+
+ /** Strategy used to locate the cached message context. */
+ @Nonnull
+ private Function<ProfileRequestContext, RelyingPartyCachedMessageContext> cachedMessageContextLookupStrategy;
+
+ /** Metadata cache for cached response containers. */
+ @NonnullAfterInit private MetadataCache<ResolveEntityResponseContainer> responseCache;
+
+ /** Cached message context to operate on. */
+ @NonnullBeforeExec private RelyingPartyCachedMessageContext cachedMessageContext;
+
+ /** Request message to operate on. */
+ @NonnullBeforeExec private ResolveEntityRequest validatedRequest;
+
+ /**
+ * Constructor.
+ */
+ public LookupCachedResolveEntityResponse() {
+ final Function<ProfileRequestContext, RelyingPartyCachedMessageContext> recls =
+ new ChildContextLookup<>(RelyingPartyCachedMessageContext.class).compose(
+ new InboundMessageContextLookup());
+ assert recls != null;
+ cachedMessageContextLookupStrategy = recls;
+ }
+
+ /**
+ * Set the strategy used to locate the cached message context
+ *
+ * @param strategy What to set.
+ */
+ public void setCachedMessageContextLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext,RelyingPartyCachedMessageContext> strategy) {
+ checkSetterPreconditions();
+ cachedMessageContextLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null!");
+ }
+
+ /**
+ * Set the metadata cache for cached response containers.
+ *
+ * @param cache What to set.
+ */
+ public void setResponseCache(@Nonnull final MetadataCache<ResolveEntityResponseContainer> cache) {
+ checkSetterPreconditions();
+ responseCache = Constraint.isNotNull(cache, "Response metadata cache cannot be null");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+ if (responseCache == null) {
+ throw new ComponentInitializationException("Response metadata cache cannot be null");
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+ if (!super.doPreExecute(profileRequestContext)) {
+ return false;
+ }
+
+ cachedMessageContext = cachedMessageContextLookupStrategy.apply(profileRequestContext);
+ if (cachedMessageContext == null) {
+ log.error("{} Could not resolve cached response context", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return false;
+ }
+
+ if (cachedMessageContext.getValidatedRequest() instanceof ResolveEntityRequest resolveEntityRequest) {
+ validatedRequest = resolveEntityRequest;
+ } else {
+ log.error("{} Could not resolve validated resolve entity request", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return false;
+ }
+
+ return true;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+ assert validatedRequest != null;
+ final ResolveEntityRequestCriterion requestCriterion = new ResolveEntityRequestCriterion(validatedRequest);
+ final CriteriaSet criteria = new CriteriaSet(requestCriterion);
+ try {
+ final List<ResolveEntityResponseContainer> result = responseCache.get(criteria);
+ if (result.size() != 1) {
+ log.debug("{} No cached response record found from the metadata cache", getLogPrefix(), result.size());
+ } else {
+ final ResolveEntityResponseContainer cachedResponse = result.get(0);
+ cachedMessageContext.setCachedResponse(cachedResponse.getResponse());
+ log.debug("{} Response found from the cache, publishing event {}", getLogPrefix(),
+ OidFederationEventIds.CACHED_RESPONSE_FOUND);
+ ActionSupport.buildEvent(profileRequestContext, OidFederationEventIds.CACHED_RESPONSE_FOUND);
+ return;
+ }
+ } catch (final MetadataCacheException e) {
+ log.error("{} Could not fetch response record from the metadata cache", getLogPrefix(), e);
+ }
+ }
+}
\ No newline at end of file
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/ValidateResolveEntityProfileConfiguration.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/ValidateResolveEntityProfileConfiguration.java
new file mode 100644
index 0000000..a2cf306
--- /dev/null
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/ValidateResolveEntityProfileConfiguration.java
@@ -0,0 +1,231 @@
+/*
+ * 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.oidfed.profile.impl;
+
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.idp.profile.IdPEventIds;
+import net.shibboleth.oidfed.metadata.EntityStatement;
+import net.shibboleth.oidfed.profile.config.navigate.MandatoryTrustMarksLookupFunction;
+import net.shibboleth.oidfed.profile.context.RelyingPartyTrustChainContext;
+import net.shibboleth.oidfed.profile.context.VerifiedTrustChain;
+import net.shibboleth.profile.context.RelyingPartyContext;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.primitive.StringSupport;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.action.ActionSupport;
+
+/**
+ * Validates the currently selected trust chain against the current selected profile configuration.
+ *
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#INVALID_MSG_CTX}
+ * @event {@link ValidateResolveEntityProfileConfiguration#RESELECT_TRUST_CHAIN}
+ */
+public class ValidateResolveEntityProfileConfiguration extends AbstractProfileAction {
+
+ /** ID of event returned if a flow wishes to indicate that another trust chain should be selected instead. */
+ @Nonnull @NotEmpty public static final String RESELECT_TRUST_CHAIN = "ReselectTrustChain";
+
+ /** Class logger. */
+ @Nonnull private Logger log = LoggerFactory.getLogger(ValidateResolveEntityProfileConfiguration.class);
+
+ /** Strategy that will return or create a {@link RelyingPartyContext}. */
+ @Nonnull private Function<ProfileRequestContext, RelyingPartyContext> relyingPartyContextCreationStrategy;
+
+ /** Strategy used to lookup the trust chain context. */
+ @Nonnull private Function<ProfileRequestContext, RelyingPartyTrustChainContext> trustChainContextLookupStrategy;
+
+ /** Strategy used to lookup mandatory trust marks. */
+ @Nonnull private Function<ProfileRequestContext, List<String>> mandatoryTrustMarksLookupStrategy;
+
+ /** Strategy used to lookup subject entity ID. */
+ @NonnullAfterInit private Function<ProfileRequestContext, String> subjectEntityIdLookupStrategy;
+
+ /** Trust chain context to operate on. */
+ @NonnullBeforeExec private RelyingPartyTrustChainContext trustChainContext;
+
+ /** Selected trust chain to operate on. */
+ @NonnullBeforeExec private VerifiedTrustChain selectedTrustChain;
+
+ /** Entity ID whose trust chain are operated on. */
+ @NonnullBeforeExec private String entityId;
+
+ /**
+ * Constructor.
+ */
+ public ValidateResolveEntityProfileConfiguration() {
+ relyingPartyContextCreationStrategy = new ChildContextLookup<>(RelyingPartyContext.class, true);
+ final Function<ProfileRequestContext, RelyingPartyTrustChainContext> tcls =
+ new ChildContextLookup<>(RelyingPartyTrustChainContext.class).compose(
+ new InboundMessageContextLookup());
+ assert tcls != null;
+ trustChainContextLookupStrategy = tcls;
+ mandatoryTrustMarksLookupStrategy = new MandatoryTrustMarksLookupFunction();
+ }
+
+ /**
+ * Set the strategy used to return or create the {@link RelyingPartyContext} .
+ *
+ * @param strategy creation strategy
+ */
+ public void setRelyingPartyContextCreationStrategy(
+ @Nonnull final Function<ProfileRequestContext, RelyingPartyContext> strategy) {
+ ifInitializedThrowUnmodifiabledComponentException();
+
+ relyingPartyContextCreationStrategy =
+ Constraint.isNotNull(strategy, "RelyingPartyContext creation strategy cannot be null");
+ }
+
+ /**
+ * Set the strategy used to lookup the trust chain context.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setTrustChainContextLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext, RelyingPartyTrustChainContext> strategy) {
+ checkSetterPreconditions();
+ trustChainContextLookupStrategy =
+ Constraint.isNotNull(strategy, "TrustChainContextLookupStrategy cannot be null");
+ }
+
+ /**
+ * Set the strategy used to lookup mandatory trust marks.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setMandatoryTrustMarksLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext, List<String>> strategy) {
+ checkSetterPreconditions();
+ mandatoryTrustMarksLookupStrategy = Constraint.isNotNull(strategy,
+ "Mandatory trust marks lookup strategy cannot be null");
+ }
+
+ /**
+ * Set the strategy used to lookup subject entity ID.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setSubjectEntityIdLookupStrategy(@Nonnull final Function<ProfileRequestContext, String> strategy) {
+ checkSetterPreconditions();
+ subjectEntityIdLookupStrategy = Constraint.isNotNull(strategy,
+ "Subject entity ID lookup strategy cannot be null");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+ if (!super.doPreExecute(profileRequestContext)) {
+ return false;
+ }
+
+ trustChainContext = trustChainContextLookupStrategy.apply(profileRequestContext);
+ if (trustChainContext == null || trustChainContext.getSelectedTrustChain() == null) {
+ log.error("{} Unable to locate selected trust chain", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
+ return false;
+ }
+
+ selectedTrustChain = trustChainContext.getSelectedTrustChain();
+ if (selectedTrustChain == null) {
+ log.error("{} Selected trust chain contents is not populated", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
+ return false;
+ }
+
+ entityId = StringSupport.trimOrNull(subjectEntityIdLookupStrategy.apply(profileRequestContext));
+ if (entityId == null) {
+ log.error("{} Subject entity ID could not be located", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
+ return false;
+ }
+
+ return true;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+ final Map<String,List<String>> verifiedTrustMarks = trustChainContext.getVerifiedTrustMarkIds();
+ final List<String> mandatoryTrustMarks = mandatoryTrustMarksLookupStrategy.apply(profileRequestContext);
+ if (mandatoryTrustMarks != null && !mandatoryTrustMarks.isEmpty()) {
+ log.debug("{} Verifying the mandatory trust marks {}", getLogPrefix(), mandatoryTrustMarks);
+ if (verifiedTrustMarks == null || verifiedTrustMarks.get(entityId) == null
+ || !verifiedTrustMarks.get(entityId).containsAll(mandatoryTrustMarks)) {
+ log.info("{} Rejecting registration as some of the following mandatory trust marks are missing: {}",
+ getLogPrefix(), mandatoryTrustMarks);
+ final List<List<EntityStatement<?>>> rejectedTrustChains = trustChainContext.getRejectedTrustChains();
+ if (rejectedTrustChains == null) {
+ trustChainContext.setRejectedTrustChains(List.of(selectedTrustChain.getTrustChain()));
+ } else {
+ final List<List<EntityStatement<?>>> rejectedChains = new ArrayList<>(rejectedTrustChains);
+ rejectedChains.add(selectedTrustChain.getTrustChain());
+ trustChainContext.setRejectedTrustChains(CollectionSupport.copyToList(rejectedChains));
+ }
+ ActionSupport.buildEvent(profileRequestContext, RESELECT_TRUST_CHAIN);
+ return;
+ }
+ }
+
+ final List<EntityStatement<?>> trustChain = selectedTrustChain.getTrustChain();
+ assert trustChain != null;
+ trustChainContext.setSelectedMetadataExpiration(resolveTrustChainExpiration(trustChain));
+
+ final RelyingPartyContext rpContext = relyingPartyContextCreationStrategy.apply(profileRequestContext);
+ if (rpContext == null) {
+ log.error("{} Unable to locate or create RelyingPartyContext", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_RELYING_PARTY_CTX);
+ return;
+ }
+ log.debug("Attaching RelyingPartyContext for {} and set it verified", entityId);
+ rpContext.setRelyingPartyId(entityId);
+ rpContext.setVerified(true);
+ }
+
+ /**
+ * Resolve expiration time for the given trust chain.
+ *
+ * @param trustChain trust chain
+ * @return expiration time
+ */
+ @Nullable private Instant resolveTrustChainExpiration(@Nonnull final List<EntityStatement<?>> trustChain) {
+ Instant metadataExpiration = null;
+ for (final EntityStatement<?> statement : trustChain) {
+ final Instant statementExpiration = statement.getParsedPayload().getExpiration();
+ metadataExpiration = metadataExpiration == null ? statementExpiration :
+ statementExpiration.isBefore(metadataExpiration) ? statementExpiration : metadataExpiration;
+ }
+ return metadataExpiration;
+ }
+}
\ No newline at end of file
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/ValidateResolveEntityRequest.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/ValidateResolveEntityRequest.java
new file mode 100644
index 0000000..bd3114d
--- /dev/null
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/ValidateResolveEntityRequest.java
@@ -0,0 +1,186 @@
+/*
+ * 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.oidfed.profile.impl;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
+import org.slf4j.Logger;
+
+import com.nimbusds.oauth2.sdk.Request;
+
+import net.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.oidc.metadata.cache.MetadataCache;
+import net.shibboleth.oidc.metadata.cache.MetadataCacheException;
+import net.shibboleth.oidfed.messaging.impl.ResolveEntityRequest;
+import net.shibboleth.oidfed.metadata.cache.SubjectEntityIDCriterion;
+import net.shibboleth.oidfed.metadata.cache.local.LocalKeyContainer;
+import net.shibboleth.oidfed.profile.OidFederationEventIds;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.primitive.StringSupport;
+import net.shibboleth.shared.resolver.CriteriaSet;
+
+/**
+ * Validates the resolve entity request against the profile configuration and stores the validated (possibly modified)
+ * request into {@link RelyingPartyCachedMessageContext#setValidatedRequest(Request)}.
+ *
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#INVALID_MSG_CTX}
+ */
+public class ValidateResolveEntityRequest extends AbstractProfileAction {
+
+ /** Class logger. */
+ @Nonnull private Logger log = LoggerFactory.getLogger(ValidateResolveEntityRequest.class);
+
+ /** Strategy used to create the cached message context. */
+ @Nonnull
+ private Function<ProfileRequestContext, RelyingPartyCachedMessageContext> cachedMessageContextCreationStrategy;
+
+ /** Cache containing local copies of trusted trust anchor keys. */
+ @NonnullAfterInit private MetadataCache<Map<String, LocalKeyContainer>> localTrustAnchorsCache;
+
+ /** Request message to operate on. */
+ @NonnullBeforeExec private ResolveEntityRequest requestMessage;
+
+ /** Cached message context to operate on. */
+ @NonnullBeforeExec private RelyingPartyCachedMessageContext cachedMessageContext;
+
+ /**
+ * Constructor.
+ */
+ public ValidateResolveEntityRequest() {
+ final Function<ProfileRequestContext, RelyingPartyCachedMessageContext> reccs =
+ new ChildContextLookup<>(RelyingPartyCachedMessageContext.class, true).compose(
+ new InboundMessageContextLookup());
+ assert reccs != null;
+ cachedMessageContextCreationStrategy = reccs;
+ }
+
+ /**
+ * Set the strategy used to return or create the resolve entity context.
+ *
+ * @param strategy creation strategy
+ */
+ public void setResolveEntityContextCreationStrategy(
+ @Nonnull final Function<ProfileRequestContext, RelyingPartyCachedMessageContext> strategy) {
+ checkSetterPreconditions();
+ cachedMessageContextCreationStrategy = Constraint.isNotNull(strategy,
+ "RelyingPartyResolveEntityContext creation strategy cannot be null");
+ }
+
+ /**
+ * Set the strategy used to lookup the trust chain context.
+ *
+ * @param cache lookup strategy
+ */
+ public void setLocalTrustAnchorsCache(
+ @Nonnull final MetadataCache<Map<String, LocalKeyContainer>> cache) {
+ checkSetterPreconditions();
+ localTrustAnchorsCache =
+ Constraint.isNotNull(cache, "LocalTrustAnchorsCache cannot be null");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+ if (localTrustAnchorsCache == null) {
+ throw new ComponentInitializationException("LocalTrustAnchorsCache cannot be null");
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+ if (!super.doPreExecute(profileRequestContext)) {
+ return false;
+ }
+
+ requestMessage = Optional.ofNullable(profileRequestContext.getInboundMessageContext())
+ .map(messageContext -> messageContext.getMessage())
+ .filter(ResolveEntityRequest.class::isInstance)
+ .map(ResolveEntityRequest.class::cast)
+ .orElse(null);
+ if (requestMessage == null) {
+ log.error("{} Unable to fetch the request message to operate on", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return false;
+ }
+
+ cachedMessageContext = cachedMessageContextCreationStrategy.apply(profileRequestContext);
+ if (cachedMessageContext == null) {
+ log.error("{} Unable to create resolve entity context", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return false;
+ }
+ return true;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+ final List<String> validatedAnchors = requestMessage.getTrustAnchors().stream()
+ .filter(anchor -> isLocallyTrusted(anchor))
+ .toList();
+ if (validatedAnchors.isEmpty()) {
+ log.info("{} No locally trusted anchors left after filtering", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, OidFederationEventIds.INVALID_TRUST_ANCHOR);
+ return;
+ }
+ log.debug("{} The following trust anchors were validated: {}", getLogPrefix(), validatedAnchors);
+ cachedMessageContext.setValidatedRequest(
+ new ResolveEntityRequest(requestMessage.getEndpointURI(), requestMessage.getSubject(),
+ validatedAnchors, requestMessage.getEntityTypes(), requestMessage.getClientAuthentication()));
+ }
+
+ /**
+ * Verifies whether the given trust anchor candidate is locally trusted.
+ *
+ * @param candidate the trust anchor candidate
+ * @return true if locally trusted, false otherwise
+ */
+ protected boolean isLocallyTrusted(@Nullable final String candidate) {
+ if (StringSupport.trimOrNull(candidate) == null) {
+ return false;
+ }
+ assert candidate != null;
+ final SubjectEntityIDCriterion criterion = new SubjectEntityIDCriterion(candidate);
+ try {
+ final List<Map<String,LocalKeyContainer>> result = localTrustAnchorsCache.get(new CriteriaSet(criterion));
+ if (result.isEmpty() || result.get(0).isEmpty()) {
+ log.debug("{} No locally trusted keys found for {}", getLogPrefix(), candidate);
+ return false;
+ }
+ return result.get(0).containsKey(candidate);
+ } catch (final MetadataCacheException e) {
+ log.error("{} Could not fetch value for {} from the metadata cache", getLogPrefix(), candidate, e);
+ }
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/ValidateSelectedTrustChain.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/ValidateSelectedTrustChain.java
new file mode 100644
index 0000000..f9aa9a7
--- /dev/null
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/ValidateSelectedTrustChain.java
@@ -0,0 +1,231 @@
+/*
+ * 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.oidfed.profile.impl;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.oidc.metadata.cache.MetadataCache;
+import net.shibboleth.oidc.metadata.cache.MetadataCacheException;
+import net.shibboleth.oidfed.messaging.impl.ResolveEntityRequest;
+import net.shibboleth.oidfed.metadata.EntityStatement;
+import net.shibboleth.oidfed.metadata.cache.SubjectEntityIDCriterion;
+import net.shibboleth.oidfed.metadata.cache.configuration.EntityConfigurationContainer;
+import net.shibboleth.oidfed.profile.OidFederationEventIds;
+import net.shibboleth.oidfed.profile.context.RelyingPartyTrustChainContext;
+import net.shibboleth.oidfed.profile.context.VerifiedTrustChain;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.resolver.CriteriaSet;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.action.ActionSupport;
+
+/**
+ * Validates that the currenty selected trust chain meets the trust anchor requirements in the resolve entity request.
+ * If not and if other candidates remains, {@link OidFederationEventIds#RESELECT_TRUST_CHAIN} is published. If no other
+ * candidates are available, {@link OidFederationEventIds#INVALID_TRUST_ANCHOR} or
+ * {@link OidFederationEventIds#INVALID_SUBJECT} is published.
+ *
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#INVALID_MSG_CTX}
+ * @event {@link OidFederationEventIds#RESELECT_TRUST_CHAIN}
+ * @event {@link OidFederationEventIds#INVALID_TRUST_ANCHOR}
+ * @event {@link OidFederationEventIds#INVALID_SUBJECT}
+ */
+public class ValidateSelectedTrustChain extends AbstractProfileAction {
+
+ /** Class logger. */
+ @Nonnull private Logger log = LoggerFactory.getLogger(ValidateSelectedTrustChain.class);
+
+ /** Metadata cache for entity configurations. */
+ @NonnullAfterInit private MetadataCache<EntityConfigurationContainer> entityConfigurationCache;
+
+ /** Strategy used to lookup the trust chain context. */
+ @Nonnull private Function<ProfileRequestContext, RelyingPartyTrustChainContext> trustChainContextLookupStrategy;
+
+ /** Strategy used to locate the resolve entity context. */
+ @Nonnull
+ private Function<ProfileRequestContext, RelyingPartyCachedMessageContext> resolveEntityContextLookupStrategy;
+
+ /** The validated request to operate on. */
+ @NonnullBeforeExec private ResolveEntityRequest validatedRequest;
+
+ /**
+ * Constructor.
+ */
+ public ValidateSelectedTrustChain() {
+ final Function<ProfileRequestContext, RelyingPartyTrustChainContext> tcls =
+ new ChildContextLookup<>(RelyingPartyTrustChainContext.class).compose(
+ new InboundMessageContextLookup());
+ assert tcls != null;
+ trustChainContextLookupStrategy = tcls;
+ final Function<ProfileRequestContext, RelyingPartyCachedMessageContext> recls =
+ new ChildContextLookup<>(RelyingPartyCachedMessageContext.class).compose(
+ new InboundMessageContextLookup());
+ assert recls != null;
+ resolveEntityContextLookupStrategy = recls;
+ }
+
+ /**
+ * Set the metadata cache for entity configurations.
+ *
+ * @param cache What to set.
+ */
+ public void setEntityConfigurationCache(@Nonnull final MetadataCache<EntityConfigurationContainer> cache) {
+ checkSetterPreconditions();
+ entityConfigurationCache = Constraint.isNotNull(cache, "Entity configuration metadata cache cannot be null");
+ }
+
+ /**
+ * Set the strategy used to lookup the trust chain context.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setTrustChainContextLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext, RelyingPartyTrustChainContext> strategy) {
+ checkSetterPreconditions();
+ trustChainContextLookupStrategy =
+ Constraint.isNotNull(strategy, "TrustChainContextLookupStrategy cannot be null");
+ }
+
+ /**
+ * Set the strategy used to locate the resolve entity context
+ *
+ * @param strategy What to set.
+ */
+ public void setResolveEntityContextLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext,RelyingPartyCachedMessageContext> strategy) {
+ checkSetterPreconditions();
+ resolveEntityContextLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null!");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+
+ if (entityConfigurationCache == null) {
+ throw new ComponentInitializationException("Entity configuration metadata cache cannot be null");
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+ if (!super.doPreExecute(profileRequestContext)) {
+ return false;
+ }
+
+ final RelyingPartyCachedMessageContext cachedResponseContext =
+ resolveEntityContextLookupStrategy.apply(profileRequestContext);
+ if (cachedResponseContext == null) {
+ log.error("{} Could not resolve cached message context", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return false;
+ }
+
+ if (cachedResponseContext.getValidatedRequest() instanceof ResolveEntityRequest resolveEntityRequest) {
+ validatedRequest = resolveEntityRequest;
+ } else {
+ log.error("{} Could not resolve request message", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return false;
+ }
+
+ return true;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+ final RelyingPartyTrustChainContext trustChainContext =
+ trustChainContextLookupStrategy.apply(profileRequestContext);
+ final VerifiedTrustChain selectedTrustChain =
+ trustChainContext != null ? trustChainContext.getSelectedTrustChain() : null;
+ if (selectedTrustChain == null) {
+ final List<VerifiedTrustChain> allChains =
+ trustChainContext != null ? trustChainContext.getPolicyCompliantTrustChains() : null;
+ if (allChains == null || allChains.isEmpty()) {
+ if (isSubjectValid(validatedRequest.getSubject())) {
+ log.debug("{} No trust chains were resolved, subject is valid", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, OidFederationEventIds.INVALID_TRUST_ANCHOR);
+ return;
+ } else {
+ log.debug("{} No trust chains were resolved, subject is not valid", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, OidFederationEventIds.INVALID_SUBJECT);
+ return;
+ }
+ } else {
+ log.debug("{} No trust chains left to choose from", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, OidFederationEventIds.INVALID_TRUST_ANCHOR);
+ return;
+ }
+ }
+
+ final List<String> trustAnchors = validatedRequest.getTrustAnchors();
+ final List<EntityStatement<?>> candidateChain = selectedTrustChain.getTrustChain();
+ assert candidateChain != null;
+ final String candidateAnchor = candidateChain.get(candidateChain.size() - 1).getSubject();
+ if (!trustAnchors.contains(candidateAnchor)) {
+ log.debug("{} Selected trust chain candidate has unrequested trust anchor {}", getLogPrefix(),
+ candidateAnchor);
+ assert trustChainContext != null;
+ final List<List<EntityStatement<?>>> rejectedTrustChains = trustChainContext.getRejectedTrustChains();
+ if (rejectedTrustChains == null) {
+ trustChainContext.setRejectedTrustChains(List.of(selectedTrustChain.getTrustChain()));
+ } else {
+ final List<List<EntityStatement<?>>> rejectedChains = new ArrayList<>(rejectedTrustChains);
+ rejectedChains.add(selectedTrustChain.getTrustChain());
+ trustChainContext.setRejectedTrustChains(CollectionSupport.copyToList(rejectedChains));
+ }
+ ActionSupport.buildEvent(profileRequestContext, OidFederationEventIds.RESELECT_TRUST_CHAIN);
+ return;
+ }
+ }
+
+ /**
+ * Checks if an entity configuration can be resolved for the given subject and it's thus valid for federation.
+ *
+ * @param subject the subject to be verified
+ * @return true if the given subject is valid, false otherwise.
+ */
+ protected boolean isSubjectValid(@Nonnull final String subject) {
+ final SubjectEntityIDCriterion subjectCriterion = new SubjectEntityIDCriterion(subject);
+ try {
+ final List<EntityConfigurationContainer> result =
+ entityConfigurationCache.get(new CriteriaSet(subjectCriterion));
+ if (result.size() == 1 && result.get(0).getStatement() != null) {
+ return true;
+ }
+ } catch (final MetadataCacheException e) {
+ log.debug("{} Exception catched when resolving entty configuration", e);
+ }
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/navigate/DefaultEntityTypesLookupFunction.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/navigate/DefaultEntityTypesLookupFunction.java
new file mode 100644
index 0000000..9866b5b
--- /dev/null
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/navigate/DefaultEntityTypesLookupFunction.java
@@ -0,0 +1,46 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.oidfed.profile.navigate;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Function;
+
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import net.shibboleth.oidfed.messaging.impl.ResolveEntityRequest;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.primitive.NonnullSupplier;
+
+/**
+ * Default function to lookup entity types to be included to the response metadata.
+ */
+public class DefaultEntityTypesLookupFunction implements Function<ProfileRequestContext, List<String>> {
+
+ /** {@inheritDoc} */
+ @Override @Nullable
+ public List<String> apply(@Nullable final ProfileRequestContext profileRequestContext) {
+ return Optional.ofNullable(profileRequestContext)
+ .map(prc -> prc.getInboundMessageContext())
+ .map(msgCtx -> msgCtx.getMessage())
+ .filter(ResolveEntityRequest.class::isInstance)
+ .map(ResolveEntityRequest.class::cast)
+ .map(req -> req.getEntityTypes())
+ .orElseGet(NonnullSupplier.of(CollectionSupport.emptyList()));
+ }
+
+}
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/credential/ClientInformationFederationEntityCredentialResolver.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/credential/ClientInformationFederationEntityCredentialResolver.java
deleted file mode 100644
index d205eb4..0000000
--- a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/credential/ClientInformationFederationEntityCredentialResolver.java
+++ /dev/null
@@ -1,128 +0,0 @@
-/*
- * 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.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.oidc.security.credential.JOSEObjectCredentialResolver;
-import net.shibboleth.oidc.security.credential.impl.BasicJOSEObjectCredentialResolver;
-import net.shibboleth.oidc.security.jose.criterion.ClientInformationCriterion;
-import net.shibboleth.oidfed.metadata.EntityStatement;
-import net.shibboleth.oidfed.metadata.cache.SubjectEntityStatementCriterion;
-import net.shibboleth.oidfed.metadata.util.EntityStatementHelper;
-import net.shibboleth.oidfed.support.ClientInformationExtensionSupport;
-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).
- * @throws ResolverException if resolution fails
- */
- @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/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/credential/SelectedTrustChainFederationEntityCredentialResolver.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/credential/SelectedTrustChainFederationEntityCredentialResolver.java
new file mode 100644
index 0000000..c91fbde
--- /dev/null
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/credential/SelectedTrustChainFederationEntityCredentialResolver.java
@@ -0,0 +1,124 @@
+/*
+ * 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.oidfed.security.credential;
+
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
+import org.opensaml.profile.criterion.ProfileRequestContextCriterion;
+import org.opensaml.security.credential.Credential;
+import org.slf4j.Logger;
+
+import net.shibboleth.oidc.security.credential.JOSEObjectCredentialResolver;
+import net.shibboleth.oidc.security.credential.impl.BasicJOSEObjectCredentialResolver;
+import net.shibboleth.oidfed.metadata.EntityStatement;
+import net.shibboleth.oidfed.metadata.cache.SubjectEntityStatementCriterion;
+import net.shibboleth.oidfed.profile.context.RelyingPartyTrustChainContext;
+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 from the selected trust chain stored in {@link RelyingPartyTrustChainContext}.
+ */
+public class SelectedTrustChainFederationEntityCredentialResolver extends BasicJOSEObjectCredentialResolver {
+
+ /** Class logger. */
+ @Nonnull
+ private final Logger log = LoggerFactory.getLogger(SelectedTrustChainFederationEntityCredentialResolver.class);
+
+ /** Resolver for fetching federation entity credentials from entity configuration. */
+ @Nonnull private final JOSEObjectCredentialResolver entityConfigurationCredentialResolver;
+
+ /** Strategy used to lookup the trust chain context. */
+ @Nonnull private Function<ProfileRequestContext, RelyingPartyTrustChainContext> trustChainContextLookupStrategy;
+
+ /**
+ * Constructor.
+ *
+ * @param resolver The resolver for fetching federation entity credentials from entity configuration.
+ */
+ public SelectedTrustChainFederationEntityCredentialResolver(@Nonnull
+ @ParameterName(name="entityConfigurationCredentialResolver") final JOSEObjectCredentialResolver resolver) {
+ entityConfigurationCredentialResolver = Constraint.isNotNull(resolver,
+ "EntityConfigurationCredentialResolver cannot be null");
+ final Function<ProfileRequestContext, RelyingPartyTrustChainContext> tcls =
+ new ChildContextLookup<>(RelyingPartyTrustChainContext.class).compose(
+ new InboundMessageContextLookup());
+ assert tcls != null;
+ trustChainContextLookupStrategy = tcls;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ @Nonnull protected Iterable<Credential> resolveFromSource(@Nullable final CriteriaSet criteriaSet)
+ throws ResolverException {
+
+ Constraint.isNotNull(criteriaSet, "CriteriaSet was null");
+
+ if (criteriaSet != null) {
+ final ProfileRequestContextCriterion criterion = criteriaSet.get(ProfileRequestContextCriterion.class);
+ if (criterion != null) {
+ return resolveFromProfileRequestContext(criteriaSet, criterion.getProfileRequestContext());
+ }
+ }
+
+ log.debug("Criteria did not contain a ProfileRequestContextCriterion could not perform resolution");
+ return CollectionSupport.emptySet();
+ }
+
+ /**
+ * Resolve the keyset from the entity configuration payload.
+ *
+ * @param criteriaSet the criteria set
+ * @param profileRequestContext the PRC containing selected trust chain
+ *
+ * @return a collection of credentials from the entity configuration key set (if any).
+ * @throws ResolverException if resolution fails
+ */
+ @Nonnull protected Iterable<Credential> resolveFromProfileRequestContext(@Nonnull final CriteriaSet criteriaSet,
+ @Nonnull final ProfileRequestContext profileRequestContext) throws ResolverException {
+
+
+ final var trustChainContext = trustChainContextLookupStrategy.apply(profileRequestContext);
+ if (trustChainContext == null || trustChainContext.getSelectedTrustChain() == null) {
+ log.error("Unable to locate selected trust chain");
+ return CollectionSupport.emptySet();
+ }
+
+ final var selectedTrustChain = trustChainContext.getSelectedTrustChain();
+ if (selectedTrustChain == null) {
+ log.error("Selected trust chain contents is not populated");
+ return CollectionSupport.emptySet();
+ }
+
+ final EntityStatement<?> configuration = selectedTrustChain.getTrustChain().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));
+ }
+
+}
\ No newline at end of file
diff --git a/oidfed-common-impl/src/main/resources/net/shibboleth/oidfed/conf/oidfed/oidfed.properties b/oidfed-common-impl/src/main/resources/net/shibboleth/oidfed/conf/oidfed/oidfed.properties
index bf994d4..c2e1c91 100644
--- a/oidfed-common-impl/src/main/resources/net/shibboleth/oidfed/conf/oidfed/oidfed.properties
+++ b/oidfed-common-impl/src/main/resources/net/shibboleth/oidfed/conf/oidfed/oidfed.properties
@@ -131,3 +131,20 @@ idp.oidfed.entityConfiguration.authorityHints = https://your.authority.example.o
#idp.oidfed.admin.resolvertest.TrustChainMetadataCache = shibboleth.oidfed.TrustChainMetadataCache
#idp.oidfed.admin.resolvertest.TrustMarkStatusMetadataCache = shibboleth.oidfed.TrustMarkStatusMetadataCache
+# resolve-entity-beans
+#idp.oidfed.resolveEntity.authn.flows = OAuth2Client
+#idp.service.logging.oidfedresolve = OIDFED.ResolveEntity
+#idp.oidfed.cache.resolveEntity.minRefreshDelay = PT1S
+#idp.oidfed.cache.resolveEntity.maxRefreshDelay = PT30S
+#idp.oidfed.resolveEntity.LocalTrustAnchorsMetadataCache = shibboleth.oidfed.LocalTrustAnchorsMetadataCache
+#idp.oidfed.resolveEntity.MetadataPolicyOperators = shibboleth.oidfed.StandardMetadataPolicyOperators
+#idp.oidfed.resolveEntity.MetadataPolicyMergingStrategy = MetadataPolicyMergingStrategy
+#idp.oidfed.resolveEntity.LocalMetadataPolicyStrategy = DefaultLocalMetadataPolicyStrategy
+#idp.oidfed.resolveEntity.TrustChainMetadataCache = shibboleth.oidfed.TrustChainMetadataCache
+#idp.oidfed.resolveEntity.TrustMarkStatusMetadataCache = shibboleth.oidfed.TrustMarkStatusMetadataCache
+#idp.oidfed.resolveEntity.sigalg = RS256
+#idp.oidfed.resolveEntity.endpointAuthMethods = private_key_jwt
+#idp.oidfed.resolveEntity.targetedEndpointAsJWTAudience = true
+#idp.oidfed.resolveEntity.requireSingleJWTAudience = true
+#idp.oidfed.resolveEntity.trustchain.resolver.useResolverApiCondition = shibboleth.Conditions.FALSE
+#idp.oidfed.resolveEntity.trustchain.resolver.fallbackToLocalCondition = shibboleth.Conditions.TRUE
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list