[java-oidfed-common] 03/05: Initial version of the 'oidfed/resolve-trust-chains' flow
Codeberg
noreply at shibboleth.net
Fri May 22 10:52:50 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/9d1ae1d6788c0b766d08122b7fa617ce449331ad
commit 9d1ae1d6788c0b766d08122b7fa617ce449331ad
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Fri May 22 12:15:37 2026 +0300
Initial version of the 'oidfed/resolve-trust-chains' flow
- Imported and refactored required webflow actions and context from java-idp-plugin-oidc-op-oidfed
- API: net.shibboleth.oidfed.profile.context
- IMPL: net.shibboleth.oidfed.profile.impl: CallResolveEntityApi and ResolveTrustChains
- Referred predicates/functions in net.shibboleth.oidfed.profile.navigate
---
.../context/RelyingPartyTrustChainContext.java | 280 +++++++++++
.../oidfed/profile/context/VerifiedTrustChain.java | 76 +++
.../resolve-trust-chains-beans.xml | 70 +++
.../resolve-trust-chains-flow.xml | 52 ++
.../impl/AbstractTrustChainResolutionAction.java | 388 +++++++++++++++
.../oidfed/profile/impl/CallResolveEntityApi.java | 534 +++++++++++++++++++++
.../oidfed/profile/impl/ResolveTrustChains.java | 263 ++++++++++
...mbinedMetadataFromTrustChainLookupStrategy.java | 86 ++++
.../DefaultMetadataValidationCondition.java | 82 ++++
.../DefaultTrustChainIDsLookupStrategy.java | 43 ++
...ultTrustChainMetadataPolicyMergingStrategy.java | 145 ++++++
11 files changed, 2019 insertions(+)
diff --git a/oidfed-common-api/src/main/java/net/shibboleth/oidfed/profile/context/RelyingPartyTrustChainContext.java b/oidfed-common-api/src/main/java/net/shibboleth/oidfed/profile/context/RelyingPartyTrustChainContext.java
new file mode 100644
index 0000000..5329012
--- /dev/null
+++ b/oidfed-common-api/src/main/java/net/shibboleth/oidfed/profile/context/RelyingPartyTrustChainContext.java
@@ -0,0 +1,280 @@
+/*
+ * 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.time.Instant;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.BaseContext;
+
+import com.nimbusds.jwt.SignedJWT;
+
+import net.shibboleth.oidfed.metadata.EntityStatement;
+import net.shibboleth.oidfed.profile.TrustedRemoteResolverEntity;
+import net.shibboleth.shared.annotation.constraint.Live;
+
+/**
+ * Subcontext carrying information for trust chains related to a relying party.
+ */
+public final class RelyingPartyTrustChainContext extends BaseContext {
+
+ /** Trust chain provided within the request. */
+ @Nullable private List<EntityStatement<?>> providedTrustChain;
+
+ /** All resolved trust chains for the relying party. */
+ @Nullable private List<List<EntityStatement<?>>> resolvedTrustChains;
+
+ /** Policy-compliant trust chains for the relying party. */
+ @Nullable private List<VerifiedTrustChain> policyCompliantTrustChains;
+
+ /** Selected trust chain for the relying party. */
+ @Nullable private VerifiedTrustChain selectedTrustChain;
+
+ /** Expiration instant for the selected metadata. */
+ @Nullable private Instant selectedMetadataExpiration;
+
+ /** Verified trust mark IDs for the selected trust chain. */
+ @Nullable private Map<String, List<String>> verifiedTrustMarkIds;
+
+ /** Verified trust marks for the selected trust chain. */
+ @Nullable private Map<String, List<SignedJWT>> verifiedTrustMarks;
+
+ /** Verified trust mark issuers. */
+ @Nonnull @Live private Map<String, EntityStatement<?>> verifiedTrustMarkIssuers;
+
+ /** All previously selected but rejected trust chains. */
+ @Nullable private List<List<EntityStatement<?>>> rejectedTrustChains;
+
+ /** All already attempted trusted remote resolver entities. */
+ @Nullable private List<TrustedRemoteResolverEntity> attemptedTrustedRemoteResolverEntities;
+
+ /**
+ * Constructor.
+ */
+ public RelyingPartyTrustChainContext() {
+ verifiedTrustMarkIssuers = new HashMap<>();
+ }
+
+ /**
+ * Get the trust chain provided within the request.
+ *
+ * @return provided trust chain
+ */
+ @Nullable public List<EntityStatement<?>> getProvidedTrustChain() {
+ return providedTrustChain;
+ }
+
+ /**
+ * Set the trust chain provided within the request.
+ *
+ * @param trustChain provided trust chain
+ *
+ * @return this context
+ */
+ @Nonnull public RelyingPartyTrustChainContext setProvidedTrustChain(
+ @Nullable final List<EntityStatement<?>> trustChain) {
+ providedTrustChain = trustChain;
+ return this;
+ }
+
+ /**
+ * Get the resolved trust chains for the relying party.
+ *
+ * @return the trust chains
+ */
+ @Nullable public List<List<EntityStatement<?>>> getResolvedTrustChains() {
+ return resolvedTrustChains;
+ }
+
+ /**
+ * Set the resolved trust chains for the relying party.
+ *
+ * @param trustChains the trust chains
+ *
+ * @return this context
+ */
+ @Nonnull public RelyingPartyTrustChainContext setResolvedTrustChains(
+ @Nullable final List<List<EntityStatement<?>>> trustChains) {
+ resolvedTrustChains = trustChains;
+ return this;
+ }
+
+ /**
+ * Get the policy-compliant trust chains for the relying party.
+ *
+ * @return the trust chains
+ */
+ @Nullable public List<VerifiedTrustChain> getPolicyCompliantTrustChains() {
+ return policyCompliantTrustChains;
+ }
+
+ /**
+ * Set the policy-compliant trust chains for the relying party.
+ *
+ * @param chains the trust chains and client informations
+ *
+ * @return this context
+ */
+ @Nonnull public RelyingPartyTrustChainContext setPolicyCompliantTrustChains(
+ @Nullable final List<VerifiedTrustChain> chains) {
+ policyCompliantTrustChains = chains;
+ return this;
+ }
+
+ /**
+ * Get the selected trust chain for the relying party.
+ *
+ * @return the trust chain
+ */
+ @Nullable public VerifiedTrustChain getSelectedTrustChain() {
+ return selectedTrustChain;
+ }
+
+ /**
+ * Set the selected trust chain for the relying party.
+ *
+ * @param chain the selected trust chain
+ *
+ * @return this context
+ */
+ @Nonnull public RelyingPartyTrustChainContext setSelectedTrustChains(
+ @Nullable final VerifiedTrustChain chain) {
+ selectedTrustChain = chain;
+ return this;
+ }
+
+ /**
+ * Get the expiration instant for the selected metadata.
+ *
+ * @return the expiration instant
+ */
+ @Nullable public Instant getSelectedMetadataExpiration() {
+ return selectedMetadataExpiration;
+ }
+
+ /**
+ * Set the expiration instant for the selected metadata.
+ *
+ * @param expiration the expiration instant
+ *
+ * @return this context
+ */
+ @Nonnull public RelyingPartyTrustChainContext setSelectedMetadataExpiration(@Nullable final Instant expiration) {
+ selectedMetadataExpiration = expiration;
+ return this;
+ }
+
+ /**
+ * Get the verified trust mark IDs for the selected trust chain.
+ *
+ * @return verified trust mark IDs
+ */
+ @Nullable public Map<String, List<String>> getVerifiedTrustMarkIds() {
+ return verifiedTrustMarkIds;
+ }
+
+ /**
+ * Set the verified trust mark IDs for the selected trust chain.
+ *
+ * @param ids verified trust mark IDs
+ *
+ * @return this context
+ */
+ @Nonnull public RelyingPartyTrustChainContext setVerifiedTrustMarkIds(
+ @Nullable final Map<String, List<String>> ids) {
+ verifiedTrustMarkIds = ids;
+ return this;
+ }
+
+ /**
+ * Get the verified trust marks for the selected trust chain.
+ *
+ * @return verified trust marks
+ */
+ @Nullable public Map<String, List<SignedJWT>> getVerifiedTrustMarks() {
+ return verifiedTrustMarks;
+ }
+
+ /**
+ * Set the verified trust marks for the selected trust chain.
+ *
+ * @param trustMarks verified trust marks
+ *
+ * @return this context
+ */
+ @Nonnull public RelyingPartyTrustChainContext setVerifiedTrustMarks(
+ @Nullable final Map<String, List<SignedJWT>> trustMarks) {
+ verifiedTrustMarks = trustMarks;
+ return this;
+ }
+
+ /**
+ * Get the verified trust mark issuers.
+ *
+ * @return verified trust mark issuers
+ */
+ @Nonnull @Live public Map<String, EntityStatement<?>> getVerifiedTrustMarkIssuers() {
+ return verifiedTrustMarkIssuers;
+ }
+
+ /**
+ * Get the previously selected but rejected trust chains for the relying party.
+ *
+ * @return the trust chains
+ */
+ @Nullable public List<List<EntityStatement<?>>> getRejectedTrustChains() {
+ return rejectedTrustChains;
+ }
+
+ /**
+ * Set the previously selected but rejected trust chains for the relying party.
+ *
+ * @param trustChains the trust chains
+ *
+ * @return this context
+ */
+ @Nonnull public RelyingPartyTrustChainContext setRejectedTrustChains(
+ @Nullable final List<List<EntityStatement<?>>> trustChains) {
+ rejectedTrustChains = trustChains;
+ return this;
+ }
+
+ /**
+ * Get the attempted remote resolver entities for the relying party.
+ *
+ * @return the trust chains
+ */
+ @Nullable public List<TrustedRemoteResolverEntity> getAttemptedTrustedRemoteResolverEntities() {
+ return attemptedTrustedRemoteResolverEntities;
+ }
+
+ /**
+ * Set the attempted remote resolver entities for the relying party.
+ *
+ * @param attemptedEntities the attempted remote resolver entities
+ *
+ * @return this context
+ */
+ @Nonnull public RelyingPartyTrustChainContext setAttemptedTrustedRemoteResolverEntities(
+ @Nullable final List<TrustedRemoteResolverEntity> attemptedEntities) {
+ attemptedTrustedRemoteResolverEntities = attemptedEntities;
+ return this;
+ }
+}
\ No newline at end of file
diff --git a/oidfed-common-api/src/main/java/net/shibboleth/oidfed/profile/context/VerifiedTrustChain.java b/oidfed-common-api/src/main/java/net/shibboleth/oidfed/profile/context/VerifiedTrustChain.java
new file mode 100644
index 0000000..3562109
--- /dev/null
+++ b/oidfed-common-api/src/main/java/net/shibboleth/oidfed/profile/context/VerifiedTrustChain.java
@@ -0,0 +1,76 @@
+/*
+ * 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.List;
+
+import javax.annotation.Nonnull;
+
+import net.shibboleth.oidfed.metadata.EntityStatement;
+import net.shibboleth.oidfed.metadata.payload.claim.Metadata;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.logic.Constraint;
+
+/**
+ * A wrapper for a verified trust chain containing the trust chain and its policy-enforced metadata. The trust chain
+ * length is verified to equal to three or more.
+ */
+public class VerifiedTrustChain {
+
+ /** Verified trust chain. */
+ @Nonnull @NotEmpty private final List<EntityStatement<?>> trustChain;
+
+ /** Policy-enforced metadata. */
+ @Nonnull private Metadata metadata;
+
+ /**
+ * Constructor.
+ *
+ * @param chain verified trust chain
+ * @param data policy-enforced metadata
+ */
+ public VerifiedTrustChain(@Nonnull @NotEmpty final List<EntityStatement<?>> chain, @Nonnull final Metadata data) {
+ trustChain = Constraint.isNotNull(chain, "Trust chain cannot be null");
+ Constraint.isTrue(chain.size() > 2, "Trust chain cannot be shorter than three");
+ metadata = Constraint.isNotNull(data, "Metadata cannot be null");
+ }
+
+ /**
+ * Get the verified trust chain.
+ *
+ * @return verified trust chain
+ */
+ @Nonnull @NotEmpty public List<EntityStatement<?>> getTrustChain() {
+ return trustChain;
+ }
+
+ /**
+ * Get the policy-enforced metadata.
+ *
+ * @return policy-enforced metadata
+ */
+ @Nonnull public Metadata getMetadata() {
+ return metadata;
+ }
+
+ /**
+ * Set the policy-enforced metadata.
+ *
+ * @param data policy-enforced metadata
+ */
+ public void setMetadata(@Nonnull Metadata data) {
+ metadata = Constraint.isNotNull(data, "Metadata cannot be null");
+ }
+}
diff --git a/oidfed-common-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/resolve-trust-chains/resolve-trust-chains-beans.xml b/oidfed-common-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/resolve-trust-chains/resolve-trust-chains-beans.xml
new file mode 100644
index 0000000..cc5e1ab
--- /dev/null
+++ b/oidfed-common-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/resolve-trust-chains/resolve-trust-chains-beans.xml
@@ -0,0 +1,70 @@
+<?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="CallResolveEntityApi"
+ class="net.shibboleth.oidfed.profile.impl.CallResolveEntityApi"
+ p:entityConfigurationCache-ref="shibboleth.oidfed.EntityConfigurationMetadataCache"
+ p:resolveEntityTrustChainMetadataCache-ref="shibboleth.oidfed.ResolveEntityTrustChainMetadataCache"
+ p:objectMapper-ref="shibboleth.oidfed.JWTPayloadJSONObjectMapper"
+ p:trustedEntitiesLookupStrategy="#{getObject('shibboleth.oidfed.TrustedRemoteResolverEntitiesLookupStrategy') ?: getObject('shibboleth.oidfed.DefaultTrustedRemoteResolverEntitiesLookupStrategy')}"
+ p:preSelectedTrustChainIdsLookupStrategy="#{getObject('shibboleth.oidfed.PreSelectedTrustChainIDsLookupStrategy') ?: getObject('shibboleth.oidfed.DefaultPreSelectedTrustChainIDsLookupStrategy')}"
+ p:entityIDLookupStrategy="#{getObject('shibboleth.oidfed.trust-chain-resolver.EntityIDLookupStrategy') ?: getObject('shibboleth.oidfed.shibboleth.oidfed.trust-chain-resolver.DefaultEntityIDLookupStrategy')}"/>
+
+ <bean id="ResolveTrustChains" class="net.shibboleth.oidfed.profile.impl.ResolveTrustChains"
+ scope="prototype"
+ p:trustChainCache-ref="#{'%{idp.oidfed.authorize.TrustChainMetadataCache:shibboleth.oidfed.TrustChainMetadataCache}'.trim()}"
+ p:metadataPolicyMergingStrategy-ref="#{'%{idp.oidfed.authorize.TrustChainMetadataPolicyMergingStrategy:DefaultTrustChainMetadataPolicyMergingStrategy}'.trim()}"
+ p:metadataPolicyEnforcer-ref="#{'%{idp.oidfed.authorize.MetadataPolicyEnforcer:DefaultMetadataPolicyEnforcer}'.trim()}"
+ p:preSelectedTrustChainIdsLookupStrategy="#{getObject('shibboleth.oidfed.PreSelectedTrustChainIDsLookupStrategy') ?: getObject('shibboleth.oidfed.DefaultPreSelectedTrustChainIDsLookupStrategy')}"
+ p:arraysAsSpaceSeparatedList="%{idp.oidfed.policy.arraysAsSpaceSeparatedList:scope}"
+ p:metadataValidationCondition-ref="#{'%{idp.oidfed.MetadataValidationCondition:DefaultMetadataValidationCondition}'.trim()}"
+ p:federationPolicyConstraints-ref="%{idp.oidfed.FederationPolicyConstraints:shibboleth.oidfed.DefaultFederationPolicyConstraints}"
+ p:entityIDLookupStrategy="#{getObject('shibboleth.oidfed.trust-chain-resolver.EntityIDLookupStrategy') ?: getObject('shibboleth.oidfed.shibboleth.oidfed.trust-chain-resolver.DefaultEntityIDLookupStrategy')}">
+ <property name="metadataLookupStrategy">
+ <bean class="net.shibboleth.oidfed.profile.navigate.DefaultCombinedMetadataFromTrustChainLookupStrategy" />
+ </property>
+ </bean>
+
+ <bean id="DefaultMetadataValidationCondition"
+ class="net.shibboleth.oidfed.profile.navigate.DefaultMetadataValidationCondition" />
+
+ <bean id="DefaultMetadataPolicyEnforcer"
+ class="net.shibboleth.oidfed.metadata.policy.impl.DefaultFederationMetadataPolicyEnforcer"
+ p:metadataPolicyOperators-ref="#{'%{idp.oidfed.authorize.MetadataPolicyOperatorsy:shibboleth.oidfed.StandardMetadataPolicyOperators}'.trim()}"/>
+
+ <bean id="DefaultTrustChainMetadataPolicyMergingStrategy"
+ class="net.shibboleth.oidfed.profile.navigate.DefaultTrustChainMetadataPolicyMergingStrategy"
+ p:metadataPolicyMergingStrategy-ref="#{'%{idp.oidfed.authorize.MetadataPolicMergingyStrategy:MetadataPolicMergingyStrategy}'.trim()}"
+ p:localMetadataPolicyStrategy-ref="#{'%{idp.oidfed.authorize.LocalMetadataPolicyStrategy:DefaultLocalMetadataPolicyStrategy}'.trim()}"/>
+
+ <bean id="MetadataPolicMergingyStrategy"
+ class="net.shibboleth.oidfed.metadata.policy.impl.DefaultFederationMetadataPolicyMergingStrategy"
+ p:metadataPolicyOperators-ref="#{'%{idp.oidfed.authorize.MetadataPolicyOperatorsy:shibboleth.oidfed.StandardMetadataPolicyOperators}'.trim()}"/>
+
+ <bean id="DefaultLocalMetadataPolicyStrategy"
+ parent="shibboleth.Functions.Constant">
+ <constructor-arg name="target">
+ <util:map>
+ <entry key="scope">
+ <bean class="net.shibboleth.oidc.metadata.policy.MetadataPolicy">
+ <property name="defaultValue">
+ <util:list value-type="java.lang.String">
+ <value>openid</value>
+ </util:list>
+ </property>
+ </bean>
+ </entry>
+ <entry key="token_endpoint_auth_method">
+ <bean class="net.shibboleth.oidc.metadata.policy.MetadataPolicy" p:defaultValue="private_key_jwt" />
+ </entry>
+ </util:map>
+ </constructor-arg>
+ </bean>
+
+</beans>
diff --git a/oidfed-common-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/resolve-trust-chains/resolve-trust-chains-flow.xml b/oidfed-common-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/resolve-trust-chains/resolve-trust-chains-flow.xml
new file mode 100644
index 0000000..0776003
--- /dev/null
+++ b/oidfed-common-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/resolve-trust-chains/resolve-trust-chains-flow.xml
@@ -0,0 +1,52 @@
+<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"
+ abstract="true">
+
+ <decision-state id="ChooseResolutionMethod">
+ <on-entry>
+ <evaluate expression="flowRequestContext.getFlowScope().get('transitionAfterTrustChainResolution') != null ? flowRequestContext.getFlowScope().get('transitionAfterTrustChainResolution') : 'BuildResponseMessage'" result="flowScope.transitionAfterTrustChainResolution"/>
+ <evaluate expression="flowRequestContext.getFlowScope().get('transitionOnNoTrustChainsResolved') != null ? flowRequestContext.getFlowScope().get('transitionAfterTrustChainResolution') : 'NoTrustChainsResolved'" result="flowScope.transitionOnNoTrustChainsResolved"/>
+ </on-entry>
+ <if test="UseResolverApiCondition.test(opensamlProfileRequestContext)"
+ then="CallResolveEntityApi" else="ResolveTrustChains" />
+ </decision-state>
+
+ <action-state id="CallResolveEntityApi">
+ <evaluate expression="CallResolveEntityApi" />
+ <evaluate expression="'proceed'" />
+ <transition on="proceed" to="#{transitionAfterTrustChainResolution}">
+ <set name="flowScope.transitionForReselectTrustChain" value="'CallResolveEntityApi'" />
+ </transition>
+ <transition on="CheckFallback" to="CheckIfFallbackToLocalResolution" />
+ </action-state>
+
+ <decision-state id="CheckIfFallbackToLocalResolution">
+ <if test="FallbackToLocalResolutionCondition.test(opensamlProfileRequestContext)"
+ then="ResolveTrustChains" else="#{transitionOnNoTrustChainsResolved}" />
+ </decision-state>
+
+ <action-state id="ResolveTrustChains">
+ <evaluate expression="ResolveTrustChains" />
+ <evaluate expression="'proceed'" />
+ <transition on="proceed" to="#{transitionAfterTrustChainResolution}"/>
+ </action-state>
+
+ <end-state id="proceed"/>
+ <end-state id="InvalidMessageContext"/>
+ <end-state id="InvalidMetadataPolicy"/>
+ <end-state id="InvalidMetadataAgainstPolicy"/>
+ <end-state id="NoTrustChainsResolved" />
+ <end-state id="InvalidTrustChainAgainstConstraints" />
+
+ <global-transitions>
+ <transition on="InvalidMessageContext" to="InvalidMessageContext" />
+ <transition on="InvalidMetadataPolicy" to="InvalidMetadataPolicy" />
+ <transition on="InvalidMetadataAgainstPolicy" to="InvalidMetadataAgainstPolicy" />
+ <transition on="NoTrustChainsResolved" to="#{transitionOnNoTrustChainsResolved}" />
+ <transition on="InvalidTrustChainAgainstConstraints" to="InvalidTrustChainAgainstConstraints" />
+ </global-transitions>
+
+ <bean-import resource="classpath:/META-INF/net/shibboleth/idp/flows/oidfed/resolve-trust-chains/resolve-trust-chains-beans.xml" />
+
+</flow>
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/AbstractTrustChainResolutionAction.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/AbstractTrustChainResolutionAction.java
new file mode 100644
index 0000000..50856dd
--- /dev/null
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/AbstractTrustChainResolutionAction.java
@@ -0,0 +1,388 @@
+/*
+ * 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.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.BiFunction;
+import java.util.function.BiPredicate;
+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.minidev.json.JSONObject;
+import net.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.oidc.metadata.policy.MetadataPolicy;
+import net.shibboleth.oidfed.metadata.EntityConfiguration;
+import net.shibboleth.oidfed.metadata.EntityStatement;
+import net.shibboleth.oidfed.metadata.constraints.FederationPolicyConstraint;
+import net.shibboleth.oidfed.metadata.constraints.FederationPolicyConstraintHelper;
+import net.shibboleth.oidfed.metadata.impl.EntityConfigurationImpl;
+import net.shibboleth.oidfed.metadata.payload.claim.impl.MetadataImpl;
+import net.shibboleth.oidfed.metadata.payload.impl.EntityConfigurationPayloadImpl;
+import net.shibboleth.oidfed.metadata.policy.FederationMetadataPolicyHelper;
+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.Live;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.collection.Pair;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.logic.ConstraintViolationException;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+
+/**
+ * Base action for actions initializing {@link RelyingPartyTrustChainContext} and performing metadata and metadata
+ * policy related operations.
+ *
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#INVALID_MSG_CTX}
+ */
+public class AbstractTrustChainResolutionAction extends AbstractProfileAction {
+
+ /** Class logger. */
+ @Nonnull private Logger log = LoggerFactory.getLogger(AbstractTrustChainResolutionAction.class);
+
+ /** Strategy used to create the trust chain context. */
+ @Nonnull private Function<ProfileRequestContext, RelyingPartyTrustChainContext> trustChainContextCreationStrategy;
+
+ /** Strategy used to get combined entity metadata from trust chain. */
+ @NonnullAfterInit private Function<List<EntityStatement<?>>,Map<String,Map<String,Object>>> metadataLookupStrategy;
+
+ /** Strategy used to merge metadata policies in trust chain for specific entity type. */
+ @NonnullAfterInit private BiFunction<List<EntityStatement<?>>,String,Map<String, MetadataPolicy>>
+ metadataPolicyMergingStrategy;
+
+ /** Enforcer function for applying metadata policy for an item. */
+ @NonnullAfterInit private BiFunction<Object, MetadataPolicy, Pair<Object, Boolean>> metadataPolicyEnforcer;
+
+ /** Condition used to validate metadata for an entity type. */
+ @NonnullAfterInit private BiPredicate<String, Map<String, Object>> metadataValidationCondition;
+
+ /** List of claim names who are transformed from a space-separated String into a List. */
+ @Nonnull private List<String> arraysAsSpaceSeparatedList;
+
+ /** Map of supported federation policy constraints. */
+ @NonnullAfterInit private Map<String, FederationPolicyConstraint> federationPolicyConstraints;
+
+ /**
+ * Constructor.
+ */
+ public AbstractTrustChainResolutionAction() {
+ final Function<ProfileRequestContext, RelyingPartyTrustChainContext> tccs =
+ new ChildContextLookup<>(RelyingPartyTrustChainContext.class, true).compose(
+ new InboundMessageContextLookup());
+ assert tccs != null;
+ trustChainContextCreationStrategy = tccs;
+ arraysAsSpaceSeparatedList = CollectionSupport.listOf("scope");
+ }
+
+ /**
+ * Set the strategy used to create the trust chain context.
+ *
+ * @param strategy creation strategy
+ */
+ public void setTrustChainContextCreationStrategy(
+ @Nonnull final Function<ProfileRequestContext, RelyingPartyTrustChainContext> strategy) {
+ checkSetterPreconditions();
+ trustChainContextCreationStrategy =
+ Constraint.isNotNull(strategy, "TrustChainContextCreationStrategy cannot be null");
+ }
+
+ /**
+ * Get the strategy used to create the trust chain context.
+ *
+ * @return creation strategy
+ */
+ @Nonnull
+ public Function<ProfileRequestContext, RelyingPartyTrustChainContext> getTrustChainContextCreationStrategy() {
+ checkComponentActive();
+ return trustChainContextCreationStrategy;
+ }
+
+ /**
+ * Set the strategy used to get combined entity metadata from trust chain.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setMetadataLookupStrategy(
+ @Nonnull final Function<List<EntityStatement<?>>,Map<String,Map<String,Object>>> strategy) {
+ checkSetterPreconditions();
+ metadataLookupStrategy =
+ Constraint.isNotNull(strategy, "MetadataLookupStrategy cannot be null");
+ }
+
+ /**
+ * Get the strategy used to get combined entity metadata from trust chain.
+ *
+ * @return lookup strategy
+ */
+ @NonnullAfterInit
+ public Function<List<EntityStatement<?>>,Map<String,Map<String,Object>>> getMetadataLookupStrategy() {
+ checkComponentActive();
+ return metadataLookupStrategy;
+ }
+
+ /**
+ * Set the strategy used to merge metadata policies in trust chain for specific entity type.
+ *
+ * @param strategy merging strategy
+ */
+ public void setMetadataPolicyMergingStrategy(@Nonnull final
+ BiFunction<List<EntityStatement<?>>,String,Map<String, MetadataPolicy>> strategy) {
+ checkSetterPreconditions();
+ metadataPolicyMergingStrategy =
+ Constraint.isNotNull(strategy, "MetadataPolicyMergingStrategy cannot be null");
+ }
+
+ /**
+ * Get the strategy used to merge metadata policies in trust chain for specific entity type.
+ *
+ * @return merging strategy
+ */
+ @Nonnull
+ public BiFunction<List<EntityStatement<?>>,String,Map<String, MetadataPolicy>> getMetadataPolicyMergingStrategy() {
+ checkComponentActive();
+ assert metadataPolicyMergingStrategy != null;
+ return metadataPolicyMergingStrategy;
+ }
+
+ /**
+ * Set the enforcer function for applying metadata policy for an item.
+ *
+ * @param enforcer policy enforcer
+ */
+ public void setMetadataPolicyEnforcer(
+ @Nonnull final BiFunction<Object, MetadataPolicy, Pair<Object, Boolean>> enforcer) {
+ checkSetterPreconditions();
+ metadataPolicyEnforcer = Constraint.isNotNull(enforcer, "Metadata policy enforcer cannot be null");
+ }
+
+ /**
+ * Get the enforcer function for applying metadata policy for an item.
+ *
+ * @return policy enforcer
+ */
+ @Nonnull public BiFunction<Object, MetadataPolicy, Pair<Object, Boolean>> getMetadataPolicyEnforcer() {
+ checkComponentActive();
+ assert metadataPolicyEnforcer != null;
+ return metadataPolicyEnforcer;
+ }
+
+ /**
+ * Set the list of claim names who are transformed from a space-separated String into a List.
+ *
+ * @param list list of claim names
+ */
+ public void setArraysAsSpaceSeparatedList(@Nonnull final List<String> list) {
+ checkSetterPreconditions();
+ arraysAsSpaceSeparatedList = Constraint.isNotNull(list, "ArraysAsSpaceSeparatedList cannot be null");
+ }
+
+ /**
+ * Get the list of claim names who are transformed from a space-separated String into a List.
+ *
+ * @return list of claim names
+ */
+ @Nonnull public List<String> getArraysAsSpaceSeparatedList() {
+ checkComponentActive();
+ return arraysAsSpaceSeparatedList;
+ }
+
+ /**
+ * Set the condition used to validate metadata for an entity type.
+ *
+ * @param condition validation condition
+ */
+ public void setMetadataValidationCondition(@Nonnull final BiPredicate<String,Map<String,Object>> condition) {
+ checkSetterPreconditions();
+ metadataValidationCondition = Constraint.isNotNull(condition, "MetadataValidationCondition cannot be null");
+ }
+
+ /**
+ * Set the map of supported federation policy constraints.
+ *
+ * @param constraints map of supported federation policy constraints.
+ */
+ public void setFederationPolicyConstraints(@Nonnull final Map<String, FederationPolicyConstraint> constraints) {
+ checkSetterPreconditions();
+ federationPolicyConstraints = Constraint.isNotNull(constraints, "Map of policy constraints cannot be null");
+ }
+
+ /**
+ * Get the map of supported federation policy constraints.
+ *
+ * @return map of supported federation policy constraints.
+ */
+ @Nonnull public Map<String, FederationPolicyConstraint> getFederationPolicyConstraints() {
+ checkComponentActive();
+ assert federationPolicyConstraints != null;
+ return federationPolicyConstraints;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+
+ if (metadataLookupStrategy == null) {
+ throw new ComponentInitializationException("MetadataLookupStrategy cannot be null");
+ }
+ if (metadataPolicyMergingStrategy == null) {
+ throw new ComponentInitializationException("MetadataPolicyMergingStrategy cannot be null");
+ }
+ if (metadataPolicyEnforcer == null) {
+ throw new ComponentInitializationException("MetadataPolicyEnforcer cannot be null");
+ }
+ if (metadataValidationCondition == null) {
+ throw new ComponentInitializationException("MetadataValidationCondition cannot be null");
+ }
+ if (federationPolicyConstraints == null) {
+ throw new ComponentInitializationException("Map of policy constraints cannot be null");
+ }
+ }
+
+ /**
+ * Populates the given policy compliant trust chains with the given trust chain if its metadata is policy compliant.
+ *
+ * @param candidate the trust chain to be evaluated
+ * @param policyCompliantChains the list of policy-compliant trust chains to be populated
+ * @return error event ID if metadata policy merging or enforcement failed, null otherwise
+ */
+ @Nullable protected String populatePolicyComplaintChains(@Nonnull final List<EntityStatement<?>> candidate,
+ @Nonnull @Live List<VerifiedTrustChain> policyCompliantChains) {
+ final Map<String,Map<String,Object>> candidateMetadata = getMetadataLookupStrategy().apply(candidate);
+ log.trace("{} Metadata resolved via lookup strategy: {}", getLogPrefix(), candidateMetadata);
+ if (candidateMetadata != null) {
+ final List<EntityStatement<?>> chain = new ArrayList<>(candidate);
+ final EntityConfiguration updatedLeaf = updateEntityConfiguration(candidate.get(0), candidateMetadata);
+ if (updatedLeaf == null) {
+ log.error("Could not update the leaf entity configuration for {}", candidate.get(0).getSubject());
+ return EventIds.INVALID_MSG_CTX;
+ }
+ chain.set(0, updatedLeaf);
+ if (!FederationPolicyConstraintHelper.verifyPolicyConstraints(
+ chain, getFederationPolicyConstraints())) {
+ return OidFederationEventIds.INVALID_TRUST_CHAIN_AGAINST_CONSTRAINTS;
+ }
+ final Map<String,Map<String,Object>> metadata =
+ Optional.ofNullable(getMetadataLookupStrategy().apply(chain)).orElse(CollectionSupport.emptyMap());
+ log.trace("{} Constrained metadata resolved via lookup strategy: {}", getLogPrefix(), metadata);
+ final Map<String,Map<String,Object>> verifiedMetadata = new HashMap<>();
+ for (final String entityType : metadata.keySet()) {
+ final Map<String, MetadataPolicy> mergedPolicies;
+ try {
+ mergedPolicies = getMetadataPolicyMergingStrategy().apply(chain, entityType);
+ log.debug("{} Merged policy for type {} for chain {}", getLogPrefix(), entityType, mergedPolicies);
+ } catch (final ConstraintViolationException e) {
+ log.warn("{} Could not merge metadata policies", getLogPrefix(), e);
+ return OidFederationEventIds.INVALID_METADATA_POLICY;
+ }
+
+ final JSONObject requestMetadata = new JSONObject(metadata.get(entityType));
+ for (final String claim : mergedPolicies.keySet()) {
+ assert claim != null;
+ final MetadataPolicy policy = mergedPolicies.get(claim);
+ try {
+ final Object enforcedValue = enforceValue(claim, requestMetadata.get(claim), policy);
+ if (enforcedValue != null) {
+ requestMetadata.put(claim, enforcedValue);
+ } else {
+ log.debug("{} Enforcer returned null for entity {} claim {}", getLogPrefix(), entityType,
+ claim);
+ }
+ } catch (final ConstraintViolationException e) {
+ log.warn("{} The requested metadata is not compliant with the policy", getLogPrefix());
+ return OidFederationEventIds.INVALID_METADATA_AGAINST_POLICY;
+ }
+ }
+
+ log.debug("{} The requested metadata is compliant with the policy", getLogPrefix());
+ if (metadataValidationCondition.test(entityType, requestMetadata)) {
+ verifiedMetadata.put(entityType, requestMetadata);
+ log.debug("{} Policy-enforced metadata {}", getLogPrefix(), requestMetadata.toJSONString());
+ } else {
+ log.warn("{} Metadata validation failed for {} for entity type {}", getLogPrefix(),
+ chain.get(0).getSubject(), entityType);
+ }
+ }
+ policyCompliantChains.add(new VerifiedTrustChain(chain, new MetadataImpl(verifiedMetadata)));
+ log.debug("{} Policy-enforced metadata {}", getLogPrefix(), verifiedMetadata);
+ return null;
+ }
+ return EventIds.INVALID_MSG_CTX;
+ }
+
+ /**
+ * Enforces the given value with the given metadata policy.
+ *
+ * @param claim name of the claim to be enforced
+ * @param value value of the claim
+ * @param policy the metadata policy to be used for enforcing
+ * @return the enforced value
+ * @throws ConstraintViolationException if the operation was not successful
+ */
+ @Nullable protected Object enforceValue(@Nonnull final String claim, @Nullable final Object value,
+ @Nullable final MetadataPolicy policy) throws ConstraintViolationException {
+ log.debug("{} Claim {} set in policy included in the request: {}", getLogPrefix(), claim,
+ value == null);
+ final Object enforcerInput = FederationMetadataPolicyHelper.transformSpaceSeparatedStringIntoList(
+ arraysAsSpaceSeparatedList, claim, value);
+
+ final Pair<Object,Boolean> mergeResult = getMetadataPolicyEnforcer().apply(enforcerInput, policy);
+ final Boolean enforcerResult = mergeResult != null ? mergeResult.getSecond() : null;
+ if (enforcerResult == null || !enforcerResult.booleanValue()) {
+ throw new ConstraintViolationException("Metadata claim " + claim + " is not compliant with the policy");
+ }
+ log.trace("{} Validation result is OK for claim {}", getLogPrefix(), claim);
+ return Optional.ofNullable(mergeResult)
+ .map(pair -> pair.getFirst())
+ .map(result -> FederationMetadataPolicyHelper.transformListIntoSpaceSeparatedString(
+ arraysAsSpaceSeparatedList,claim, result))
+ .orElse(null);
+ }
+
+ /**
+ * Updates the given entity configuration to contain given metadata.
+ *
+ * @param entityStatement entity configuration source
+ * @param metadata metadata to be included in the updated entity configuration
+ * @return updated entity configuration or null if the source was unexpected
+ */
+ @Nullable protected EntityConfiguration updateEntityConfiguration(
+ @Nullable final EntityStatement<?> entityStatement,
+ @Nonnull final Map<String, Map<String, Object>> metadata) {
+ if (entityStatement instanceof EntityConfiguration entityConfiguration) {
+ final EntityConfigurationPayloadImpl payload =
+ new EntityConfigurationPayloadImpl(entityConfiguration.getParsedPayload());
+ payload.setMetadata(new MetadataImpl(metadata));
+ return new EntityConfigurationImpl(entityStatement.getJwt(), payload);
+ }
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/CallResolveEntityApi.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/CallResolveEntityApi.java
new file mode 100644
index 0000000..fd49db6
--- /dev/null
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/CallResolveEntityApi.java
@@ -0,0 +1,534 @@
+/*
+ * 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.net.MalformedURLException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.text.ParseException;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
+import org.slf4j.Logger;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.nimbusds.jwt.SignedJWT;
+
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.oidc.metadata.cache.MetadataCache;
+import net.shibboleth.oidc.metadata.cache.MetadataCacheException;
+import net.shibboleth.oidfed.metadata.EntityConfiguration;
+import net.shibboleth.oidfed.metadata.EntityStatement;
+import net.shibboleth.oidfed.metadata.ResolveEntityResponse;
+import net.shibboleth.oidfed.metadata.cache.FederationEndpointEntityStatementCriterion;
+import net.shibboleth.oidfed.metadata.cache.ResponseContainerExpirationCriterion;
+import net.shibboleth.oidfed.metadata.cache.SubjectEntityIDCriterion;
+import net.shibboleth.oidfed.metadata.cache.SubjectEntityStatementCriterion;
+import net.shibboleth.oidfed.metadata.cache.configuration.EntityConfigurationContainer;
+import net.shibboleth.oidfed.metadata.cache.resolver.ResolveEntityCacheContainerIdentifier;
+import net.shibboleth.oidfed.metadata.cache.resolver.ResolveEntityCacheIdentifierCriterion;
+import net.shibboleth.oidfed.metadata.cache.resolver.ResolveEntityResponseContainer;
+import net.shibboleth.oidfed.metadata.payload.ResolveEntityResponsePayload;
+import net.shibboleth.oidfed.metadata.payload.claim.Metadata;
+import net.shibboleth.oidfed.metadata.util.EntityStatementHelper;
+import net.shibboleth.oidfed.profile.TrustedRemoteResolverEntity;
+import net.shibboleth.oidfed.profile.context.RelyingPartyTrustChainContext;
+import net.shibboleth.oidfed.profile.context.VerifiedTrustChain;
+import net.shibboleth.oidfed.profile.navigate.DefaultTrustChainIDsLookupStrategy;
+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.logic.FunctionSupport;
+import net.shibboleth.shared.logic.PredicateSupport;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.primitive.NonnullSupplier;
+import net.shibboleth.shared.resolver.CriteriaSet;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.action.ActionSupport;
+
+/**
+ * Resolves trust chain, policy-enforced metadata and trust marks via configurable
+ * {@link #resolveEntityTrustChainMetadataCache}. The data is populated to the {@link RelyingPartyTrustChainContext}.
+ *
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#INVALID_MSG_CTX}
+ */
+public class CallResolveEntityApi extends AbstractProfileAction {
+
+ /** Class logger. */
+ @Nonnull private Logger log = LoggerFactory.getLogger(CallResolveEntityApi.class);
+
+ /** Strategy used to create the trust chain context. */
+ @Nonnull private Function<ProfileRequestContext, RelyingPartyTrustChainContext> trustChainContextCreationStrategy;
+
+ /** Cache used to fetch the issuer entity configuration from. */
+ @NonnullAfterInit private MetadataCache<EntityConfigurationContainer> entityConfigurationCache;
+
+ /** Cache containing responses from resolve entity APIs. */
+ @NonnullAfterInit private MetadataCache<ResolveEntityResponseContainer> resolveEntityTrustChainMetadataCache;
+
+ /** Strategy used to obtain the entity id value of the request. */
+ @NonnullAfterInit private Function<ProfileRequestContext, String> entityIDLookupStrategy;
+
+ /** Strategy used to fetch the pre-selected trust chain entity IDs. */
+ @NonnullAfterInit private Function<ProfileRequestContext, List<String>> preSelectedTrustChainIdsLookupStrategy;
+
+ /** Strategy used to get entity IDs from a trust chain. */
+ @Nonnull private Function<List<EntityStatement<?>>, List<String>> trustChainIDsLookupStrategy;
+
+ /** Strategy used to fetch entity configuration delivered to the trust chain cache. */
+ @Nonnull private Function<ProfileRequestContext, EntityConfiguration> entityConfigurationLookupStrategy;
+
+ /** Condition to require entity configuration via {@link #entityConfigurationLookupStrategy}. */
+ @Nonnull private Predicate<ProfileRequestContext> requireEntityConfigurationCondition;
+
+ /** Strategy used to get map of trusted entities for resolve entity APIs. */
+ @NonnullAfterInit
+ private Function<ProfileRequestContext, List<TrustedRemoteResolverEntity>> trustedEntitiesLookupStrategy;
+
+ /** Strategy used to fetch the entity types used in the resolve entity request. */
+ @Nonnull private Function<ProfileRequestContext, List<String>> entityTypesLookupStrategy;
+
+ /** Lookup function to supply cached response lifetime. */
+ @Nonnull private Function<ProfileRequestContext,Duration> cachedResponseLifetimeLookupStrategy;
+
+ /** JSON object mapper used for decoding JSON into Map. */
+ @NonnullAfterInit private ObjectMapper objectMapper;
+
+ /** Entity id. */
+ @NonnullBeforeExec private String entityId;
+
+ /** Trusted entities to use. */
+ @NonnullBeforeExec private List<TrustedRemoteResolverEntity> trustedEntities;
+
+ /**
+ * Constructor.
+ */
+ public CallResolveEntityApi() {
+ final Function<ProfileRequestContext, RelyingPartyTrustChainContext> tccs =
+ new ChildContextLookup<>(RelyingPartyTrustChainContext.class, true).compose(
+ new InboundMessageContextLookup());
+ assert tccs != null;
+ trustChainContextCreationStrategy = tccs;
+ trustChainIDsLookupStrategy = new DefaultTrustChainIDsLookupStrategy();
+ entityConfigurationLookupStrategy = FunctionSupport.constant(null);
+ requireEntityConfigurationCondition = PredicateSupport.alwaysFalse();
+ entityTypesLookupStrategy = FunctionSupport.constant(List.of("openid_relying_party"));
+ cachedResponseLifetimeLookupStrategy = FunctionSupport.constant(Duration.ofMinutes(5));
+ }
+
+ /**
+ * Set the strategy used to create the trust chain context.
+ *
+ * @param strategy creation strategy
+ */
+ public void setTrustChainContextCreationStrategy(
+ @Nonnull final Function<ProfileRequestContext, RelyingPartyTrustChainContext> strategy) {
+ checkSetterPreconditions();
+ trustChainContextCreationStrategy =
+ Constraint.isNotNull(strategy, "TrustChainContextCreationStrategy cannot be null");
+ }
+
+ /**
+ * Set the cache used to fetch the issuer entity configuration from.
+ *
+ * @param cache cache used to fetch the issuer entity configuration from
+ */
+ public void setEntityConfigurationCache(@Nonnull final MetadataCache<EntityConfigurationContainer> cache) {
+ checkSetterPreconditions();
+ entityConfigurationCache = Constraint.isNotNull(cache, "Entity Configuration cache cannot be null");
+ }
+
+ public void setResolveEntityTrustChainMetadataCache(
+ @Nonnull final MetadataCache<ResolveEntityResponseContainer> cache) {
+ checkSetterPreconditions();
+ resolveEntityTrustChainMetadataCache =
+ Constraint.isNotNull(cache, "ResolveEntityTrustChainMetadataCache cannot be null");
+ }
+
+ /**
+ * Set the strategy used to locate the entity id of the request.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setEntityIDLookupStrategy(@Nonnull final Function<ProfileRequestContext, String> strategy) {
+ checkSetterPreconditions();
+ entityIDLookupStrategy =
+ Constraint.isNotNull(strategy, "EntityIDLookupStrategy cannot be null");
+ }
+
+ /**
+ * Set the strategy used to fetch the pre-selected trust chain entity IDs.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setPreSelectedTrustChainIdsLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext, List<String>> strategy) {
+ checkSetterPreconditions();
+ preSelectedTrustChainIdsLookupStrategy = Constraint.isNotNull(strategy,
+ "PreSelectedTrustChainIdsLookupStrategy cannot be null");
+ }
+
+ /**
+ * Set the strategy used to get entity IDs from a trust chain.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setTrustChainIDsLookupStrategy(@Nonnull final Function<List<EntityStatement<?>>,
+ List<String>> strategy) {
+ checkSetterPreconditions();
+ trustChainIDsLookupStrategy = Constraint.isNotNull(strategy, "TrustChainIDsLookupStrategy cannot be null");
+ }
+
+ /**
+ * Set the strategy used to fetch entity configuration delivered to the trust chain cache.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setEntityConfigurationLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext, EntityConfiguration> strategy) {
+ checkSetterPreconditions();
+ entityConfigurationLookupStrategy = Constraint.isNotNull(strategy,
+ "EntityConfigurationLookupStrategy cannot be null");
+ }
+
+ /**
+ * Set the condition to require entity configuration via {@link #entityConfigurationLookupStrategy}.
+ * @param predicate condition
+ */
+ public void setRequireEntityConfigurationCondition(@Nonnull final Predicate<ProfileRequestContext> predicate) {
+ checkSetterPreconditions();
+ requireEntityConfigurationCondition =
+ Constraint.isNotNull(predicate, "RequireEntityConfigurationCondition cannot be null");
+ }
+
+ /**
+ * Set the strategy used to get map of trusted entities for resolve entity APIs.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setTrustedEntitiesLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext, List<TrustedRemoteResolverEntity>> strategy) {
+ checkSetterPreconditions();
+ trustedEntitiesLookupStrategy = Constraint.isNotNull(strategy, "TrustedEntitiesLookupStrategy cannot be null");
+ }
+
+ /**
+ * Set the strategy used to fetch the entity types used in the resolve entity request.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setEntityTypesLookupStrategy(@Nonnull final Function<ProfileRequestContext, List<String>> strategy) {
+ checkSetterPreconditions();
+ entityTypesLookupStrategy = Constraint.isNotNull(strategy, "EntityTypesLookupStrategy cannot be null");
+ }
+
+ /**
+ * Set the JSON {@link ObjectMapper} used for decoding JSON into Map.
+ *
+ * @param mapper object mapper
+ */
+ public void setObjectMapper(@Nonnull final ObjectMapper mapper) {
+ checkSetterPreconditions();
+
+ objectMapper = Constraint.isNotNull(mapper, "Object mapper cannot be null");
+ }
+
+ /**
+ * Set a lookup strategy for the cached response lifetime.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setCachedResponseLifetimeLookupStrategy(
+ @Nullable final Function<ProfileRequestContext,Duration> strategy) {
+ cachedResponseLifetimeLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+
+ if (entityConfigurationCache == null) {
+ throw new ComponentInitializationException("Entity Configuration cache cannot be null");
+ }
+ if (resolveEntityTrustChainMetadataCache == null) {
+ throw new ComponentInitializationException("ResolveEntityTrustChainMetadataCache cannot be null");
+ }
+ if (entityIDLookupStrategy == null) {
+ throw new ComponentInitializationException("EntityIDLookupStrategy cannot be null");
+ }
+ if (trustedEntitiesLookupStrategy == null) {
+ throw new ComponentInitializationException("TrustedEntitiesLookupStrategy cannot be null");
+ }
+ if (objectMapper == null) {
+ throw new ComponentInitializationException("ObjectMapper cannot be null");
+ }
+ if (preSelectedTrustChainIdsLookupStrategy == null) {
+ throw new ComponentInitializationException("PreSelectedTrustChainIdsLookupStrategy cannot be null");
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+ if (!super.doPreExecute(profileRequestContext)) {
+ return false;
+ }
+
+ entityId = entityIDLookupStrategy.apply(profileRequestContext);
+ if (entityId == null) {
+ log.error("{} Unable to obtain entity ID", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return false;
+ }
+ try {
+ new URL(entityId).toURI();
+ } catch (final URISyntaxException | MalformedURLException e) {
+ log.debug("{} The entity ID {} is not a valid URL, nothing to do", getLogPrefix(), entityId);
+ return false;
+ }
+
+ trustedEntities = trustedEntitiesLookupStrategy.apply(profileRequestContext);
+ if (trustedEntities == null || trustedEntities.isEmpty()) {
+ log.warn("{} No trusted entities resolved for {} nothing to do", getLogPrefix(), entityId);
+ ActionSupport.buildEvent(profileRequestContext, "CheckFallback");
+ return false;
+ }
+
+ return true;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+ log.debug("{} Resolving trust chain via resolve entity API for {}", getLogPrefix(), entityId);
+ final String nonNullEntityId = entityId;
+ assert nonNullEntityId != null;
+ final CriteriaSet baseCriteriaSet = new CriteriaSet(new SubjectEntityIDCriterion(nonNullEntityId));
+ final EntityConfiguration entityConfiguration = entityConfigurationLookupStrategy.apply(profileRequestContext);
+ if (entityConfiguration != null) {
+ log.debug("{} Entity configuration resolved and included to the criteria set", getLogPrefix());
+ baseCriteriaSet.add(new SubjectEntityStatementCriterion(entityConfiguration));
+ } else if (requireEntityConfigurationCondition.test(profileRequestContext)) {
+ log.error("{} Mandatory entity configuration could not be resolved", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_CREDENTIALS);
+ return;
+ }
+ final Duration cachedLifetime = cachedResponseLifetimeLookupStrategy.apply(profileRequestContext);
+ if (cachedLifetime == null) {
+ log.warn("{} Could not resolve lifetime for success responses", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return;
+ }
+ final Instant expirationTime = Instant.now().plus(cachedLifetime);
+ assert expirationTime != null;
+ baseCriteriaSet.add(new ResponseContainerExpirationCriterion(expirationTime));
+
+ final List<String> preSelectedChain =
+ Optional.ofNullable(preSelectedTrustChainIdsLookupStrategy.apply(profileRequestContext))
+ .orElse(CollectionSupport.emptyList());
+
+ final RelyingPartyTrustChainContext trustChainContext =
+ trustChainContextCreationStrategy.apply(profileRequestContext);
+
+ for (final TrustedRemoteResolverEntity trustedEntity : trustedEntities) {
+ if (trustedEntity == null) {
+ log.warn("{} Ignoring null trusted entity entry", getLogPrefix());
+ continue;
+ }
+ final List<TrustedRemoteResolverEntity> alreadyAttemptedEntities =
+ Optional.ofNullable(trustChainContext.getAttemptedTrustedRemoteResolverEntities())
+ .map(list -> new ArrayList<>(list))
+ .orElseGet(NonnullSupplier.of(new ArrayList<>()));
+ if (alreadyAttemptedEntities.contains(trustedEntity)) {
+ log.debug("{} Trusted entity {} has already been attempted", getLogPrefix(), trustedEntity);
+ continue;
+ } else {
+ alreadyAttemptedEntities.add(trustedEntity);
+ trustChainContext.setAttemptedTrustedRemoteResolverEntities(
+ CollectionSupport.copyToList(alreadyAttemptedEntities));
+ }
+ final CriteriaSet criteria = new CriteriaSet(new SubjectEntityIDCriterion(trustedEntity.getEntityId()),
+ new ResponseContainerExpirationCriterion(expirationTime));
+
+ final List<EntityConfigurationContainer> configurationCacheResult;
+ try {
+ configurationCacheResult = entityConfigurationCache.get(criteria);
+ } catch (final MetadataCacheException e) {
+ log.warn("{} Could not resolve entity configuration for {}", getLogPrefix(), trustedEntity, e);
+ continue;
+ }
+ if (configurationCacheResult.isEmpty()) {
+ log.warn("{} Could not resolve entity configuration for {}", getLogPrefix(), trustedEntity);
+ continue;
+ }
+ final EntityConfiguration configuration =
+ Optional.ofNullable(configurationCacheResult.get(0).getStatement())
+ .orElse(null);
+ if (configuration == null) {
+ log.warn("{} Could not resolve entity configuration for {}", getLogPrefix(), trustedEntity);
+ continue;
+ }
+ final URI uri = Optional.of(configuration.getParsedPayload().getMetadata())
+ .map(metadata -> metadata.getFederationEntityMetadata())
+ .map(entityMetadata -> entityMetadata.get("federation_resolve_endpoint"))
+ .map(endpoint -> URI.create((String) endpoint))
+ .orElse(null);
+ if (uri == null) {
+ log.warn("{} Could not fetch federation resolve endpoint for {}", getLogPrefix(), trustedEntity);
+ continue;
+ }
+ final String uriValue = uri.toString();
+ assert uriValue != null;
+ final ResolveEntityCacheContainerIdentifier entityRequest =
+ new ResolveEntityCacheContainerIdentifier(
+ uriValue, nonNullEntityId, CollectionSupport.copyToList(trustedEntity.getTrustAnchors()),
+ entityTypesLookupStrategy.apply(profileRequestContext));
+ final CriteriaSet criteriaSet = new CriteriaSet();
+ baseCriteriaSet.forEach(c -> criteriaSet.add(c));
+ criteriaSet.add(new ResolveEntityCacheIdentifierCriterion(entityRequest));
+ criteriaSet.add(new FederationEndpointEntityStatementCriterion(configuration));
+ final List<ResolveEntityResponseContainer> cacheResult;
+ try {
+ cacheResult = resolveEntityTrustChainMetadataCache.get(criteriaSet);
+ } catch (final MetadataCacheException e) {
+ log.warn("{} Could not resolve entity for {} from {}", getLogPrefix(), entityId, trustedEntity, e);
+ continue;
+ }
+ if (cacheResult.isEmpty()) {
+ log.debug("{} No data resolved for {} from {}", getLogPrefix(), entityId, trustedEntity);
+ continue;
+ }
+ final ResolveEntityResponse statement = cacheResult.get(0).getStatement();
+ if (statement != null) {
+ final ResolveEntityResponsePayload payload = statement.getParsedPayload();
+ final List<String> rawTrustChain = payload.getTrustChain();
+ final Metadata metadata = payload.getMetadata();
+ final List<Map<String,String>> rawTrustMarks = payload.getTrustMarks();
+ if (rawTrustChain == null || rawTrustChain.isEmpty() || metadata == null) {
+ log.warn("{} Could not parse mandatory parameters from the response from {}", getLogPrefix(),
+ trustedEntity);
+ continue;
+ }
+ assert objectMapper != null;
+ final List<EntityStatement<?>> chain =
+ EntityStatementHelper.deserializeTrustChain(rawTrustChain, objectMapper);
+ if (chain == null) {
+ log.warn("Could not parse the trust chain into list of entity statements");
+ continue;
+ }
+ if (!preSelectedChain.isEmpty() && !preSelectedChain.equals(trustChainIDsLookupStrategy.apply(chain))) {
+ log.debug("{} Ignored resolved trust chain that doesn't match with preselected chain",
+ getLogPrefix());
+ continue;
+ }
+ final List<VerifiedTrustChain> policyCompliantChains = new ArrayList<>();
+ policyCompliantChains.add(new VerifiedTrustChain(chain, metadata));
+ trustChainContext.setPolicyCompliantTrustChains(policyCompliantChains);
+ log.debug("{} Populated policy compliant trust chains with {}", getLogPrefix(), policyCompliantChains);
+
+ final List<SignedJWT> trustMarks = rawTrustMarks == null ? null : rawTrustMarks
+ .stream()
+ .filter(Map.class::isInstance)
+ .map(Map.class::cast)
+ .map(map -> parseTrustMark(map.get("trust_mark")))
+ .filter(Objects::nonNull)
+ .toList();
+ if (trustMarks != null) {
+ final Map<String, List<SignedJWT>> trustMarksByEntity = new HashMap<>();
+ for (final String entity : chain.stream().map(entity -> entity.getSubject()).toList()) {
+ final List<SignedJWT> trustMarksForEntity = trustMarks.stream()
+ .filter(jwt -> {
+ try {
+ return entity.equals(jwt.getJWTClaimsSet().getSubject());
+ } catch (final ParseException e1) {
+ return false;
+ }
+ }).toList();
+ if (!trustMarksForEntity.isEmpty()) {
+ trustMarksByEntity.put(entity, trustMarksForEntity);
+ }
+ }
+ if (!trustMarksByEntity.isEmpty()) {
+ trustChainContext.setVerifiedTrustMarks(trustMarksByEntity);
+ final Map<String, List<String>> trustMarkIds = trustMarksByEntity.entrySet().stream()
+ .collect(Collectors.toMap(entry -> entry.getKey(), entry -> entry.getValue().stream()
+ .map(jwt -> {
+ try {
+ return jwt.getJWTClaimsSet().getStringClaim("trust_mark_type");
+ } catch (final ParseException e1) {
+ return null;
+ }
+ })
+ .filter(Objects::nonNull)
+ .toList()));
+ log.debug("{} The following trust marks are included: {}", getLogPrefix(), trustMarkIds);
+ trustChainContext.setVerifiedTrustMarkIds(trustMarkIds);
+
+ }
+ } else {
+ log.debug("{} No trust marks included in the response from {}", getLogPrefix(), trustedEntity);
+ }
+ return;
+ } else {
+ log.debug("{} The cached response from {} was not containing statement", getLogPrefix(),
+ trustedEntity);
+ continue;
+ }
+ }
+ log.debug("{} No previously not attempted policy-compliant trust chains resolved", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, "CheckFallback");
+
+ }
+
+ /**
+ * Parses the trust mark.
+ *
+ * @param trustMark trust mark to be verified, expected to be parseable from string
+ * @return trust mark JWT if valid, null otherwise
+ */
+ @Nullable private SignedJWT parseTrustMark(@Nullable final Object trustMark) {
+ if (trustMark instanceof String string) {
+ try {
+ return SignedJWT.parse(string);
+ } catch (final ParseException e) {
+ log.error("{} Could not parse the trust mark into a JWT", getLogPrefix(), e);
+ }
+ }
+ return null;
+ }
+
+}
\ No newline at end of file
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/ResolveTrustChains.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/ResolveTrustChains.java
new file mode 100644
index 0000000..6868c38
--- /dev/null
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/ResolveTrustChains.java
@@ -0,0 +1,263 @@
+/*
+ * 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.net.MalformedURLException;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.oidc.metadata.cache.MetadataCache;
+import net.shibboleth.oidc.metadata.cache.MetadataCacheException;
+import net.shibboleth.oidfed.metadata.EntityConfiguration;
+import net.shibboleth.oidfed.metadata.EntityStatement;
+import net.shibboleth.oidfed.metadata.cache.SubjectEntityIDCriterion;
+import net.shibboleth.oidfed.metadata.cache.SubjectEntityStatementCriterion;
+import net.shibboleth.oidfed.metadata.cache.trustchain.TrustChainsContainer;
+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.DefaultTrustChainIDsLookupStrategy;
+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.logic.FunctionSupport;
+import net.shibboleth.shared.logic.PredicateSupport;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.resolver.CriteriaSet;
+
+import org.opensaml.profile.action.ActionSupport;
+
+/**
+ * Resolves metadata policy-compliant trust chains from the configurable trust chain cache, metadata policy merging
+ * strategy and enforcer. The data is populated to the {@link RelyingPartyTrustChainContext}.
+ *
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#INVALID_MSG_CTX}
+ */
+public class ResolveTrustChains extends AbstractTrustChainResolutionAction {
+
+ /** Class logger. */
+ @Nonnull private Logger log = LoggerFactory.getLogger(ResolveTrustChains.class);
+
+ /** Metadata cache for trust chains. */
+ @NonnullAfterInit private MetadataCache<TrustChainsContainer> trustChainCache;
+
+ /** Strategy used to obtain the entity id value of the request. */
+ @NonnullAfterInit private Function<ProfileRequestContext, String> entityIDLookupStrategy;
+
+ /** Strategy used to fetch the pre-selected trust chain entity IDs. */
+ @NonnullAfterInit private Function<ProfileRequestContext, List<String>> preSelectedTrustChainIdsLookupStrategy;
+
+ /** Strategy used to get entity IDs from a trust chain. */
+ @Nonnull private Function<List<EntityStatement<?>>, List<String>> trustChainIDsLookupStrategy;
+
+ /** Strategy used to fetch entity configuration delivered to the trust chain cache. */
+ @Nonnull private Function<ProfileRequestContext, EntityConfiguration> entityConfigurationLookupStrategy;
+
+ /** Condition to require entity configuration via {@link #entityConfigurationLookupStrategy}. */
+ @Nonnull private Predicate<ProfileRequestContext> requireEntityConfigurationCondition;
+
+ /** Entity id. */
+ @NonnullBeforeExec private String entityId;
+
+ /**
+ * Constructor.
+ */
+ public ResolveTrustChains() {
+ super();
+ trustChainIDsLookupStrategy = new DefaultTrustChainIDsLookupStrategy();
+ entityConfigurationLookupStrategy = FunctionSupport.constant(null);
+ requireEntityConfigurationCondition = PredicateSupport.alwaysFalse();
+ }
+
+ /**
+ * Set the metadata cache for trust chains.
+ *
+ * @param cache metadata cache
+ */
+ public void setTrustChainCache(@Nonnull final MetadataCache<TrustChainsContainer> cache) {
+ checkSetterPreconditions();
+ trustChainCache = Constraint.isNotNull(cache, "TrustChainCache cannot be null");
+ }
+
+ /**
+ * Set the strategy used to locate the entity id of the request.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setEntityIDLookupStrategy(@Nonnull final Function<ProfileRequestContext, String> strategy) {
+ checkSetterPreconditions();
+ entityIDLookupStrategy =
+ Constraint.isNotNull(strategy, "EntityIDLookupStrategy cannot be null");
+ }
+
+ /**
+ * Set the strategy used to fetch the pre-selected trust chain entity IDs.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setPreSelectedTrustChainIdsLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext, List<String>> strategy) {
+ checkSetterPreconditions();
+ preSelectedTrustChainIdsLookupStrategy = Constraint.isNotNull(strategy,
+ "PreSelectedTrustChainIdsLookupStrategy cannot be null");
+ }
+
+ /**
+ * Set the strategy used to get entity IDs from a trust chain.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setTrustChainIDsLookupStrategy(@Nonnull final Function<List<EntityStatement<?>>,
+ List<String>> strategy) {
+ checkSetterPreconditions();
+ trustChainIDsLookupStrategy = Constraint.isNotNull(strategy, "TrustChainIDsLookupStrategy cannot be null");
+ }
+
+ /**
+ * Set the strategy used to fetch entity configuration delivered to the trust chain cache.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setEntityConfigurationLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext, EntityConfiguration> strategy) {
+ checkSetterPreconditions();
+ entityConfigurationLookupStrategy = Constraint.isNotNull(strategy,
+ "EntityConfigurationLookupStrategy cannot be null");
+ }
+
+ /**
+ * Set the condition to require entity configuration via {@link #entityConfigurationLookupStrategy}.
+ * @param predicate condition
+ */
+ public void setRequireEntityConfigurationCondition(@Nonnull final Predicate<ProfileRequestContext> predicate) {
+ checkSetterPreconditions();
+ requireEntityConfigurationCondition =
+ Constraint.isNotNull(predicate, "RequireEntityConfigurationCondition cannot be null");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+
+ if (trustChainCache == null) {
+ throw new ComponentInitializationException("TrustChainCache cannot be null");
+ }
+ if (entityIDLookupStrategy == null) {
+ throw new ComponentInitializationException("EntityIDLookupStrategy cannot be null");
+ }
+ if (preSelectedTrustChainIdsLookupStrategy == null) {
+ throw new ComponentInitializationException("PreSelectedTrustChainIdsLookupStrategy cannot be null");
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+ if (!super.doPreExecute(profileRequestContext)) {
+ return false;
+ }
+
+ entityId = entityIDLookupStrategy.apply(profileRequestContext);
+ if (entityId == null) {
+ log.error("{} Unable to obtain entity ID", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return false;
+ }
+ try {
+ new URL(entityId).toURI();
+ } catch (final URISyntaxException | MalformedURLException e) {
+ log.debug("{} The entity ID {} is not a valid URL, nothing to do", getLogPrefix(), entityId);
+ return false;
+ }
+ return true;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+ log.debug("{} Resolving trust chain for {}", getLogPrefix(), entityId);
+ assert entityId != null;
+ final CriteriaSet criteriaSet = new CriteriaSet(new SubjectEntityIDCriterion(entityId));
+ final EntityConfiguration entityConfiguration = entityConfigurationLookupStrategy.apply(profileRequestContext);
+ if (entityConfiguration != null) {
+ log.debug("{} Entity configuration resolved and included to the criteria set", getLogPrefix());
+ criteriaSet.add(new SubjectEntityStatementCriterion(entityConfiguration));
+ } else if (requireEntityConfigurationCondition.test(profileRequestContext)) {
+ log.error("{} Mandatory entity configuration could not be resolved", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_CREDENTIALS);
+ return;
+ }
+ final List<TrustChainsContainer> cacheResult;
+ try {
+ cacheResult = trustChainCache.get(criteriaSet);
+ } catch (final MetadataCacheException e) {
+ log.warn("{} Could fetch trust chains for {}", getLogPrefix(), entityId);
+ ActionSupport.buildEvent(profileRequestContext, OidFederationEventIds.NO_TRUST_CHAINS_RESOLVED);
+ return;
+ }
+ if (cacheResult.isEmpty()) {
+ log.debug("{} No trust chains resolved for {}", getLogPrefix(), entityId);
+ ActionSupport.buildEvent(profileRequestContext, OidFederationEventIds.NO_TRUST_CHAINS_RESOLVED);
+ return;
+ }
+ final List<String> preSelectedChain =
+ Optional.ofNullable(preSelectedTrustChainIdsLookupStrategy.apply(profileRequestContext))
+ .orElse(CollectionSupport.emptyList());
+
+ final RelyingPartyTrustChainContext trustChainContext =
+ getTrustChainContextCreationStrategy().apply(profileRequestContext);
+ trustChainContext.setResolvedTrustChains(cacheResult.get(0).getTrustChains());
+ final List<VerifiedTrustChain> policyCompliantChains = new ArrayList<>();
+
+ String errorEventId = null;
+ for (final List<EntityStatement<?>> chain : cacheResult.get(0).getTrustChains()) {
+ assert chain != null;
+ if (!preSelectedChain.isEmpty() && !preSelectedChain.equals(trustChainIDsLookupStrategy.apply(chain))) {
+ log.debug("{} Ignored resolved trust chain that doesn't match with preselected chain", getLogPrefix());
+ continue;
+ }
+ errorEventId = populatePolicyComplaintChains(chain, policyCompliantChains);
+ }
+
+ if (policyCompliantChains.isEmpty()) {
+ if (errorEventId == null) {
+ ActionSupport.buildEvent(profileRequestContext, OidFederationEventIds.NO_TRUST_CHAINS_RESOLVED);
+ } else {
+ ActionSupport.buildEvent(profileRequestContext, errorEventId);
+ }
+ return;
+ }
+ log.debug("{} Setting the policy compliant trust chains into the context: {}", getLogPrefix(),
+ policyCompliantChains);
+ trustChainContext.setPolicyCompliantTrustChains(policyCompliantChains);
+ }
+
+}
\ No newline at end of file
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/navigate/DefaultCombinedMetadataFromTrustChainLookupStrategy.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/navigate/DefaultCombinedMetadataFromTrustChainLookupStrategy.java
new file mode 100644
index 0000000..381e5ff
--- /dev/null
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/navigate/DefaultCombinedMetadataFromTrustChainLookupStrategy.java
@@ -0,0 +1,86 @@
+/*
+ * 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.HashMap;
+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.slf4j.Logger;
+
+import net.shibboleth.oidfed.metadata.EntityStatement;
+import net.shibboleth.shared.annotation.constraint.ThreadSafeAfterInit;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * Default strategy to combine metadata claim contents from the trust chain by exploiting both entity configuration
+ * and subordinate statement issued by the immediate superior.
+ */
+ at ThreadSafeAfterInit
+public class DefaultCombinedMetadataFromTrustChainLookupStrategy extends AbstractIdentifiableInitializableComponent
+ implements Function<List<EntityStatement<?>>,Map<String,Map<String,Object>>> {
+
+ /** Class logger. */
+ @Nonnull private Logger log = LoggerFactory.getLogger(DefaultCombinedMetadataFromTrustChainLookupStrategy.class);
+
+ /** {@inheritDoc} */
+ @Override @Nullable
+ public Map<String,Map<String,Object>> apply(@Nullable final List<EntityStatement<?>> chain) {
+ if (chain == null || chain.size() < 3) {
+ log.warn("Unexpected trust chain input: {}", chain == null ? null : "size = " + chain.size());
+ return null;
+ }
+ final Map<String,Map<String,Object>> configurationMetadata =
+ Optional.ofNullable(chain.get(0).getParsedPayload().getMetadata())
+ .map(metadata -> metadata.getAllClaims()).orElse(null);
+ final Map<String,Map<String,Object>> subordinateMetadata =
+ Optional.ofNullable(chain.get(1).getParsedPayload().getMetadata())
+ .map(metadata -> metadata.getAllClaims()).orElse(null);
+
+ if (configurationMetadata == null || configurationMetadata.isEmpty()) {
+ log.debug("Entity configuration for {} doesn't contain metadata", chain.get(0).getSubject());
+ return null;
+ }
+
+ final Map<String,Map<String,Object>> result = new HashMap<>();
+ for (final String entityType : configurationMetadata.keySet()) {
+ final Map<String, Object> configurationClaims = configurationMetadata.get(entityType);
+ final Map<String, Object> subordinateClaims = new HashMap<>(Optional.ofNullable(subordinateMetadata)
+ .map(metadata -> metadata.get(entityType))
+ .orElse(CollectionSupport.emptyMap()));
+
+ for (final String configurationClaim : configurationClaims.keySet()) {
+ if (!subordinateClaims.containsKey(configurationClaim)) {
+ log.trace("Including metadata claim {} from the entity configuration", configurationClaim);
+ subordinateClaims.put(configurationClaim, configurationClaims.get(configurationClaim));
+ } else {
+ log.trace("Keeping metadata claim {} from the subordinate configuration", configurationClaim);
+ }
+ }
+
+ result.put(entityType, subordinateClaims);
+ }
+
+ return result;
+ }
+
+}
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/navigate/DefaultMetadataValidationCondition.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/navigate/DefaultMetadataValidationCondition.java
new file mode 100644
index 0000000..da8c6cf
--- /dev/null
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/navigate/DefaultMetadataValidationCondition.java
@@ -0,0 +1,82 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.oidfed.profile.navigate;
+
+import java.util.Map;
+import java.util.function.BiPredicate;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.slf4j.Logger;
+
+import com.nimbusds.oauth2.sdk.ParseException;
+import com.nimbusds.oauth2.sdk.as.AuthorizationServerMetadata;
+import com.nimbusds.oauth2.sdk.client.ClientMetadata;
+import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientMetadata;
+
+import net.minidev.json.JSONObject;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.primitive.StringSupport;
+
+/**
+ * Default validation condition for metadata entries by entity type. Validation is done by parsing the entity type
+ * into a corresponding Nimbus object.
+ */
+public class DefaultMetadataValidationCondition implements BiPredicate<String, Map<String,Object>> {
+
+ /** Class logger. */
+ @Nonnull private final static Logger log = LoggerFactory.getLogger(DefaultMetadataValidationCondition.class);
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean test(@Nullable final String key, @Nullable final Map<String, Object> metadata) {
+ if (StringSupport.trimOrNull(key) == null || metadata == null) {
+ log.error("Invalid input to the validation condition key={}, metadata={}", key, metadata);
+ return false;
+ }
+ try {
+ assert key != null;
+ switch (key) {
+ case "federation_entity":
+ log.debug("Ignoring validation of {}", key);
+ return true;
+ case "openid_provider":
+ OIDCProviderMetadata.parse(new JSONObject(metadata));
+ return true;
+ case "openid_relying_party":
+ OIDCClientMetadata.parse(new JSONObject(metadata));
+ return true;
+ case "oauth_authorization_server":
+ AuthorizationServerMetadata.parse(new JSONObject(metadata));
+ return true;
+ case "oauth_client":
+ ClientMetadata.parse(new JSONObject(metadata));
+ return true;
+ case "oauth_resource":
+ log.debug("Ignoring validation of {}", key);
+ return true;
+ default:
+ log.debug("Ignoring validation of {}", key);
+ return true;
+ }
+ } catch (final ParseException e) {
+ log.warn("Could not parse entity_type {}", key, e);
+ }
+ return false;
+ }
+
+}
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/navigate/DefaultTrustChainIDsLookupStrategy.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/navigate/DefaultTrustChainIDsLookupStrategy.java
new file mode 100644
index 0000000..7661f81
--- /dev/null
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/navigate/DefaultTrustChainIDsLookupStrategy.java
@@ -0,0 +1,43 @@
+/*
+ * 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 java.util.stream.IntStream;
+
+import javax.annotation.Nullable;
+
+import net.shibboleth.oidfed.metadata.EntityStatement;
+
+/**
+ * Default strategy for looking up the entity IDs of a trust chain.
+ */
+public class DefaultTrustChainIDsLookupStrategy implements Function<List<EntityStatement<?>>,List<String>> {
+
+ /** {@inheritDoc} */
+ @Override @Nullable
+ public List<String> apply(@Nullable final List<EntityStatement<?>> input) {
+ return Optional.ofNullable(input)
+ .filter(chain -> chain != null && !chain.isEmpty())
+ .map(chain -> IntStream.range(0, chain.size())
+ .mapToObj(i -> chain.get(i))
+ .map(statement -> statement.getSubject())
+ .distinct()
+ .toList())
+ .orElse(null);
+ }
+}
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/navigate/DefaultTrustChainMetadataPolicyMergingStrategy.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/navigate/DefaultTrustChainMetadataPolicyMergingStrategy.java
new file mode 100644
index 0000000..3e60d04
--- /dev/null
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/navigate/DefaultTrustChainMetadataPolicyMergingStrategy.java
@@ -0,0 +1,145 @@
+/*
+ * 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.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import net.shibboleth.oidc.metadata.policy.MetadataPolicy;
+import net.shibboleth.oidfed.metadata.EntityStatement;
+import net.shibboleth.oidfed.metadata.payload.SubordinateStatementPayload;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.ThreadSafeAfterInit;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.collection.Pair;
+import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.logic.ConstraintViolationException;
+import net.shibboleth.shared.primitive.StringSupport;
+
+/**
+ * Default strategy to merge the metadata policies for the given entity type from the given trust chain. Finally,
+ * a configurable local metadata policy is merged to the resulting map of metadata policies.
+ */
+ at ThreadSafeAfterInit
+public class DefaultTrustChainMetadataPolicyMergingStrategy extends AbstractIdentifiableInitializableComponent
+ implements BiFunction<List<EntityStatement<?>>,String, Map<String, MetadataPolicy>> {
+
+ /** The strategy used for merging two metadata policies. */
+ @NonnullAfterInit private BiFunction<Map<String, MetadataPolicy>, Map<String, MetadataPolicy>,
+ Pair<Map<String, MetadataPolicy>, Boolean>> metadataPolicyMergingStrategy;
+
+ /** The strategy used for local (additional) metadata policy. */
+ @NonnullAfterInit private Function<List<EntityStatement<?>>, Map<String, MetadataPolicy>>
+ localMetadataPolicyStrategy;
+
+ /**
+ * Set the strategy used for merging two metadata policies.
+ *
+ * @param strategy What to set.
+ */
+ public void setMetadataPolicyMergingStrategy(@Nonnull final BiFunction<Map<String,MetadataPolicy>,
+ Map<String,MetadataPolicy>, Pair<Map<String, MetadataPolicy>, Boolean>> strategy) {
+ checkSetterPreconditions();
+ metadataPolicyMergingStrategy = Constraint.isNotNull(strategy,
+ "Metadata policy merging strategy cannot be null");
+ }
+
+ /**
+ * Set the strategy used for local (additional) metadata policy.
+ *
+ * @param strategy What to set.
+ */
+ public void setLocalMetadataPolicyStrategy(
+ @Nonnull final Function<List<EntityStatement<?>>, Map<String, MetadataPolicy>> strategy) {
+ checkSetterPreconditions();
+ localMetadataPolicyStrategy = Constraint.isNotNull(strategy,
+ "Local metadata policy strategy cannot be null");
+ }
+
+ /** {@inheritDoc} */
+ protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+
+ if (metadataPolicyMergingStrategy == null) {
+ throw new ComponentInitializationException("Metadata policy merging strategy cannot be null");
+ }
+ if (localMetadataPolicyStrategy == null) {
+ throw new ComponentInitializationException("Local metadata policy strategy cannot be null");
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override @Nonnull
+ public Map<String, MetadataPolicy> apply(@Nullable final List<EntityStatement<?>> trustChain,
+ @Nullable final String entityType) throws ConstraintViolationException {
+ checkComponentActive();
+ if (trustChain == null || trustChain.isEmpty()) {
+ return CollectionSupport.emptyMap();
+ }
+ final Map<String, MetadataPolicy> result = new HashMap<>();
+ for (int i = trustChain.size(); i > 0; i--) {
+ final EntityStatement<?> entityStatement = trustChain.get(i - 1);
+ if (entityStatement.getParsedPayload() instanceof SubordinateStatementPayload ssp) {
+ final Map<String, Map<String, MetadataPolicy>> policy = ssp.getMetadataPolicy();
+ if (policy == null || policy.isEmpty()) {
+ continue;
+ }
+
+ if (StringSupport.trimOrNull(entityType) != null) {
+ try {
+ assert entityType != null;
+ result.putAll(mergePolicies(result, policy.get(entityType)));
+ } catch (final ConstraintViolationException e) {
+ throw new ConstraintViolationException("Could not merge federation metadata policies");
+ }
+ }
+ }
+ }
+ try {
+ result.putAll(mergePolicies(result, localMetadataPolicyStrategy.apply(trustChain)));
+ } catch (final ConstraintViolationException e) {
+ throw new ConstraintViolationException("Could not merge local metadata policy in the federation policy");
+ }
+ return result;
+ }
+
+ /**
+ * Merges the two maps of metadata policies.
+ *
+ * @param first the first map of policies
+ * @param second the second map of policies
+ * @return the map containing merged policies
+ * @throws ConstraintViolationException if the merging fails
+ */
+ @Nonnull
+ protected Map<String, MetadataPolicy> mergePolicies(@Nullable final Map<String, MetadataPolicy> first,
+ @Nullable final Map<String, MetadataPolicy> second) throws ConstraintViolationException {
+ final Pair<Map<String, MetadataPolicy>, Boolean> result = metadataPolicyMergingStrategy.apply(first, second);
+ if (result != null && Boolean.TRUE.equals(result.getSecond())) {
+ final Map<String, MetadataPolicy> mergedMap = result.getFirst();
+ return mergedMap == null ? CollectionSupport.emptyMap() : mergedMap;
+ }
+ throw new ConstraintViolationException("Merge failed");
+ }
+
+}
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list