[java-oidfed-common] 03/03: Import and refactor entity-configuration flow from java-idp-plugin-oidc-op-oidfed
Codeberg
noreply at shibboleth.net
Fri May 15 07:49:31 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/009ab5b866301de7563447b57767680844b61cb3
commit 009ab5b866301de7563447b57767680844b61cb3
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Fri May 15 10:48:56 2026 +0300
Import and refactor entity-configuration flow from java-idp-plugin-oidc-op-oidfed
- New EntityConfigurationMetadataDecorator for providing plugins a method for extending entity configuration metadata
- Implementing beans are autowired to EntityConfigurationMetadataDecoratorManager
- Moved required profile actions and functions to net.shibboleth.idp.plugin.oidc.op.oidfed.profile impl/navigate
---
.gitignore | 3 +-
.../EntityConfigurationMetadataDecorator.java | 33 +
...ntityConfigurationMetadataDecoratorManager.java | 73 ++
.../oidfed/profile/OidFederationEventIds.java | 84 +++
.../entity-configuration-beans.xml | 280 +++++++
.../entity-configuration-flow.xml | 121 +++
.../DefaultTrustChainFetchingStrategyTest.java | 365 +++++++++
.../flow/oidfed/AbstractFederationFlowTest.java | 839 +++++++++++++++++++++
.../flow/oidfed/EntityConfigurationFlowTest.java | 160 ++++
.../EntityConfigurationMetadataCacheTest.java | 594 +++++++++++++++
.../cache/SignedKeysetMetadataCacheTest.java | 233 ++++++
.../SubordinateStatementMetadataCacheTest.java | 665 ++++++++++++++++
.../oidfed/cache/TrustChainMetadataCacheTest.java | 274 +++++++
.../CustomEntityConfigurationFilterStrategy.java | 58 ++
...CustomEntityConfigurationMetadataDecorator.java | 46 ++
.../CustomSubordinateStatementFilterStrategy.java | 58 ++
.../support/CustomTrustChainFilterStrategy.java | 47 ++
.../shibboleth/oidfed/test/TrustChainTestUtil.java | 164 ++++
.../resources/credentials/fed-local-anchor.jwk | 13 +
.../credentials/fed-local-intermediate.jwk | 12 +
.../resources/credentials/fed-signing-es256.jwk | 10 +
.../resources/credentials/fed-signing-es384.jwk | 9 +
.../resources/credentials/fed-signing-es521.jwk | 9 +
.../test/resources/credentials/fed-signing-rs.jwk | 8 +
.../net/shibboleth/idp/module/conf/credentials.xml | 70 ++
.../net/shibboleth/idp/module/conf/global.xml | 53 ++
.../net/shibboleth/idp/module/conf/idp.properties | 210 ++++++
.../net/shibboleth/idp/module/conf/logback.xml | 197 +++++
.../idp/module/conf/metadata-providers.xml | 28 +
.../idp/module/conf/oidfed/oidfed-credentials.xml | 37 +
.../oidfed/oidfed-entity-configuration-claims.xml | 44 ++
.../oidfed-entity-configuration-metadata.json | 11 +
.../module/conf/oidfed/oidfed-trust-anchors.json | 24 +
.../conf/oidfed/oidfed-trustchain-resolver.xml | 25 +
.../idp/module/conf/oidfed/oidfed.properties | 13 +
.../shibboleth/idp/module/conf/relying-party.xml | 33 +
.../net/shibboleth/idp/module/conf/services.xml | 92 +++
.../impl/AbstractBuildEntityStatementAction.java | 301 ++++++++
.../profile/impl/BuildEntityConfiguration.java | 215 ++++++
...TrustMarkFromMetadataCacheFetchingFunction.java | 205 +++++
.../profile/impl/EntityStatementContext.java | 133 ++++
.../impl/EntityStatementUpdateStrategy.java | 57 ++
...ormOutboundFederationConfigurationResponse.java | 214 ++++++
.../impl/InitializeEntityStatementContext.java | 165 ++++
...ClaimsSetFromEntityStatementLookupFunction.java | 85 +++
.../profile/impl/LookupCachedNimbusResponse.java | 141 ++++
.../impl/RelyingPartyCachedMessageContext.java | 75 ++
...ntityConfigurationTrustMarksLookupStrategy.java | 77 ++
.../DefaultTrustAnchorHintsLookupStrategy.java | 89 +++
49 files changed, 6751 insertions(+), 1 deletion(-)
diff --git a/.gitignore b/.gitignore
index b1d018d..2d96300 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,4 +15,5 @@
/oidfed-common-api/target
/oidfed-common-impl/target
/oidfed-common-dist/target
-
+/oidfed-common-conf-impl/src/test/resources/conf/local-log-config.properties
+/oidfed-common-conf-impl/classpath:
diff --git a/oidfed-common-api/src/main/java/net/shibboleth/oidfed/profile/EntityConfigurationMetadataDecorator.java b/oidfed-common-api/src/main/java/net/shibboleth/oidfed/profile/EntityConfigurationMetadataDecorator.java
new file mode 100644
index 0000000..aad941f
--- /dev/null
+++ b/oidfed-common-api/src/main/java/net/shibboleth/oidfed/profile/EntityConfigurationMetadataDecorator.java
@@ -0,0 +1,33 @@
+/*
+ * 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;
+
+import java.util.Map;
+import java.util.function.BiConsumer;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import net.shibboleth.shared.component.IdentifiedComponent;
+
+/**
+ * An interface for decorating entity configuration metadata.
+ *
+ * The interface extends the {@code BiConsumer} by specifying the input types explicitly and is expected to operate via
+ * side-effects.
+ */
+public interface EntityConfigurationMetadataDecorator extends
+ BiConsumer<Map<String,Map<String,Object>>, ProfileRequestContext>, IdentifiedComponent {
+
+}
diff --git a/oidfed-common-api/src/main/java/net/shibboleth/oidfed/profile/EntityConfigurationMetadataDecoratorManager.java b/oidfed-common-api/src/main/java/net/shibboleth/oidfed/profile/EntityConfigurationMetadataDecoratorManager.java
new file mode 100644
index 0000000..4052100
--- /dev/null
+++ b/oidfed-common-api/src/main/java/net/shibboleth/oidfed/profile/EntityConfigurationMetadataDecoratorManager.java
@@ -0,0 +1,73 @@
+/*
+ * 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;
+
+import java.util.Collection;
+import java.util.List;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.springframework.beans.factory.annotation.Autowired;
+
+import net.shibboleth.shared.annotation.constraint.NotLive;
+import net.shibboleth.shared.annotation.constraint.Unmodifiable;
+import net.shibboleth.shared.collection.CollectionSupport;
+
+/**
+ * Manages and exposes instances of the {@link EntityConfigurationMetadataDecorator} interface.
+ */
+public class EntityConfigurationMetadataDecoratorManager {
+
+ /** Collection of decorators. */
+ @Nonnull private final Collection<EntityConfigurationMetadataDecorator> metadataDecorators;
+
+ /**
+ * Constructor.
+ *
+ * @param decorators instances to manage
+ */
+ @Autowired
+ public EntityConfigurationMetadataDecoratorManager(
+ @Nullable final Collection<EntityConfigurationMetadataDecorator> decorators) {
+ metadataDecorators =
+ decorators == null ? CollectionSupport.emptyList() : CollectionSupport.copyToList(decorators);
+ }
+
+ /**
+ * Get all of the registered decorators.
+ *
+ * @return all registered decorators
+ */
+ @Nonnull @NotLive @Unmodifiable public Collection<EntityConfigurationMetadataDecorator> all() {
+ return CollectionSupport.copyToList(metadataDecorators);
+ }
+
+ /**
+ * Get a {@link EntityConfigurationMetadataDecorator} by type.
+ *
+ * @param <T> class type
+ * @param claz class type
+ *
+ * @return decorator for the type, or null
+ */
+ @Nullable public <T extends EntityConfigurationMetadataDecorator> List<T> byClass(@Nonnull final Class<T> claz) {
+ return metadataDecorators.stream()
+ .filter(claz::isInstance)
+ .map(claz::cast)
+ .toList();
+ }
+
+}
diff --git a/oidfed-common-api/src/main/java/net/shibboleth/oidfed/profile/OidFederationEventIds.java b/oidfed-common-api/src/main/java/net/shibboleth/oidfed/profile/OidFederationEventIds.java
new file mode 100644
index 0000000..2e1bd20
--- /dev/null
+++ b/oidfed-common-api/src/main/java/net/shibboleth/oidfed/profile/OidFederationEventIds.java
@@ -0,0 +1,84 @@
+/*
+ * 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;
+
+import javax.annotation.Nonnull;
+
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+
+/**
+ * OpenID Federation -specific constants to use for {@link org.opensaml.profile.action.ProfileAction}
+ * {@link org.opensaml.profile.context.EventContext}s.
+ */
+public class OidFederationEventIds {
+
+ /**
+ * ID of event returned if a flow wishes to indicate that another trust chain should be selected instead
+ */
+ @Nonnull @NotEmpty public static final String RESELECT_TRUST_CHAIN = "ReselectTrustChain";
+
+ /**
+ * ID of event returned if cached response was found and set to the context.
+ */
+ @Nonnull @NotEmpty public static final String CACHED_RESPONSE_FOUND = "CachedResponseFound";
+
+ /**
+ * ID of event returned if no trust chains were resolved for the client.
+ */
+ @Nonnull @NotEmpty public static final String NO_TRUST_CHAINS_RESOLVED = "NoTrustChainsResolved";
+
+ /**
+ * ID of event returned if the given trust anchor is invalid.
+ */
+ @Nonnull @NotEmpty public static final String INVALID_TRUST_ANCHOR = "InvalidTrustAnchor";
+
+ /**
+ * ID of event returned if the given subject is invalid.
+ */
+ @Nonnull @NotEmpty public static final String INVALID_SUBJECT = "InvalidSubject";
+
+ /**
+ * ID of event returned if the given metadata is invalid.
+ */
+ @Nonnull @NotEmpty public static final String INVALID_METADATA = "InvalidMetadata";
+
+ /**
+ * ID of event returned if the given metadata policy is invalid.
+ */
+ @Nonnull @NotEmpty public static final String INVALID_METADATA_POLICY = "InvalidMetadataPolicy";
+
+ /**
+ * ID of event returned if the given metadata is invalid against policy.
+ */
+ @Nonnull @NotEmpty public static final String INVALID_METADATA_AGAINST_POLICY = "InvalidMetadataAgainstPolicy";
+
+ /**
+ * ID of event returned if the trust chain is invalid against constraints.
+ */
+ @Nonnull @NotEmpty public static final String INVALID_TRUST_CHAIN_AGAINST_CONSTRAINTS =
+ "InvalidTrustChainAgainstConstraints";
+
+ /**
+ * ID of event returned if the mandatory provided trust chain could not be fetched.
+ */
+ @Nonnull @NotEmpty public static final String MISSING_MANDATORY_PROVIDED_TRUST_CHAIN =
+ "MissingMandatoryProvidedTrustChain";
+
+ /**
+ * ID of event returned if the provided trust chain could not be verified.
+ */
+ @Nonnull @NotEmpty public static final String INVALID_PROVIDED_TRUST_CHAIN = "InvalidProvidedTrustChain";
+
+}
diff --git a/oidfed-common-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/entity-configuration/entity-configuration-beans.xml b/oidfed-common-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/entity-configuration/entity-configuration-beans.xml
new file mode 100644
index 0000000..5f02e7d
--- /dev/null
+++ b/oidfed-common-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/entity-configuration/entity-configuration-beans.xml
@@ -0,0 +1,280 @@
+<?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.OIDFederationEntityConfigurationProfileConfiguration).PROFILE_ID}" />
+
+ <bean id="shibboleth.oidfed.loggingId" class="java.lang.String" c:_0="%{idp.service.logging.oidfedconfig:OIDFED.Configuration}" />
+
+ <bean id="shibboleth.oidfed.browserProfile" class="java.lang.Boolean" c:_0="false" />
+
+ <util:constant id="shibboleth.metrics.ProfileCounter"
+ static-field="net.shibboleth.oidfed.profile.config.impl.DefaultOIDFederationEntityConfigurationProfileConfiguration.PROFILE_COUNTER" />
+
+ <bean id="shibboleth.oidfed.EntityConfigurationResponseMetadataCache" parent="shibboleth.oidfed.CacheBuilder">
+ <constructor-arg>
+ <bean p:cacheId="DefaultEntityConfigurationResponseMetadataCache" parent="shibboleth.oidfed.EntityConfigurationResponseMetadataCacheBuilderSpec"
+ p:cleanupTaskInterval="PT30S"/>
+ </constructor-arg>
+ </bean>
+
+ <bean id="shibboleth.oidfed.EntityConfigurationResponseMetadataCacheBuilderSpec"
+ class="net.shibboleth.oidc.metadata.cache.impl.DynamicMetadataCacheBuilderSpec"
+ p:minCacheDuration="%{idp.oidfed.entity-configuration.maxRefreshDelay:PT1S}"
+ p:maxCacheDuration="%{idp.oidfed.entity-configuration.maxRefreshDelay:PT30S}">
+ <property name="criteriaToIdentifierStrategy">
+ <bean parent="shibboleth.Functions.Constant" c:target-ref="shibboleth.oidfed.entityId" />
+ </property>
+ <property name="identifierExtractionStrategy">
+ <bean parent="shibboleth.Functions.Constant" c:target-ref="shibboleth.oidfed.entityId" />
+ </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.DefaultEntityConfigurationResponseFetchingStrategy" />
+ </property>
+ </bean>
+
+ <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="PopulateMetricContext"
+ class="org.opensaml.profile.action.impl.PopulateMetricContext" scope="prototype"
+ p:counterName="#{getObject('shibboleth.metrics.ProfileCounter')}"
+ p:metricStrategy="#{getObject('shibboleth.metrics.MetricStrategy')}" />
+
+ <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="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="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="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="LookupCachedResponse"
+ class="net.shibboleth.oidfed.profile.impl.LookupCachedNimbusResponse"
+ scope="prototype"
+ p:responseCache-ref="shibboleth.oidfed.EntityConfigurationResponseMetadataCache" />
+
+ <bean id="InitializeEntityStatementContext"
+ class="net.shibboleth.oidfed.profile.impl.InitializeEntityStatementContext"
+ p:metadataSkeletonLookupStrategy-ref="#{'%{idp.oidfed.configuration.EntityConfigurationMetadataSkeletonLookupStrategy:DefaultEntityConfigurationMetadataSkeletonLookupStrategy}'.trim()}"
+ p:metadataDecoratorManager-ref="#{'%{idp.oidfed.configuration.MetadataDecoratorManager:shibboleth.oidfed.DefaultMetadataDecoratorManager}'.trim()}"/>
+
+ <bean id="shibboleth.oidfed.DefaultMetadataDecoratorManager"
+ class="net.shibboleth.oidfed.profile.EntityConfigurationMetadataDecoratorManager"/>
+
+ <bean id="DefaultEntityConfigurationMetadataSkeletonLookupStrategy"
+ class="net.shibboleth.oidfed.metadata.cache.local.DefaultEntityConfigurationMetadataSkeletonLookupStrategy"
+ p:metadataSkeletonCache-ref="shibboleth.oidfed.EntityConfigurationMetadataSkeletonMetadataCache"/>
+
+ <bean id="PopulateEntityStatementSignatureSigningParameters"
+ class="net.shibboleth.oidc.profile.impl.PopulateJWTSignatureSigningParameters" scope="prototype"
+ c:strategy-ref="shibboleth.MessageContextLookup.Outbound"
+ p:securityParametersContextLookupStrategy-ref="EntityStatementSecurityParametersContextLookupStrategy">
+ <property name="configurationLookupStrategy">
+ <bean lazy-init="true"
+ class="net.shibboleth.oidc.profile.config.navigate.JWTSignatureSigningConfigurationLookupFunction" />
+ </property>
+ <property name="signatureSigningParametersResolver">
+ <bean class="net.shibboleth.oidc.security.jose.impl.ClientInformationSignatureSigningParametersResolver">
+ <constructor-arg name="signatureAlgorithmLookupStrategy">
+ <bean parent="shibboleth.Functions.Constant" c:target="" />
+ </constructor-arg>
+ <constructor-arg name="defaultAlgorithmValue" value="%{idp.oidfed.entity.sigalg:RS256}" />
+ </bean>
+ </property>
+ </bean>
+
+ <bean id="EntityStatementSecurityParametersContextLookupStrategy" parent="shibboleth.Functions.Compose"
+ c:g-ref="shibboleth.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="EntityStatementSecurityParametersCreationViaMessageContextStrategy" parent="shibboleth.Functions.Compose">
+ <constructor-arg name="g" ref="EntityStatementSecurityParametersContextLookupStrategy" />
+ <constructor-arg name="f">
+ <bean parent="shibboleth.Functions.Expression" c:expression="#input.getParent()" />
+ </constructor-arg>
+ </bean>
+
+ <bean id="ConfigurationRelyingPartyCreationStrategy" parent="shibboleth.Functions.Compose"
+ c:g-ref="shibboleth.ChildLookupOrCreate.RelyingPartyContext"
+ c:f-ref="shibboleth.MessageContextLookup.Outbound" />
+
+ <bean id="BuildEntityStatement"
+ class="net.shibboleth.oidfed.profile.impl.BuildEntityConfiguration" scope="prototype"
+ p:identifierGeneratorLookupStrategy-ref="shibboleth.oidfed.DefaultIdentifierGenerationStrategy"
+ p:objectMapper-ref="#{'%{idp.oidfed.logging.objectMapper:shibboleth.oidfed.JSONObjectMapper}'.trim()}"/>
+
+ <bean id="SignEntityStatement" class="net.shibboleth.idp.profile.impl.WebFlowMessageHandlerAdaptor"
+ scope="prototype" c:executionDirection="OUTBOUND ">
+ <constructor-arg name="messageHandler">
+ <bean id="SignEntityStatementHandler"
+ class="net.shibboleth.oidc.security.impl.SignJWTHandler" scope="prototype" p:logName="Entity Statement"
+ p:securityParametersLookupStrategy-ref="EntityStatementSecurityParametersCreationViaMessageContextStrategy"
+ p:typeHeader="entity-statement+jwt">
+ <property name="claimsToSignLookupStrategy">
+ <bean
+ class="net.shibboleth.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.FormOutboundFederationConfigurationResponse"
+ scope="prototype" p:responseCache-ref="shibboleth.oidfed.EntityConfigurationResponseMetadataCache">
+ </bean>
+
+ <bean id="shibboleth.oidfed.EntityConfigurationMetadataSkeletonMetadataCache" parent="shibboleth.oidfed.CacheBuilder">
+ <constructor-arg>
+ <bean p:cacheId="DefaultEntityConfigurationMetadataSkeletonMetadataCache"
+ parent="shibboleth.oidfed.EntityConfigurationMetadataSkeletonMetadataCacheBuilderSpec"/>
+ </constructor-arg>
+ </bean>
+
+ <bean id="shibboleth.oidfed.EntityConfigurationMetadataSkeletonMetadataCacheBuilderSpec"
+ class="net.shibboleth.oidc.metadata.cache.impl.BatchMetadataCacheBuilderSpec"
+ p:metadataValidPredicate-ref="shibboleth.Conditions.TRUE">
+ <property name="criteriaToIdentifierStrategy">
+ <bean parent="shibboleth.Functions.Constant" c:target-ref="shibboleth.oidfed.entityId" />
+ </property>
+ <property name="identifierExtractionStrategy">
+ <bean parent="shibboleth.Functions.Constant" c:target-ref="shibboleth.oidfed.entityId" />
+ </property>
+ <property name="loadingStrategy">
+ <bean class="net.shibboleth.oidc.metadata.cache.impl.DefaultResourceLoadingStrategy">
+ <constructor-arg name="metadata">
+ <bean class="org.springframework.core.io.Resource"
+ factory-bean="PreferFileSystemResourceLoader" factory-method="getResource">
+ <constructor-arg>
+ <bean class="java.lang.String" factory-method="valueOf">
+ <constructor-arg value="%{idp.oidfed.configuration.MetadataSkaletonFile:%{idp.home}/conf/oidfed/oidfed-entity-configuration-metadata.json}" />
+ </bean>
+ </constructor-arg>
+ </bean>
+ </constructor-arg>
+ </bean>
+ </property>
+ <property name="parsingStrategy">
+ <bean class="net.shibboleth.oidc.metadata.cache.impl.DefaultJSONMapParsingStrategy"
+ c:valueClass="java.util.Map"/>
+ </property>
+ <property name="sourceMetadataExpiryStrategy">
+ <bean class="net.shibboleth.oidc.metadata.cache.impl.DefaultSourceMetadataExpirationTimeStrategy"
+ c:duration="%{idp.oidfed.configuration.MetadataSkaletonCacheLifetime:PT10M}"/>
+ </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="EncodeMessage" class="org.opensaml.profile.action.impl.EncodeMessage" scope="prototype"
+ p:messageEncoderFactory-ref="oidfed.messageEncoderFactory"
+ p:httpServletResponseSupplier-ref="shibboleth.HttpServletResponseSupplier"/>
+
+ <bean id="oidfed.messageEncoderFactory"
+ class="net.shibboleth.oidc.profile.encoding.impl.OIDCResponseEncoderFactory"
+ p:messageEncoder-ref="oidfed.nimbusEncoder" scope="prototype" />
+
+ <bean id="oidfed.nimbusEncoder" class="net.shibboleth.oidc.profile.encoding.impl.SimpleNimbusResponseEncoder"
+ scope="prototype" p:httpServletResponseSupplier-ref="shibboleth.HttpServletResponseSupplier" init-method="" />
+
+<!-- TODO wire for logging
+ p:objectMapper-ref="#{'%{idp.oidfed.logging.objectMapper:shibboleth.oidfed.JSONObjectMapper}'.trim()}"/>
+-->
+ <bean id="BuildErrorResponseFromEvent"
+ class="net.shibboleth.oidc.profile.impl.BuildJSONErrorResponseFromEvent" scope="prototype"
+ p:defaultStatusCode="500" p:defaultCode="server_error"
+ p:httpServletResponseSupplier-ref="shibboleth.HttpServletResponseSupplier">
+ <property name="eventContextLookupStrategy">
+ <bean class="net.shibboleth.idp.profile.context.navigate.WebFlowCurrentEventLookupFunction" />
+ </property>
+ </bean>
+
+ <bean id="PostResponsePopulateAuditContext" parent="shibboleth.AbstractPopulateAuditContext"
+ p:fieldExtractors="#{getObject('shibboleth.oidfed.PostResponseAuditExtractors') ?: getObject('shibboleth.oidfed.DefaultPostResponseAuditExtractors')}" />
+
+ <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="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/entity-configuration/entity-configuration-flow.xml b/oidfed-common-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/entity-configuration/entity-configuration-flow.xml
new file mode 100644
index 0000000..be49e83
--- /dev/null
+++ b/oidfed-common-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/entity-configuration/entity-configuration-flow.xml
@@ -0,0 +1,121 @@
+<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">
+
+ <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="SelectRelyingPartyConfiguration" />
+ <evaluate expression="SelectProfileConfiguration" />
+ <evaluate expression="PopulateInboundInterceptContext" />
+ <evaluate expression="'proceed'" />
+ <transition on="proceed" to="CheckInboundInterceptContext" />
+ </action-state>
+
+ <decision-state id="CheckInboundInterceptContext">
+ <if test="opensamlProfileRequestContext.ensureSubcontext(T(net.shibboleth.idp.profile.context.ProfileInterceptorContext)).getAvailableFlows().isEmpty()"
+ then="LookupCachedResponse" else="DoInboundInterceptSubflow" />
+ </decision-state>
+
+ <subflow-state id="DoInboundInterceptSubflow" subflow="intercept">
+ <input name="calledAsSubflow" value="true" />
+ <transition on="proceed" to="LookupCachedResponse" />
+ </subflow-state>
+
+ <action-state id="LookupCachedResponse">
+ <evaluate expression="CallInboundMessageHandler" />
+ <evaluate expression="LookupCachedResponse" />
+ <evaluate expression="'proceed'" />
+ <transition on="CachedResponseFound" to="BuildResponseMessage" />
+ <transition on="proceed" to="InitializeEntityStatementContext" />
+ </action-state>
+
+ <action-state id="InitializeEntityStatementContext">
+ <evaluate expression="InitializeEntityStatementContext"/>
+ <evaluate expression="SelectProfileConfiguration" />
+ <evaluate expression="'proceed'" />
+
+ <transition on="proceed" to="BuildEntityConfiguration" />
+ </action-state>
+
+ <action-state id="BuildEntityConfiguration">
+ <evaluate expression="PopulateEntityStatementSignatureSigningParameters" />
+ <evaluate expression="BuildEntityStatement" />
+ <evaluate expression="SignEntityStatement" />
+ <evaluate expression="'proceed'" />
+
+ <transition on="proceed" to="BuildResponseMessage" />
+ </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">
+ <if test="opensamlProfileRequestContext.ensureSubcontext(T(net.shibboleth.idp.profile.context.ProfileInterceptorContext)).getAvailableFlows().isEmpty()"
+ then="CommitResponse" else="DoOutboundInterceptSubflow" />
+ </decision-state>
+
+ <subflow-state id="DoOutboundInterceptSubflow" subflow="intercept">
+ <input name="calledAsSubflow" value="true" />
+ <transition on="proceed" to="CommitResponse" />
+ <transition to="HandleError" />
+ </subflow-state>
+
+ <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 id="CommitResponse">
+ <on-entry>
+ <evaluate expression="CallOutboundMessageHandler" />
+ <evaluate expression="EncodeMessage" />
+ <evaluate expression="PostResponsePopulateAuditContext" />
+ <evaluate expression="WriteAuditLog" />
+ <evaluate expression="RecordResponseComplete" />
+ </on-entry>
+ </end-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>
+
+ <global-transitions>
+ <transition on-exception="java.lang.RuntimeException" to="LogRuntimeException" />
+ <transition on="#{!'proceed'.equals(currentEvent.id)}" to="HandleError" />
+ </global-transitions>
+
+ <bean-import resource="entity-configuration-beans.xml" />
+
+</flow>
\ No newline at end of file
diff --git a/oidfed-common-conf-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/trustchain/DefaultTrustChainFetchingStrategyTest.java b/oidfed-common-conf-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/trustchain/DefaultTrustChainFetchingStrategyTest.java
new file mode 100644
index 0000000..9e9caec
--- /dev/null
+++ b/oidfed-common-conf-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/trustchain/DefaultTrustChainFetchingStrategyTest.java
@@ -0,0 +1,365 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.trustchain;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+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.SubordinateStatement;
+import net.shibboleth.oidfed.metadata.cache.PreSelectedTrustChainCriterion;
+import net.shibboleth.oidfed.metadata.cache.configuration.EntityConfigurationContainer;
+import net.shibboleth.oidfed.metadata.cache.local.LocalKeyContainer;
+import net.shibboleth.oidfed.metadata.cache.subordinate.SubordinateStatementCacheIdentifier;
+import net.shibboleth.oidfed.metadata.cache.subordinate.SubordinateStatementContainer;
+import net.shibboleth.oidfed.metadata.cache.trustchain.DefaultTrustChainFetchingStrategy;
+import net.shibboleth.oidfed.metadata.cache.trustchain.TrustChainsContainer;
+import net.shibboleth.oidfed.metadata.payload.EntityConfigurationPayload;
+import net.shibboleth.oidfed.metadata.util.EntityStatementHelper;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.resolver.CriteriaSet;
+
+/**
+ * Unit tests for {@link DefaultTrustChainFetchingStrategy}
+ */
+ at SuppressWarnings("null")
+public class DefaultTrustChainFetchingStrategyTest {
+
+ DefaultTrustChainFetchingStrategy function;
+
+ Function<CriteriaSet, String> criteriaToSubjectEntityIdStrategy;
+ MetadataCache<EntityConfigurationContainer> entityConfigurationCache;
+ MetadataCache<SubordinateStatementContainer> subordinateStatementCache;
+ MetadataCache<Map<String, LocalKeyContainer>> localTrustAnchorsCache;
+ Function<CriteriaSet, Duration> criteriaToValidContainerLifetimeStrategy;
+ Function<CriteriaSet, Duration> criteriaToInvalidContainerLifetimeStrategy;
+
+ @SuppressWarnings("unchecked")
+ public void initMocks() {
+ criteriaToSubjectEntityIdStrategy = mock(Function.class);
+ entityConfigurationCache = mock(MetadataCache.class);
+ subordinateStatementCache = mock(MetadataCache.class);
+ localTrustAnchorsCache = mock(MetadataCache.class);
+ criteriaToValidContainerLifetimeStrategy = mock(Function.class);
+ criteriaToInvalidContainerLifetimeStrategy = mock(Function.class);
+ }
+
+ @BeforeMethod
+ public void setup() {
+ initMocks();
+ function = new DefaultTrustChainFetchingStrategy();
+ function.setCriteriaToSubjectEntityIdStrategy(criteriaToSubjectEntityIdStrategy);
+ function.setEntityConfigurationCache(entityConfigurationCache);
+ function.setSubordinateStatementCache(subordinateStatementCache);
+ function.setLocalTrustAnchorsCache(localTrustAnchorsCache);
+ function.setCriteriaToValidContainerLifetimeStrategy(criteriaToValidContainerLifetimeStrategy);
+ function.setCriteriaToInvalidContainerLifetimeStrategy(criteriaToInvalidContainerLifetimeStrategy);
+ function.setId("mockFunction");
+ try {
+ function.initialize();
+ } catch (ComponentInitializationException e) {
+ Assert.fail("Could not initialize the function", e);
+ }
+ }
+
+ @Test
+ public void nullCriteria_returnsNull() {
+ Assert.assertEquals(function.apply(null), null);
+ }
+
+ @Test
+ public void noValidLifetimeCriteria_returnsNull() {
+ when(criteriaToValidContainerLifetimeStrategy.apply(any())).thenReturn(null);
+ when(criteriaToInvalidContainerLifetimeStrategy.apply(any())).thenReturn(Duration.ofMinutes(5));
+ Assert.assertEquals(function.apply(new CriteriaSet()), null);
+ }
+
+ @Test
+ public void noInvalidLifetimeCriteria_returnsNull() {
+ when(criteriaToValidContainerLifetimeStrategy.apply(any())).thenReturn(Duration.ofMinutes(5));
+ when(criteriaToInvalidContainerLifetimeStrategy.apply(any())).thenReturn(null);
+ Assert.assertEquals(function.apply(new CriteriaSet()), null);
+ }
+
+ @Test
+ public void noEntityConfigurationResolved_returnsNull() throws MetadataCacheException {
+ when(criteriaToValidContainerLifetimeStrategy.apply(any())).thenReturn(Duration.ofMinutes(5));
+ when(criteriaToInvalidContainerLifetimeStrategy.apply(any())).thenReturn(Duration.ofMinutes(5));
+ when(entityConfigurationCache.get(any())).thenReturn(CollectionSupport.emptyList());
+ Assert.assertEquals(function.apply(new CriteriaSet()), null);
+ }
+
+ @Test
+ public void entityConfigurationException_returnsNull() throws MetadataCacheException {
+ when(criteriaToValidContainerLifetimeStrategy.apply(any())).thenReturn(Duration.ofMinutes(5));
+ when(criteriaToInvalidContainerLifetimeStrategy.apply(any())).thenReturn(Duration.ofMinutes(5));
+ when(entityConfigurationCache.get(any())).thenThrow(MetadataCacheException.class);
+ Assert.assertEquals(function.apply(new CriteriaSet()), null);
+ }
+
+ @Test
+ public void entityConfigurationNoHints_returnsNull() throws MetadataCacheException {
+ when(criteriaToValidContainerLifetimeStrategy.apply(any())).thenReturn(Duration.ofMinutes(5));
+ when(criteriaToInvalidContainerLifetimeStrategy.apply(any())).thenReturn(Duration.ofMinutes(5));
+ final EntityConfigurationContainer ecContainer =
+ ecContainer("mockEntityId", CollectionSupport.emptyList());
+ when(entityConfigurationCache.get(any())).thenReturn(CollectionSupport.listOf(ecContainer));
+ Assert.assertEquals(function.apply(new CriteriaSet()), null);
+ }
+
+ @Test
+ public void entityConfigurationOneUnresolvableHint_returnsEmptyContainer() throws MetadataCacheException {
+ when(criteriaToValidContainerLifetimeStrategy.apply(any())).thenReturn(Duration.ofMinutes(5));
+ when(criteriaToInvalidContainerLifetimeStrategy.apply(any())).thenReturn(Duration.ofMinutes(5));
+ final EntityConfigurationContainer ecContainer =
+ ecContainer("mockEntityId", CollectionSupport.listOf("https://federation.local/immediate"));
+ when(entityConfigurationCache.get(any())).thenReturn(CollectionSupport.listOf(ecContainer));
+ assertEmptyResult(function.apply(new CriteriaSet()));
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void entityConfigurationOneResolvableHint_returnsContainerWithOneChain() throws MetadataCacheException {
+ final String leaf = "https://federation.local/leaf";
+ final String anchor = "https://federation.local/immediate";
+ when(criteriaToValidContainerLifetimeStrategy.apply(any())).thenReturn(Duration.ofMinutes(5));
+ when(criteriaToInvalidContainerLifetimeStrategy.apply(any())).thenReturn(Duration.ofMinutes(5));
+ final EntityConfigurationContainer ecContainer =
+ ecContainer(leaf, CollectionSupport.listOf(anchor, "https://federation.local/other"));
+ final EntityConfigurationContainer authorityContainer =
+ ecContainer(anchor, CollectionSupport.emptyList());
+ when(entityConfigurationCache.get(any()))
+ .thenReturn(CollectionSupport.listOf(ecContainer), CollectionSupport.listOf(authorityContainer),
+ CollectionSupport.emptyList());
+ final SubordinateStatementContainer ssContainer = ssContainer(leaf, anchor);
+ when(subordinateStatementCache.get(any())).thenReturn(CollectionSupport.listOf(ssContainer),
+ CollectionSupport.emptyList());
+ final TrustChainsContainer result = function.apply(new CriteriaSet());
+ Assert.assertNotNull(result);
+ assert result != null;
+ Assert.assertEquals(result.getTrustChains().size(), 1);
+ Assert.assertEquals(EntityStatementHelper.getEntityIds(result.getTrustChains().get(0)),
+ List.of(leaf, anchor));
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void entityConfigurationOneResolvableLocalAuthorityWithBrokenHint_returnsContainerWithOneChain()
+ throws MetadataCacheException {
+ final String leaf = "https://federation.local/leaf";
+ final String immediate = "https://federation.local/immediate";
+ final String anchor = "https://federation.local/anchor";
+ when(criteriaToValidContainerLifetimeStrategy.apply(any())).thenReturn(Duration.ofMinutes(5));
+ when(criteriaToInvalidContainerLifetimeStrategy.apply(any())).thenReturn(Duration.ofMinutes(5));
+ final EntityConfigurationContainer ecContainer =
+ ecContainer(leaf, CollectionSupport.listOf(immediate, "https://federation.local/other"));
+ final EntityConfigurationContainer immediateContainer =
+ ecContainer(immediate, CollectionSupport.listOf(anchor));
+ when(entityConfigurationCache.get(any()))
+ .thenReturn(CollectionSupport.listOf(ecContainer), CollectionSupport.listOf(immediateContainer),
+ CollectionSupport.emptyList());
+ final SubordinateStatementContainer ssContainer = ssContainer(leaf, immediate);
+ when(subordinateStatementCache.get(any())).thenReturn(CollectionSupport.listOf(ssContainer),
+ CollectionSupport.emptyList());
+ when(localTrustAnchorsCache.get(any())).thenReturn(
+ CollectionSupport.listOf(CollectionSupport.singletonMap(immediate, mock(LocalKeyContainer.class))));
+ final TrustChainsContainer result = function.apply(new CriteriaSet());
+ Assert.assertNotNull(result);
+ assert result != null;
+ Assert.assertEquals(result.getTrustChains().size(), 1);
+ Assert.assertEquals(EntityStatementHelper.getEntityIds(result.getTrustChains().get(0)),
+ List.of(leaf, immediate));
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void entityConfigurationOneResolvableLocalAuthorityWithWorkingHint_returnsContainerWithTwoChains()
+ throws MetadataCacheException {
+ final String leaf = "https://federation.local/leaf";
+ final String immediate = "https://federation.local/immediate";
+ final String anchor = "https://federation.local/anchor";
+ when(criteriaToValidContainerLifetimeStrategy.apply(any())).thenReturn(Duration.ofMinutes(5));
+ when(criteriaToInvalidContainerLifetimeStrategy.apply(any())).thenReturn(Duration.ofMinutes(5));
+ final EntityConfigurationContainer ecContainer =
+ ecContainer(leaf, CollectionSupport.listOf(immediate));
+ final EntityConfigurationContainer immediateContainer =
+ ecContainer(immediate, CollectionSupport.listOf(anchor));
+ final EntityConfigurationContainer authorityContainer =
+ ecContainer(anchor, CollectionSupport.emptyList());
+ when(entityConfigurationCache.get(any()))
+ .thenReturn(CollectionSupport.listOf(ecContainer), CollectionSupport.listOf(immediateContainer),
+ CollectionSupport.listOf(authorityContainer), CollectionSupport.emptyList());
+ final SubordinateStatementContainer ssContainer1 = ssContainer(leaf, immediate);
+ final SubordinateStatementContainer ssContainer2 = ssContainer(immediate, anchor);
+ when(subordinateStatementCache.get(any())).thenReturn(CollectionSupport.listOf(ssContainer1),
+ CollectionSupport.listOf(ssContainer2), CollectionSupport.emptyList());
+ when(localTrustAnchorsCache.get(any())).thenReturn(
+ CollectionSupport.listOf(CollectionSupport.singletonMap(immediate, mock(LocalKeyContainer.class))),
+ CollectionSupport.emptyList());
+ final TrustChainsContainer result = function.apply(new CriteriaSet());
+ Assert.assertNotNull(result);
+ assert result != null;
+ Assert.assertEquals(result.getTrustChains().size(), 2);
+ Assert.assertEquals(EntityStatementHelper.getEntityIds(result.getTrustChains().get(0)),
+ List.of(leaf, immediate));
+ Assert.assertEquals(EntityStatementHelper.getEntityIds(result.getTrustChains().get(1)),
+ List.of(leaf, immediate, anchor));
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void entityConfigurationOneResolvableHint_matchPreSelected_returnsContainerWithOneChain()
+ throws MetadataCacheException {
+ final String leaf = "https://federation.local/leaf";
+ final String anchor = "https://federation.local/immediate";
+ final PreSelectedTrustChainCriterion preSelectedCriterion =
+ new PreSelectedTrustChainCriterion(CollectionSupport.listOf(leaf, anchor));
+ when(criteriaToValidContainerLifetimeStrategy.apply(any())).thenReturn(Duration.ofMinutes(5));
+ when(criteriaToInvalidContainerLifetimeStrategy.apply(any())).thenReturn(Duration.ofMinutes(5));
+ final EntityConfigurationContainer ecContainer =
+ ecContainer(leaf, CollectionSupport.listOf(anchor, "https://federation.local/other"));
+ final EntityConfigurationContainer authorityContainer =
+ ecContainer(anchor, CollectionSupport.emptyList());
+ when(entityConfigurationCache.get(any()))
+ .thenReturn(CollectionSupport.listOf(ecContainer), CollectionSupport.listOf(authorityContainer),
+ CollectionSupport.emptyList());
+ final SubordinateStatementContainer ssContainer = ssContainer(leaf, anchor);
+ when(subordinateStatementCache.get(any())).thenReturn(CollectionSupport.listOf(ssContainer),
+ CollectionSupport.emptyList());
+ final TrustChainsContainer result = function.apply(new CriteriaSet(preSelectedCriterion));
+ Assert.assertNotNull(result);
+ assert result != null;
+ Assert.assertEquals(result.getTrustChains().size(), 1);
+ Assert.assertEquals(EntityStatementHelper.getEntityIds(result.getTrustChains().get(0)),
+ List.of(leaf, anchor));
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void entityConfigurationOneResolvableHint_notMatchingPreSelected_returnsEmptyContainer()
+ throws MetadataCacheException {
+ final String leaf = "https://federation.local/leaf";
+ final String anchor = "https://federation.local/immediate";
+ final PreSelectedTrustChainCriterion preSelectedCriterion =
+ new PreSelectedTrustChainCriterion(CollectionSupport.listOf(leaf, "https://federation.local/not"));
+ when(criteriaToValidContainerLifetimeStrategy.apply(any())).thenReturn(Duration.ofMinutes(5));
+ when(criteriaToInvalidContainerLifetimeStrategy.apply(any())).thenReturn(Duration.ofMinutes(5));
+ final EntityConfigurationContainer ecContainer =
+ ecContainer(leaf, CollectionSupport.listOf(anchor, "https://federation.local/other"));
+ final EntityConfigurationContainer authorityContainer =
+ ecContainer(anchor, CollectionSupport.emptyList());
+ when(entityConfigurationCache.get(any()))
+ .thenReturn(CollectionSupport.listOf(ecContainer), CollectionSupport.listOf(authorityContainer),
+ CollectionSupport.emptyList());
+ final SubordinateStatementContainer ssContainer = ssContainer(leaf, anchor);
+ when(subordinateStatementCache.get(any())).thenReturn(CollectionSupport.listOf(ssContainer),
+ CollectionSupport.emptyList());
+ assertEmptyResult(function.apply(new CriteriaSet(preSelectedCriterion)));
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void entityConfigurationTwoResolvableHints_returnsContainerWithTwoChains() throws MetadataCacheException {
+ final String leaf = "https://federation.local/leaf";
+ final String anchor1 = "https://federation.local/immediate1";
+ final String anchor2 = "https://federation.local/immediate2";
+ when(criteriaToValidContainerLifetimeStrategy.apply(any())).thenReturn(Duration.ofMinutes(5));
+ when(criteriaToInvalidContainerLifetimeStrategy.apply(any())).thenReturn(Duration.ofMinutes(5));
+ final EntityConfigurationContainer ecContainer =
+ ecContainer(leaf, CollectionSupport.listOf(anchor1, anchor2));
+ final EntityConfigurationContainer authorityContainer1 =
+ ecContainer(anchor1, CollectionSupport.emptyList());
+ final EntityConfigurationContainer authorityContainer2 =
+ ecContainer(anchor2, CollectionSupport.emptyList());
+ when(entityConfigurationCache.get(any()))
+ .thenReturn(CollectionSupport.listOf(ecContainer), CollectionSupport.listOf(authorityContainer1),
+ CollectionSupport.listOf(authorityContainer2));
+ final SubordinateStatementContainer ssContainer1 = ssContainer(leaf, anchor1);
+ final SubordinateStatementContainer ssContainer2 = ssContainer(leaf, anchor2);
+ when(subordinateStatementCache.get(any())).thenReturn(CollectionSupport.listOf(ssContainer1),
+ CollectionSupport.listOf(ssContainer2));
+ final TrustChainsContainer result = function.apply(new CriteriaSet());
+ Assert.assertNotNull(result);
+ assert result != null;
+ Assert.assertEquals(result.getTrustChains().size(), 2);
+ Assert.assertEquals(EntityStatementHelper.getEntityIds(result.getTrustChains().get(0)),
+ List.of(leaf, anchor1));
+ Assert.assertEquals(EntityStatementHelper.getEntityIds(result.getTrustChains().get(1)),
+ List.of(leaf, anchor2));
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void entityConfigurationAuthorityLoop_returnsEmptyContantainer() throws MetadataCacheException {
+ final String leaf = "https://federation.local/leaf";
+ final String anchor1 = "https://federation.local/immediate1";
+ final String anchor2 = "https://federation.local/immediate2";
+ when(criteriaToValidContainerLifetimeStrategy.apply(any())).thenReturn(Duration.ofMinutes(5));
+ when(criteriaToInvalidContainerLifetimeStrategy.apply(any())).thenReturn(Duration.ofMinutes(5));
+ final EntityConfigurationContainer ecContainer =
+ ecContainer(leaf, CollectionSupport.listOf(anchor1));
+ final EntityConfigurationContainer authorityContainer1 =
+ ecContainer(anchor1, CollectionSupport.listOf(anchor2));
+ final EntityConfigurationContainer authorityContainer2 =
+ ecContainer(anchor2, CollectionSupport.listOf(anchor1));
+ when(entityConfigurationCache.get(any()))
+ .thenReturn(CollectionSupport.listOf(ecContainer), CollectionSupport.listOf(authorityContainer1),
+ CollectionSupport.listOf(authorityContainer2));
+ final SubordinateStatementContainer ssContainer1 = ssContainer(leaf, anchor1);
+ final SubordinateStatementContainer ssContainer2 = ssContainer(anchor1, anchor2);
+ final SubordinateStatementContainer ssContainer3 = ssContainer(anchor2, anchor1);
+ when(subordinateStatementCache.get(any())).thenReturn(CollectionSupport.listOf(ssContainer1),
+ CollectionSupport.listOf(ssContainer2), CollectionSupport.listOf(ssContainer3));
+ assertEmptyResult(function.apply(new CriteriaSet()));
+ }
+
+ protected void assertEmptyResult(final TrustChainsContainer container) {
+ Assert.assertNotNull(container);
+ Assert.assertEquals(container.getTrustChains(), CollectionSupport.emptyList());
+ }
+
+ protected EntityConfigurationContainer ecContainer(final String id, final List<String> authorityHints) {
+ final Instant expiration = Instant.now().plusSeconds(300);
+ final EntityConfiguration configuration = mock(EntityConfiguration.class);
+ final EntityConfigurationPayload payload = mock(EntityConfigurationPayload.class);
+ when(payload.getAuthorityHints()).thenReturn(authorityHints);
+ when(configuration.getParsedPayload()).thenReturn(payload);
+ when(configuration.getSubject()).thenReturn(id);
+ return new EntityConfigurationContainer(id, configuration, expiration, expiration);
+ }
+
+ protected SubordinateStatementContainer ssContainer(final String subject, final String issuer) {
+ final Instant expiration = Instant.now().plusSeconds(300);
+ final SubordinateStatement statement = mock(SubordinateStatement.class);
+ when(statement.getSubject()).thenReturn(subject);
+ when(statement.getIssuer()).thenReturn(issuer);
+ return new SubordinateStatementContainer(
+ new SubordinateStatementCacheIdentifier(issuer, subject), statement, expiration, expiration);
+ }
+}
diff --git a/oidfed-common-conf-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/AbstractFederationFlowTest.java b/oidfed-common-conf-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/AbstractFederationFlowTest.java
new file mode 100644
index 0000000..00f8742
--- /dev/null
+++ b/oidfed-common-conf-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/AbstractFederationFlowTest.java
@@ -0,0 +1,839 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.profile.flow.oidfed;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URLEncoder;
+import java.nio.charset.Charset;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.NoSuchAlgorithmException;
+import java.security.interfaces.RSAPublicKey;
+import java.time.Instant;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import javax.annotation.Nonnull;
+
+import org.apache.hc.client5.http.classic.HttpClient;
+import org.apache.hc.core5.http.ClassicHttpRequest;
+import org.apache.hc.core5.http.ClassicHttpResponse;
+import org.apache.hc.core5.http.HttpEntity;
+import org.mockito.ArgumentMatcher;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.core.io.Resource;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.webflow.executor.FlowExecutionResult;
+import org.springframework.webflow.test.MockExternalContext;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.JWSHeader;
+import com.nimbusds.jose.crypto.RSASSASigner;
+import com.nimbusds.jose.jwk.JWK;
+import com.nimbusds.jose.jwk.JWKSet;
+import com.nimbusds.jose.jwk.RSAKey;
+import com.nimbusds.jwt.JWT;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.PlainJWT;
+import com.nimbusds.jwt.SignedJWT;
+import com.nimbusds.oauth2.sdk.ErrorResponse;
+import com.nimbusds.oauth2.sdk.ParseException;
+import com.nimbusds.oauth2.sdk.Response;
+import com.nimbusds.oauth2.sdk.id.Issuer;
+import com.nimbusds.openid.connect.sdk.SubjectType;
+import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientMetadata;
+
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import net.minidev.json.JSONObject;
+import net.shibboleth.idp.test.flows.AbstractFlowTest;
+import net.shibboleth.oidc.security.credential.BasicJWKCredential;
+import net.shibboleth.oidc.security.credential.BasicJWKCredentialFactoryBean;
+import net.shibboleth.oidfed.metadata.EntityStatement;
+import net.shibboleth.oidfed.test.TrustChainTestUtil;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.servlet.impl.HttpServletRequestResponseContext;
+
+/**
+ * Abstract unit test for the flows supporting OpenID federation.
+ */
+ at ContextConfiguration(locations = { "classpath*:/META-INF/net.shibboleth.idp/postconfig.xml",})
+public class AbstractFederationFlowTest extends AbstractFlowTest {
+
+ public static final String USE_CUSTOM_RESOLVER_API_CONDITION = "useCustomResolverApi";
+ public static final String USE_CUSTOM_FAILBACK_TO_LOCAL_CONDITION = "useCustomFallbackToLocal";
+
+ final static AtomicInteger clientIndex = new AtomicInteger();
+ final static AtomicInteger intermediateIndex = new AtomicInteger();
+
+ @Nonnull public static final String END_STATE_ID = "CommitResponse";
+ @Nonnull protected final String flowId;
+ @Nonnull protected final String endStateId;
+
+ protected final String redirectUri = "https://rp.federation.local/cb";
+ protected final String clientIdPattern = "https://testrp%s.federation.local";
+ protected final String intermediateIdPattern = "https://intermediate-authority%s.federation.local";
+ protected final String trustedIntermediateId = "https://local-trusted-intermediate-authority.federation.local";
+ protected final String anchorId = "https://trust-anchor.federation.local";
+ protected final String anchorFetchEndpoint = anchorId + "/fetch";
+ protected final String anchorResolveEndpoint = anchorId + "/resolve";
+ protected final String trustMarkIssuerId = "https://trust-mark-issuer.federation.local";
+ protected final String trustMarkEndpoint = "https://trust-mark-issuer.federation.local/issue";
+ protected final String trustMarkStatusEndpoint = "https://trust-mark-issuer.federation.local/status";
+ protected final String issuer = "https://op.example.org";
+
+ protected static JWK rpKey;
+ protected static JWK leafKey;
+ protected static JWK anchorKey;
+ protected static JWK trustedAnchorKey;
+ protected static JWK intermediateKey;
+ protected static JWK trustedIntermediateKey;
+ protected static JWK trustMarkIssuerKey;
+
+ protected String subject = "jdoe";
+
+ @Autowired
+ @Qualifier("shibboleth.oidfed.HttpClient")
+ protected HttpClient federationHttpClient;
+
+ static {
+ try {
+ rpKey = initializeNewJwk("RSA", 2048, "mockRpKey");
+ leafKey = initializeNewJwk("RSA", 2048, "mockLeafKey");
+ anchorKey = initializeNewJwk("RSA", 2048, "mockAnchorKey");
+ final BasicJWKCredential localAnchor = loadCredential("/credentials/fed-local-anchor.jwk");
+ trustedAnchorKey = new RSAKey.Builder((RSAPublicKey) localAnchor.getPublicKey())
+ .privateKey(localAnchor.getPrivateKey())
+ .keyID("locallyTrustedAnchorKey")
+ .build();
+ final BasicJWKCredential localIntermediate = loadCredential("/credentials/fed-local-intermediate.jwk");
+ trustedIntermediateKey = new RSAKey.Builder((RSAPublicKey) localIntermediate.getPublicKey())
+ .privateKey(localIntermediate.getPrivateKey())
+ .keyID("locallyTrustedIntermediateKey")
+ .build();
+ intermediateKey = initializeNewJwk("RSA", 2048, "mockIntermediateKey");
+ trustMarkIssuerKey = initializeNewJwk("RSA", 2048, "mockTrustMarkIssuerKey");
+ } catch (final NoSuchAlgorithmException e) {
+ Assert.fail("Could not initialize keys for the tests", e);
+ }
+ }
+
+ protected AbstractFederationFlowTest(@Nonnull final String id) {
+ this(id, END_STATE_ID);
+ }
+
+ protected AbstractFederationFlowTest(@Nonnull final String id, @Nonnull final String endId) {
+ flowId = Constraint.isNotEmpty(id, "Flow ID cannot be empty");
+ endStateId = Constraint.isNotEmpty(endId, "End state ID cannot be empty");
+ }
+
+ /**
+ * Initialize mock request, response, and external context. Overrides to remove authorization header.
+ */
+ @Override
+ @BeforeMethod public void initializeMocks() {
+ overrideEndStateOutput(flowId, endStateId);
+
+ request = new MockHttpServletRequest();
+ response = new MockHttpServletResponse();
+ externalContext = new MockExternalContext();
+ externalContext.setNativeRequest(request);
+ externalContext.setNativeResponse(response);
+ }
+
+ /**
+ * {@link HttpServletRequestResponseContext#loadCurrent(HttpServletRequest, HttpServletResponse)}
+ */
+ @SuppressWarnings("null")
+ @Override
+ @BeforeMethod public void initializeThreadLocals() {
+ HttpServletRequestResponseContext.loadCurrent(request, response);
+ }
+
+ @BeforeMethod
+ public void removeHeaders() {
+ request.removeHeader(USE_CUSTOM_RESOLVER_API_CONDITION);
+ request.removeHeader(USE_CUSTOM_FAILBACK_TO_LOCAL_CONDITION);
+ }
+
+ protected static JWK initializeNewJwk(final String algorithm, final int size, final String kid)
+ throws NoSuchAlgorithmException {
+ if ("RSA".equals(algorithm)) {
+ final KeyPairGenerator keyGen = KeyPairGenerator.getInstance(algorithm);
+ keyGen.initialize(size);
+ final KeyPair keyPair = keyGen.generateKeyPair();
+ return new RSAKey.Builder((RSAPublicKey) keyPair.getPublic())
+ .privateKey(keyPair.getPrivate())
+ .keyID(kid)
+ .build();
+ }
+ throw new NoSuchAlgorithmException(algorithm);
+ }
+
+ protected JWT plainRequestObject(final Map<String,Object> claims) {
+ final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder();
+ for (final String claim : claims.keySet()) {
+ builder.claim(claim, claims.get(claim));
+ }
+ return new PlainJWT(builder.build());
+ }
+
+ protected JWT signedRequestObject(final Map<String,Object> claims) {
+ return signedRequestObject(claims, rpKey);
+ }
+
+ protected JWT signedRequestObject(final Map<String,Object> claims, final JWK jwk) {
+ return signedRequestObject(claims, new JWSHeader.Builder(JWSAlgorithm.RS256).keyID(jwk.getKeyID()).build(),
+ jwk);
+ }
+
+ protected JWT signedRequestObject(final Map<String,Object> claims, final JWSHeader header, final JWK jwk) {
+ final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder();
+ for (final String claim : claims.keySet()) {
+ builder.claim(claim, claims.get(claim));
+ }
+ try {
+ final SignedJWT jwt = new SignedJWT(header, builder.build());
+ final RSASSASigner signer = new RSASSASigner(jwk.toRSAKey());
+ jwt.sign(signer);
+ return jwt;
+ } catch (JOSEException e) {
+ Assert.fail(e.getMessage(), e);
+ }
+ return null;
+ }
+
+ protected String entityConfigurationUrl(final String entityId) {
+ return entityId + "/.well-known/openid-federation";
+ }
+
+ protected String subordinateStatementUrl(final String fetchEndpoint, final String subject) {
+ return fetchEndpoint + "?sub=" + URLEncoder.encode(subject, Charset.forName("UTF-8"));
+ }
+
+ protected String resolveEntityUrl(final String resolveEndpoint, final String subject,
+ final String... trustAnchors) {
+ final StringBuilder builder = new StringBuilder(resolveEndpoint + "?sub=" +
+ URLEncoder.encode(subject, Charset.forName("UTF-8")));
+ for (final String trustAnchor : trustAnchors) {
+ builder.append("&trust_anchor=" + URLEncoder.encode(trustAnchor, Charset.forName("UTF-8")));
+ }
+ builder.append("&entity_type=openid_relying_party");
+ return builder.toString();
+ }
+
+ protected void mapResponse(final String requestUri, final ClassicHttpResponse classicResponse) throws IOException {
+ when(federationHttpClient.executeOpen(any(), argThat(new RequestUriMatcher(requestUri)), any()))
+ .thenReturn(classicResponse);
+ }
+
+ protected ClassicHttpResponse mockResponse(final String contents)
+ throws UnsupportedOperationException, IOException {
+ return mockResponse(200, "application/entity-statement+jwt", contents);
+ }
+
+ protected ClassicHttpResponse mockResponse(final int code, final String contentType, final String contents)
+ throws UnsupportedOperationException, IOException {
+ final ClassicHttpResponse classicResponse = mock(ClassicHttpResponse.class);
+ when(classicResponse.getCode()).thenReturn(code);
+ final HttpEntity responseEntity = mock(HttpEntity.class);
+ when(responseEntity.getContentType()).thenReturn(contentType);
+ when(responseEntity.getContent()).thenReturn(new ByteArrayInputStream(contents.getBytes()));
+ when(classicResponse.getEntity()).thenReturn(responseEntity);
+ return classicResponse;
+ }
+
+ protected String rpEntityConfiguration(final String clientId, final String... authorityHints)
+ throws URISyntaxException {
+ final OIDCClientMetadata metadata = new OIDCClientMetadata();
+ metadata.setRedirectionURI(new URI(redirectUri));
+ metadata.setJWKSet(new JWKSet(rpKey.toPublicJWK()));
+ return rpEntityConfiguration(clientId, metadata, authorityHints);
+ }
+
+ protected String rpEntityConfiguration(final String clientId, final JWK leafKey,
+ final String... authorityHints) throws URISyntaxException {
+ final OIDCClientMetadata metadata = new OIDCClientMetadata();
+ metadata.setRedirectionURI(new URI(redirectUri));
+ metadata.setJWKSet(new JWKSet(rpKey.toPublicJWK()));
+ return rpEntityConfiguration(clientId, metadata, null, leafKey, authorityHints);
+ }
+
+ protected String rpEntityConfiguration(final String clientId, final OIDCClientMetadata metadata,
+ final String... authorityHints) throws URISyntaxException {
+ return rpEntityConfiguration(clientId, metadata, null, leafKey, authorityHints);
+ }
+
+ protected String rpEntityConfiguration(final String clientId, final OIDCClientMetadata metadata,
+ final List<Map<String, String>> trustMarks, final JWK leafKey, final String... authorityHints)
+ throws URISyntaxException {
+ return rpEntityConfiguration(clientId, metadata, trustMarks, null, leafKey, authorityHints);
+ }
+
+ protected String rpEntityConfiguration(final String clientId, final OIDCClientMetadata metadata,
+ final List<Map<String, String>> trustMarks, final List<String> crit, final JWK leafKey,
+ final String... authorityHints) throws URISyntaxException {
+ final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(clientId).subject(clientId)
+ .issueTime(Date.from(Instant.now()))
+ .expirationTime(Date.from(Instant.now().plusSeconds(300)))
+ .claim("jwks", new JWKSet(leafKey).toJSONObject(true))
+ .claim("metadata", Map.of("openid_relying_party", metadata.toJSONObject()))
+ .claim("authority_hints", authorityHints == null || authorityHints.length == 0 ?
+ new String[] { anchorId } : authorityHints);
+ if (trustMarks != null) {
+ builder.claim("trust_marks", trustMarks);
+ }
+ if (crit != null) {
+ builder.claim("crit", crit);
+ }
+ final EntityStatement<?> rpConfiguration =
+ TrustChainTestUtil.entityStatement(JWSAlgorithm.RS256, leafKey, builder.build());
+ return rpConfiguration.getJwt().serialize();
+ }
+
+ protected String rpEntityConfigurationUnmatchingKey(final String clientId,
+ final OIDCClientMetadata metadata, final String... authorityHints) throws URISyntaxException {
+ final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder().issuer(clientId).subject(clientId)
+ .issueTime(Date.from(Instant.now()))
+ .expirationTime(Date.from(Instant.now().plusSeconds(300)))
+ .claim("jwks", new JWKSet(leafKey).toJSONObject(true))
+ .claim("metadata", Map.of("openid_relying_party", metadata.toJSONObject()))
+ .claim("authority_hints", authorityHints == null || authorityHints.length == 0 ?
+ new String[] { anchorId } : authorityHints)
+ .build();
+ final EntityStatement<?> rpConfiguration =
+ TrustChainTestUtil.entityStatement(JWSAlgorithm.RS256, anchorKey, claimsSet);
+ return rpConfiguration.getJwt().serialize();
+ }
+
+ protected String trustMarkIssuerConfiguration(final String entityId, final String... authorityHints) {
+ final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder().issuer(entityId).subject(entityId)
+ .issueTime(Date.from(Instant.now()))
+ .expirationTime(Date.from(Instant.now().plusSeconds(300)))
+ .claim("jwks", new JWKSet(trustMarkIssuerKey).toJSONObject(true))
+ .claim("authority_hints", authorityHints == null || authorityHints.length == 0 ?
+ new String[] { anchorId } : authorityHints)
+ .claim("metadata", Map.of("federation_entity", Map.of("trust_mark_endpoint",
+ trustMarkEndpoint, "federation_trust_mark_status_endpoint", trustMarkStatusEndpoint)))
+ .build();
+ final EntityStatement<?> rpConfiguration =
+ TrustChainTestUtil.entityStatement(JWSAlgorithm.RS256, trustMarkIssuerKey, claimsSet);
+ return rpConfiguration.getJwt().serialize();
+ }
+
+ protected String opEntityConfiguration(final String issuer, final String... authorityHints)
+ throws URISyntaxException {
+ return opEntityConfiguration(issuer, emptyOpMetadata(issuer), authorityHints);
+ }
+
+ protected String opEntityConfiguration(final String issuer, final OIDCProviderMetadata metadata,
+ final String... authorityHints) throws URISyntaxException {
+ final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder().issuer(issuer).subject(issuer)
+ .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", metadata.toJSONObject()))
+ .claim("authority_hints", authorityHints == null || authorityHints.length == 0 ?
+ new String[] { anchorId } : authorityHints)
+ .build();
+ final EntityStatement<?> rpConfiguration =
+ TrustChainTestUtil.entityStatement(JWSAlgorithm.RS256, leafKey, claimsSet);
+ return rpConfiguration.getJwt().serialize();
+ }
+
+ protected String entityConfiguration(final String entityId, final Map<String, Object> metadata,
+ final String... authorityHints) {
+ final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder().issuer(entityId).subject(entityId)
+ .issueTime(Date.from(Instant.now()))
+ .expirationTime(Date.from(Instant.now().plusSeconds(300)))
+ .claim("jwks", new JWKSet(leafKey).toJSONObject(true))
+ .claim("metadata", metadata)
+ .claim("authority_hints", authorityHints == null || authorityHints.length == 0 ?
+ new String[] { anchorId } : authorityHints)
+ .build();
+ final EntityStatement<?> configuration =
+ TrustChainTestUtil.entityStatement(JWSAlgorithm.RS256, leafKey, claimsSet);
+ return configuration.getJwt().serialize();
+ }
+
+ protected String trustedAnchorConfiguration() {
+ return trustedAnchorConfiguration(null);
+ }
+
+ protected String trustedAnchorConfiguration(final Map<String, Object> constraints) {
+ return trustedAnchorConfiguration(constraints,
+ Map.of("https://example.org/email-allowing-trust-mark", List.of(trustMarkIssuerId)));
+ }
+
+ protected String trustedAnchorConfiguration(final Map<String, Object> constraints,
+ final Map<String,List<String>> trustMarkIssuers) {
+ return trustedAnchorConfiguration(constraints, trustMarkIssuers, trustedAnchorKey,
+ new JWKSet(trustedAnchorKey));
+ }
+
+ protected String trustedAnchorConfiguration(final Map<String, Object> constraints,
+ final Map<String,List<String>> trustMarkIssuers, final JWK signerKey, final JWKSet jwks) {
+
+ final String anchorId = "https://trust-anchor.federation.local";
+ final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(anchorId).subject(anchorId)
+ .issueTime(Date.from(Instant.now()))
+ .expirationTime(Date.from(Instant.now().plusSeconds(300)))
+ .claim("jwks", jwks.toJSONObject(true))
+ .claim("metadata", Map.of("federation_entity", Map.of("federation_fetch_endpoint",
+ anchorFetchEndpoint, "federation_resolve_endpoint", anchorResolveEndpoint)));
+ if (constraints != null) {
+ builder.claim("constraints", constraints);
+ }
+ if (trustMarkIssuers != null) {
+ builder.claim("trust_mark_issuers", trustMarkIssuers);
+ }
+ final EntityStatement<?> anchorConfiguration =
+ TrustChainTestUtil.entityStatement(JWSAlgorithm.RS256, signerKey, builder.build());
+ return anchorConfiguration.getJwt().serialize();
+ }
+
+ protected String intermediateConfiguration(final String intermediateId) {
+ return intermediateConfiguration(intermediateId, new JWKSet(intermediateKey), intermediateKey);
+ }
+
+ protected String intermediateConfiguration(final String intermediateId, final JWKSet jwks, final JWK signerKey) {
+ final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder().issuer(intermediateId).subject(intermediateId)
+ .issueTime(Date.from(Instant.now()))
+ .expirationTime(Date.from(Instant.now().plusSeconds(300)))
+ .claim("jwks", jwks.toJSONObject(true))
+ .claim("metadata", Map.of("federation_entity", Map.of("federation_fetch_endpoint",
+ intermediateId + "/fetch")))
+ .claim("authority_hints", new String[] { anchorId })
+ .build();
+ final EntityStatement<?> intermediateConfiguration =
+ TrustChainTestUtil.entityStatement(JWSAlgorithm.RS256, signerKey, claimsSet);
+ return intermediateConfiguration.getJwt().serialize();
+ }
+
+ protected String subordinateStatement(final String issuer, final Map<String, Object> metadata) {
+ return subordinateStatement(issuer, metadata, leafKey);
+ }
+
+ protected String subordinateStatement(final String issuer, final Map<String, Object> metadata,
+ final List<String> metadataPolicyCrit, final List<String> crit) {
+ return subordinateStatement(issuer, metadata, metadataPolicyCrit, crit, leafKey);
+ }
+
+ protected String subordinateStatement(final String issuer, final Map<String, Object> metadata,
+ final JWK subjectKey) {
+ return subordinateStatement(issuer, metadata, null, null, subjectKey);
+ }
+
+ protected String subordinateStatement(final String issuer, final Map<String, Object> metadata,
+ final List<String> metadataPolicyCrit, final List<String> crit, final JWK subjectKey) {
+ final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder().issuer(anchorId).subject(issuer)
+ .issueTime(Date.from(Instant.now()))
+ .expirationTime(Date.from(Instant.now().plusSeconds(300)))
+ .claim("jwks", new JWKSet(subjectKey).toJSONObject(true))
+ .claim("metadata", metadata)
+ .claim("metadata_policy_crit", metadataPolicyCrit)
+ .claim("crit", crit)
+ .build();
+ final EntityStatement<?> rpConfiguration =
+ TrustChainTestUtil.entityStatement(JWSAlgorithm.RS256, trustedAnchorKey, claimsSet);
+ return rpConfiguration.getJwt().serialize();
+ }
+
+ protected String rpSubordinateStatement(final String issuer, final JWK issuerKey, final JWK subjetKey,
+ final String subjectId, final Map<String, Object> rpPolicy, final String... authorityHints) {
+ final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder().issuer(issuer).subject(subjectId)
+ .issueTime(Date.from(Instant.now()))
+ .expirationTime(Date.from(Instant.now().plusSeconds(300)))
+ .claim("jwks", new JWKSet(subjetKey).toJSONObject(true))
+ .claim("metadata", Map.of("openid_relying_party", new OIDCClientMetadata().toJSONObject()))
+ .claim("metadata_policy", rpPolicy)
+ .build();
+ final EntityStatement<?> rpConfiguration =
+ TrustChainTestUtil.entityStatement(JWSAlgorithm.RS256, issuerKey, claimsSet);
+ return rpConfiguration.getJwt().serialize();
+ }
+
+ protected String subordinateStatement(final String issuer, final JWK issuerKey, final JWK subjetKey,
+ final String subjectId, final Map<String, Object> rpPolicy, final Map<String, Object> constraints) {
+ final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder().issuer(issuer).subject(subjectId)
+ .issueTime(Date.from(Instant.now()))
+ .expirationTime(Date.from(Instant.now().plusSeconds(300)))
+ .claim("jwks", new JWKSet(subjetKey).toJSONObject(true))
+ .claim("metadata", Map.of("openid_relying_party", new OIDCClientMetadata().toJSONObject()))
+ .claim("metadata_policy", rpPolicy)
+ .claim("constraints", constraints)
+ .build();
+ final EntityStatement<?> rpConfiguration =
+ TrustChainTestUtil.entityStatement(JWSAlgorithm.RS256, issuerKey, claimsSet);
+ return rpConfiguration.getJwt().serialize();
+ }
+
+ protected String uniqueClientId() {
+ return String.format(clientIdPattern, clientIndex.getAndIncrement());
+ }
+
+ protected String uniqueIntermediateId() {
+ return String.format(intermediateIdPattern, intermediateIndex.getAndIncrement());
+ }
+
+ protected void rpConfigureMockHttpClient(final String clientId) {
+ try {
+ mapResponse(entityConfigurationUrl(clientId), mockResponse(rpEntityConfiguration(clientId)));
+ mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+ mapResponse(subordinateStatementUrl(anchorFetchEndpoint, clientId),
+ mockResponse(subordinateStatement(clientId, Map.of("openid_relying_party",
+ new OIDCClientMetadata().toJSONObject()))));
+ } catch (UnsupportedOperationException | IOException | URISyntaxException e) {
+ Assert.fail("Could not initialize mock HTTP client", e);
+ }
+ }
+
+ protected void rpConfigureMockHttpClient(final String clientId, final URI jwksUri) {
+ final OIDCClientMetadata metadata = new OIDCClientMetadata();
+ try {
+ metadata.setRedirectionURI(new URI(redirectUri));
+ metadata.setJWKSetURI(jwksUri);
+ mapResponse(entityConfigurationUrl(clientId), mockResponse(rpEntityConfiguration(clientId, metadata)));
+ mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+ mapResponse(subordinateStatementUrl(anchorFetchEndpoint, clientId),
+ mockResponse(subordinateStatement(clientId, Map.of("openid_relying_party",
+ new OIDCClientMetadata().toJSONObject()))));
+ } catch (UnsupportedOperationException | IOException | URISyntaxException e) {
+ Assert.fail("Could not initialize mock HTTP client", e);
+ }
+ }
+
+ protected void rpConfigureMockHttpClient(final String clientId, final JWK leafKey) {
+ try {
+ mapResponse(entityConfigurationUrl(clientId),
+ mockResponse(rpEntityConfiguration(clientId, leafKey)));
+ mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+ mapResponse(subordinateStatementUrl(anchorFetchEndpoint, clientId),
+ mockResponse(subordinateStatement(clientId, Map.of("openid_relying_party",
+ new OIDCClientMetadata().toJSONObject()))));
+ } catch (UnsupportedOperationException | IOException | URISyntaxException e) {
+ Assert.fail("Could not initialize mock HTTP client", e);
+ }
+ }
+
+ protected void rpConfigureMockHttpClient(final String clientId, final OIDCClientMetadata metadata) {
+ try {
+ mapResponse(entityConfigurationUrl(clientId), mockResponse(rpEntityConfiguration(clientId, metadata)));
+ mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+ mapResponse(subordinateStatementUrl(anchorFetchEndpoint, clientId),
+ mockResponse(subordinateStatement(clientId, Map.of("openid_relying_party",
+ new OIDCClientMetadata().toJSONObject()))));
+ } catch (UnsupportedOperationException | IOException | URISyntaxException e) {
+ Assert.fail("Could not initialize mock HTTP client", e);
+ }
+ }
+
+ protected void rpConfigureMockHttpClient(final String clientId, final JSONObject metadata) {
+ try {
+ mapResponse(entityConfigurationUrl(clientId), mockResponse(rpEntityConfiguration(clientId)));
+ mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+ mapResponse(subordinateStatementUrl(anchorFetchEndpoint, clientId),
+ mockResponse(subordinateStatement(clientId, Map.of("openid_relying_party", metadata))));
+ } catch (UnsupportedOperationException | IOException | URISyntaxException e) {
+ Assert.fail("Could not initialize mock HTTP client", e);
+ }
+ }
+
+ protected void rpConfigureMockHttpClient(final String clientId, final String rpEntityConfiguration) {
+ rpConfigureMockHttpClient(clientId, rpEntityConfiguration, trustedAnchorConfiguration());
+ }
+
+ protected void rpConfigureMockHttpClient(final String clientId, final String rpEntityConfiguration,
+ final String trustedAnchorConfiguration) {
+ try {
+ mapResponse(entityConfigurationUrl(clientId), mockResponse(rpEntityConfiguration));
+ mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration));
+ mapResponse(subordinateStatementUrl(anchorFetchEndpoint, clientId),
+ mockResponse(subordinateStatement(clientId, Map.of("openid_relying_party",
+ new OIDCClientMetadata().toJSONObject()))));
+ } catch (UnsupportedOperationException | IOException e) {
+ Assert.fail("Could not initialize mock HTTP client", e);
+ }
+ }
+
+ protected void rpConfigureMockHttpClientWithAnchorConstraints(final String clientId,
+ final Map<String,Object> constraints) {
+ try {
+ mapResponse(entityConfigurationUrl(clientId), mockResponse(rpEntityConfiguration(clientId)));
+ mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+ mapResponse(subordinateStatementUrl(anchorFetchEndpoint, clientId),
+ mockResponse(subordinateStatement(anchorId, trustedAnchorKey, leafKey, clientId,
+ Map.of("openid_relying_party", Collections.emptyMap()), constraints)));
+ } catch (UnsupportedOperationException | IOException | URISyntaxException e) {
+ Assert.fail("Could not initialize mock HTTP client", e);
+ }
+ }
+
+ protected void rpConfigureMockHttpClientWithMetadataCrit(final String clientId, final List<String> crit) {
+ try {
+ mapResponse(entityConfigurationUrl(clientId), mockResponse(rpEntityConfiguration(clientId)));
+ mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+ mapResponse(subordinateStatementUrl(anchorFetchEndpoint, clientId),
+ mockResponse(subordinateStatement(clientId, Map.of("openid_relying_party",
+ new OIDCClientMetadata().toJSONObject()), crit, null)));
+ } catch (UnsupportedOperationException | IOException | URISyntaxException e) {
+ Assert.fail("Could not initialize mock HTTP client", e);
+ }
+ }
+
+ protected void rpConfigureMockHttpClientWithCrit(final String clientId, final List<String> configCrit,
+ final List<String> subordinateCrit) {
+ try {
+ final OIDCClientMetadata metadata = new OIDCClientMetadata();
+ metadata.setRedirectionURI(new URI(redirectUri));
+ metadata.setJWKSet(new JWKSet(rpKey.toPublicJWK()));
+ mapResponse(entityConfigurationUrl(clientId),
+ mockResponse(rpEntityConfiguration(clientId, metadata, null, configCrit, rpKey)));
+ mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+ mapResponse(subordinateStatementUrl(anchorFetchEndpoint, clientId),
+ mockResponse(subordinateStatement(clientId, Map.of("openid_relying_party",
+ new OIDCClientMetadata().toJSONObject()), null, subordinateCrit)));
+ } catch (UnsupportedOperationException | IOException | URISyntaxException e) {
+ Assert.fail("Could not initialize mock HTTP client", e);
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ protected void rpConfigureMockHttpClient(final String clientId, final Map<String, Object> testVector) {
+ final String intermediateId = uniqueIntermediateId();
+ try {
+ final Map<String, Object> vectorMetadata = (Map<String, Object>) testVector.get("metadata");
+ if (vectorMetadata.isEmpty()) {
+ mapResponse(entityConfigurationUrl(clientId), mockResponse(
+ rpEntityConfiguration(clientId, intermediateId)));
+ } else {
+ final Map<String, Object> metadata = new HashMap<>(vectorMetadata);
+ metadata.put("redirect_uris", List.of(redirectUri));
+ metadata.put("jwks", new JWKSet(rpKey.toPublicJWK()).toJSONObject());
+ mapResponse(entityConfigurationUrl(clientId),
+ mockResponse(rpEntityConfiguration(clientId,
+ OIDCClientMetadata.parse(new JSONObject(metadata)), intermediateId)));
+ }
+ mapResponse(entityConfigurationUrl(intermediateId), mockResponse(intermediateConfiguration(intermediateId)));
+ mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+ mapResponse(subordinateStatementUrl(anchorFetchEndpoint, intermediateId),
+ mockResponse(rpSubordinateStatement(anchorId, trustedAnchorKey, intermediateKey, intermediateId,
+ Map.of("openid_relying_party", (Map<String, Object>) testVector.get("TA")), anchorId)));
+ mapResponse(subordinateStatementUrl(intermediateId + "/fetch", clientId),
+ mockResponse(rpSubordinateStatement(intermediateId, intermediateKey, leafKey, clientId,
+ Map.of("openid_relying_party", (Map<String, Object>) testVector.get("INT")),
+ intermediateId)));
+ } catch (UnsupportedOperationException | IOException | URISyntaxException | ParseException e) {
+ Assert.fail("Could not initialize mock HTTP client", e);
+ }
+ }
+
+ protected OIDCProviderMetadata emptyOpMetadata(final String issuer) {
+ return new OIDCProviderMetadata(new Issuer(issuer), List.of(SubjectType.PUBLIC),
+ URI.create("https://mock.example.org/jwks"));
+ }
+
+ protected void opConfigureMockHttpClient(final String issuer) {
+ try {
+ mapResponse(entityConfigurationUrl(issuer), mockResponse(opEntityConfiguration(issuer)));
+ mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+ mapResponse(subordinateStatementUrl(anchorFetchEndpoint, issuer),
+ mockResponse(subordinateStatement(issuer, Map.of("openid_provider",
+ emptyOpMetadata(issuer).toJSONObject()))));
+ } catch (UnsupportedOperationException | IOException | URISyntaxException e) {
+ Assert.fail("Could not initialize mock HTTP client", e);
+ }
+ }
+
+ protected void opConfigureMockHttpClient(final String issuer, final OIDCProviderMetadata metadata) {
+ try {
+ mapResponse(entityConfigurationUrl(issuer), mockResponse(opEntityConfiguration(issuer, metadata)));
+ mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+ mapResponse(subordinateStatementUrl(anchorFetchEndpoint, issuer),
+ mockResponse(subordinateStatement(issuer, Map.of("openid_provider",
+ metadata.toJSONObject()))));
+ } catch (UnsupportedOperationException | IOException | URISyntaxException e) {
+ Assert.fail("Could not initialize mock HTTP client", e);
+ }
+ }
+
+ protected void opConfigureMockHttpClient(final String issuer, final JSONObject metadata) {
+ try {
+ mapResponse(entityConfigurationUrl(issuer), mockResponse(opEntityConfiguration(issuer)));
+ mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+ mapResponse(subordinateStatementUrl(anchorFetchEndpoint, issuer),
+ mockResponse(subordinateStatement(issuer, Map.of("openid_provider", metadata))));
+ } catch (UnsupportedOperationException | IOException | URISyntaxException e) {
+ Assert.fail("Could not initialize mock HTTP client", e);
+ }
+ }
+
+ protected void opConfigureMockHttpClient(final String issuer, final String opEntityConfiguration) {
+ try {
+ mapResponse(entityConfigurationUrl(issuer), mockResponse(opEntityConfiguration));
+ mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+ mapResponse(subordinateStatementUrl(anchorFetchEndpoint, issuer),
+ mockResponse(subordinateStatement(issuer, Map.of("openid_provider",
+ emptyOpMetadata(issuer).toJSONObject()))));
+ } catch (UnsupportedOperationException | IOException e) {
+ Assert.fail("Could not initialize mock HTTP client", e);
+ }
+ }
+
+ protected void opConfigureMockHttpClientWithAnchorConstraints(final String issuer,
+ final Map<String,Object> constraints) {
+ try {
+ mapResponse(entityConfigurationUrl(issuer), mockResponse(opEntityConfiguration(issuer)));
+ mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+ mapResponse(subordinateStatementUrl(anchorFetchEndpoint, issuer),
+ mockResponse(subordinateStatement(anchorId, trustedAnchorKey, leafKey, issuer,
+ Map.of("openid_provider", Collections.emptyMap()), constraints)));
+ } catch (UnsupportedOperationException | IOException | URISyntaxException e) {
+ Assert.fail("Could not initialize mock HTTP client", e);
+ }
+ }
+
+ protected List<Map<String, Object>> loadPolicyTestVectors() throws IOException {
+ final ObjectMapper objectMapper = new ObjectMapper();
+ final Resource file = new ClassPathResource(
+ "/net/shibboleth/idp/oidc/metadata/impl/metadata-policy-test-vectors-2025-02-13.json");
+
+ final Charset utf8 = Charset.forName("UTF-8");
+ assert utf8 != null;
+ try {
+ final List<Map<String, Object>> policies =
+ objectMapper.readValue(file.getContentAsString(utf8),
+ new TypeReference<List<Map<String, Object>>>(){});
+ Assert.assertNotNull(policies);
+ return policies;
+ } catch (final JsonProcessingException e) {
+ throw new IOException("Could not parse JSON from the input", e);
+ }
+ }
+
+ 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;
+ }
+
+ protected void assertErrorCode(final FlowExecutionResult result, final String errorCode,
+ final String message) {
+ final ErrorResponse errorResponse = parseErrorResponse(result, message);
+ Assert.assertEquals(errorResponse.getErrorObject().getCode(), errorCode, message);
+ }
+
+ protected void assertErrorDescriptionContains(final FlowExecutionResult result, final String errorDescription,
+ final String message) {
+ final ErrorResponse errorResponse = parseErrorResponse(result, message);
+ Assert.assertNotNull(errorResponse.getErrorObject().getDescription(), message);
+ Assert.assertTrue(errorResponse.getErrorObject().getDescription().contains(errorDescription), message);
+ }
+
+ protected void populateClientAssertionParams(final Map<String, String> requestParameters,
+ final JWT jwt) {
+ requestParameters.put("client_assertion", jwt.serialize());
+ requestParameters.put("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer");
+ }
+
+ protected JWTClaimsSet validJwtAuthenticationClaimsSet(final String clientId, final String audience) {
+ return new JWTClaimsSet.Builder()
+ .subject(clientId)
+ .issuer(clientId)
+ .audience(audience)
+ .expirationTime(Date.from(Instant.now().plusSeconds(600)))
+ .jwtID(idGenerator.generateIdentifier())
+ .build();
+ }
+
+ protected String trustMarkStatusResponse(final String issuer, final String trustMark, final String status,
+ final JWK signerKey) {
+ final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder().issuer(issuer)
+ .issueTime(Date.from(Instant.now()))
+ .claim("trust_mark", trustMark)
+ .claim("status", status)
+ .build();
+ return TrustChainTestUtil.signedJwt(JWSAlgorithm.RS256, signerKey,
+ "trust-mark-status-response+jwt", claimsSet).serialize();
+ }
+
+ protected static BasicJWKCredential loadCredential(final String classPathLocation) {
+ final BasicJWKCredentialFactoryBean factory = new BasicJWKCredentialFactoryBean();
+ factory.setResource(new ClassPathResource(classPathLocation));
+ try {
+ factory.afterPropertiesSet();
+ return factory.getObject();
+ } catch (final Exception e) {
+ Assert.fail();
+ return null;
+ }
+ }
+
+ 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 class RequestUriMatcher implements ArgumentMatcher<ClassicHttpRequest> {
+
+ @Nonnull private final String uri;
+
+ public RequestUriMatcher(final String value) {
+ uri = Constraint.isNotEmpty(value, "URI value cannot be null");
+ }
+
+ @Override
+ public boolean matches(final ClassicHttpRequest match) {
+ try {
+ return match != null && uri.equals(match.getUri().toString());
+ } catch (URISyntaxException e) {
+ }
+ return false;
+ }
+ }
+
+}
diff --git a/oidfed-common-conf-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/EntityConfigurationFlowTest.java b/oidfed-common-conf-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/EntityConfigurationFlowTest.java
new file mode 100644
index 0000000..d0b1afd
--- /dev/null
+++ b/oidfed-common-conf-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/EntityConfigurationFlowTest.java
@@ -0,0 +1,160 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.profile.flow.oidfed;
+
+import java.io.IOException;
+import java.net.URLEncoder;
+import java.nio.charset.Charset;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.webflow.executor.FlowExecutionResult;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.nimbusds.jose.Algorithm;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jwt.SignedJWT;
+import com.nimbusds.oauth2.sdk.ParseException;
+import com.nimbusds.oauth2.sdk.Response;
+
+import net.shibboleth.idp.plugin.oidc.op.profile.flow.oidfed.support.CustomEntityConfigurationMetadataDecorator;
+import net.shibboleth.oidfed.metadata.EntityConfiguration;
+import net.shibboleth.oidfed.metadata.cache.trustmark.DefaultTrustMarkFetchingStrategy;
+import net.shibboleth.oidfed.metadata.impl.EntityConfigurationImpl;
+import net.shibboleth.oidfed.metadata.payload.EntityConfigurationPayload;
+import net.shibboleth.oidfed.metadata.payload.claim.Metadata;
+import net.shibboleth.oidfed.test.TrustChainTestUtil;
+import net.shibboleth.shared.collection.CollectionSupport;
+
+/**
+ * Unit test for the entity configuration flow.
+ */
+ at SuppressWarnings("null")
+public class EntityConfigurationFlowTest extends AbstractFederationFlowTest {
+
+ public static final String FLOW_ID = "oidfed/entity-configuration";
+
+ final String dynamicTrustMarkIssuerId = "https://dyn-trust-mark-issuer.federation.local";
+ final String dynamicTrustMarkType = dynamicTrustMarkIssuerId + "/example";
+
+ @Autowired
+ @Qualifier("shibboleth.oidfed.JWTPayloadJSONObjectMapper")
+ ObjectMapper payloadObjectMapper;
+
+ protected EntityConfigurationFlowTest() {
+ super(FLOW_ID);
+ }
+
+ @Test
+ public void testOutputAndCaching() throws ParseException, IOException, InterruptedException {
+ final String trustMark = TrustChainTestUtil.trustMark(JWSAlgorithm.RS256, trustMarkIssuerKey,
+ dynamicTrustMarkIssuerId, issuer, dynamicTrustMarkType, Instant.now().plusSeconds(300)).serialize();
+ try {
+ mapResponse(entityConfigurationUrl(dynamicTrustMarkIssuerId),
+ mockResponse(trustMarkIssuerConfiguration(dynamicTrustMarkIssuerId)));
+
+ mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+ mapResponse(subordinateStatementUrl(anchorFetchEndpoint, dynamicTrustMarkIssuerId),
+ mockResponse(subordinateStatement(dynamicTrustMarkIssuerId,
+ Map.of("federation_entity", CollectionSupport.emptyMap()), trustMarkIssuerKey)));
+ final String trustMarkUrl = trustMarkEndpoint + "?trust_mark_type="
+ + URLEncoder.encode(dynamicTrustMarkType, Charset.forName("UTF-8"))
+ + "&sub=" + URLEncoder.encode(issuer, Charset.forName("UTF-8"));
+ mapResponse(trustMarkUrl,
+ mockResponse(200, DefaultTrustMarkFetchingStrategy.HTTP_RESPONSE_CONTENT_TYPE.toString(),
+ trustMark));
+ } catch (UnsupportedOperationException | IOException e) {
+ Assert.fail("Could not initialize mock HTTP client", e);
+ }
+
+ request.setRequestURI("/idp/profile/oidfed/entity-configuration");
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ final Response response = parseResponse(result);
+ Assert.assertTrue(response.indicatesSuccess());
+ assertEntityStatement(response);
+
+ final FlowExecutionResult result2 = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ final Response response2 = parseResponse(result2);
+ Assert.assertEquals(response2.toHTTPResponse().getContent(), response.toHTTPResponse().getContent());
+
+ Thread.sleep(2000);
+ final FlowExecutionResult result3 = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ final Response response3 = parseResponse(result3);
+ assertEntityStatement(response3);
+ Assert.assertNotEquals(response3.toHTTPResponse().getContent(), response.toHTTPResponse().getContent());
+ }
+
+ protected void assertEntityStatement(final Response response) throws ParseException {
+ final EntityConfiguration entityStatement;
+ try {
+ entityStatement = EntityConfigurationImpl.parse(
+ SignedJWT.parse(response.toHTTPResponse().getContent()), payloadObjectMapper);
+ } catch (JsonProcessingException | java.text.ParseException e) {
+ Assert.fail();
+ return;
+ }
+ final Metadata metadata = entityStatement.getParsedPayload().getMetadata();
+ Assert.assertNotNull(metadata);
+ final Map<String,Object> entityMetadata = metadata.getFederationEntityMetadata();
+ Assert.assertNotNull(entityMetadata);
+ assert entityMetadata != null;
+ Assert.assertEquals(entityMetadata.get("organization_name"), "Example organization");
+ Assert.assertEquals(entityMetadata.get("contacts"), List.of("contact at example.org"));
+ final EntityConfigurationPayload payload = (EntityConfigurationPayload) entityStatement.getParsedPayload();
+ Assert.assertEquals(payload.getAuthorityHints(),
+ List.of("https://anchor1.example.org","https://anchor2.example.org"));
+ Assert.assertEquals(payload.getTrustAnchorHints(), List.of(anchorId, trustedIntermediateId));
+ final List<Map<String,String>> trustMarkMap = payload.getTrustMarks();
+ Assert.assertNotNull(trustMarkMap);
+ assert trustMarkMap != null;
+ Assert.assertEquals(trustMarkMap.size(), 2);
+ Assert.assertNotNull(trustMarkMap.stream()
+ .filter(map -> map.entrySet().stream()
+ .filter(entry -> "trust_mark_type".equals(entry.getKey()) &&
+ "https://example.org/a-trust-mark".equals(entry.getValue()))
+ .findAny().isPresent())
+ .findAny().orElse(null));
+ Assert.assertNotNull(trustMarkMap.stream()
+ .filter(map -> map.entrySet().stream()
+ .filter(entry -> "trust_mark_type".equals(entry.getKey()) &&
+ dynamicTrustMarkType.equals(entry.getValue()))
+ .findAny().isPresent())
+ .findAny().orElse(null));
+ final Map<String,Object> rawMetadataExtension =
+ metadata.getAllClaims().get(CustomEntityConfigurationMetadataDecorator.CUSTOM_ENTITY_TYPE);
+ Assert.assertNotNull(rawMetadataExtension);
+ Assert.assertEquals(rawMetadataExtension.size(), 3);
+ Assert.assertEquals(rawMetadataExtension.get("key0"), "static_value");
+ Assert.assertEquals(rawMetadataExtension.get("key1"), "value1");
+ Assert.assertEquals(rawMetadataExtension.get("key2"), "value2");
+ }
+
+ protected boolean containsAll(Collection<? extends Algorithm> algs, Collection<String> strings) {
+ final List<String> algStrings = new ArrayList<>();
+ for (final Algorithm alg : algs) {
+ algStrings.add(alg.toString());
+ }
+ return strings.size() == algStrings.size() ? strings.containsAll(strings) : false;
+ }
+
+}
diff --git a/oidfed-common-conf-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/cache/EntityConfigurationMetadataCacheTest.java b/oidfed-common-conf-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/cache/EntityConfigurationMetadataCacheTest.java
new file mode 100644
index 0000000..4274fc8
--- /dev/null
+++ b/oidfed-common-conf-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/cache/EntityConfigurationMetadataCacheTest.java
@@ -0,0 +1,594 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.profile.flow.oidfed.cache;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.Mockito.when;
+
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.time.Instant;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.nimbusds.jose.EncryptionMethod;
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JOSEObjectType;
+import com.nimbusds.jose.JWEAlgorithm;
+import com.nimbusds.jose.JWEObject;
+import com.nimbusds.jose.JWEHeader;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.JWSHeader;
+import com.nimbusds.jose.Payload;
+import com.nimbusds.jose.crypto.RSAEncrypter;
+import com.nimbusds.jose.jwk.JWKSet;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.PlainJWT;
+import com.nimbusds.jwt.SignedJWT;
+
+import net.shibboleth.idp.plugin.oidc.op.profile.flow.oidfed.AbstractFederationFlowTest;
+import net.shibboleth.idp.plugin.oidc.op.profile.flow.oidfed.EntityConfigurationFlowTest;
+import net.shibboleth.idp.plugin.oidc.op.profile.flow.oidfed.support.CustomEntityConfigurationFilterStrategy;
+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.cache.ResponseContainerExpirationCriterion;
+import net.shibboleth.oidfed.metadata.cache.SubjectEntityIDCriterion;
+import net.shibboleth.oidfed.metadata.cache.configuration.EntityConfigurationContainer;
+import net.shibboleth.oidfed.test.TrustChainTestUtil;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.resolver.CriteriaSet;
+
+/**
+ * Unit tests for the default entity configuration metadata cache.
+ */
+ at SuppressWarnings("null")
+public class EntityConfigurationMetadataCacheTest extends AbstractFederationFlowTest {
+
+ protected EntityConfigurationMetadataCacheTest() {
+ super(EntityConfigurationFlowTest.FLOW_ID);
+ }
+
+ @Autowired
+ @Qualifier("shibboleth.oidfed.EntityConfigurationMetadataCache")
+ MetadataCache<EntityConfigurationContainer> entityConfigurationCache;
+
+ @Test
+ public void testValidEntityConfiguration()
+ throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+ final String entityId = uniqueClientId();
+ final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(entityId).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("federation_entity", Collections.emptyMap()));
+ final String entityConfiguration = TrustChainTestUtil.signedJwt(
+ JWSAlgorithm.RS256, leafKey, "entity-statement+jwt", builder.build()).serialize();
+
+ mapResponse(entityConfigurationUrl(entityId), mockResponse(entityConfiguration));
+ try {
+ final List<EntityConfigurationContainer> result =
+ entityConfigurationCache.get(new CriteriaSet(new SubjectEntityIDCriterion(entityId),
+ new ResponseContainerExpirationCriterion(Instant.now().plusSeconds(300))));
+ Assert.assertNotNull(result);
+ Assert.assertEquals(result.size(), 1);
+ final EntityConfiguration statement = result.get(0).getStatement();
+ Assert.assertNotNull(statement);
+ assert statement != null;
+ Assert.assertNull(statement.getParsedPayload().getCustomClaims()
+ .get(CustomEntityConfigurationFilterStrategy.CUSTOM_CLAIM_NAME));
+ } catch (MetadataCacheException e) {
+ Assert.fail("Could not resolve entity configuration", e);
+ }
+ }
+
+ @Test
+ public void testValidEntityConfiguration_customCriticalClaim()
+ throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+ final String entityId = uniqueClientId();
+ final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(entityId).subject(entityId)
+ .issueTime(Date.from(Instant.now()))
+ .expirationTime(Date.from(Instant.now().plusSeconds(300)))
+ .claim("jwks", new JWKSet(leafKey).toJSONObject(true))
+ .claim("crit", List.of("default_crit"))
+ .claim("metadata", Map.of("federation_entity", Collections.emptyMap()));
+ final String entityConfiguration = TrustChainTestUtil.signedJwt(
+ JWSAlgorithm.RS256, leafKey, "entity-statement+jwt", builder.build()).serialize();
+
+ mapResponse(entityConfigurationUrl(entityId), mockResponse(entityConfiguration));
+ try {
+ final List<EntityConfigurationContainer> result =
+ entityConfigurationCache.get(new CriteriaSet(new SubjectEntityIDCriterion(entityId),
+ new ResponseContainerExpirationCriterion(Instant.now().plusSeconds(300))));
+ Assert.assertNotNull(result);
+ Assert.assertEquals(result.size(), 1);
+ final EntityConfiguration statement = result.get(0).getStatement();
+ Assert.assertNotNull(statement);
+ assert statement != null;
+ Assert.assertEquals(statement.getParsedPayload().getCustomClaims()
+ .get(CustomEntityConfigurationFilterStrategy.CUSTOM_CLAIM_NAME),
+ CustomEntityConfigurationFilterStrategy.CUSTOM_CLAIM_VALUE);
+ } catch (MetadataCacheException e) {
+ Assert.fail("Could not resolve entity configuration", e);
+ }
+ }
+
+ @Test
+ public void testUnsignedEntityConfiguration()
+ throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+ final String entityId = uniqueClientId();
+ final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(entityId).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("federation_entity", Collections.emptyMap()));
+ final String entityConfiguration = new PlainJWT(builder.build()).serialize();
+
+ mapResponse(entityConfigurationUrl(entityId), mockResponse(entityConfiguration));
+ assertNoEntityConfiguration(entityId);
+ }
+
+ @Test
+ public void testEncryptedEntityConfiguration()
+ throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+ final String entityId = uniqueClientId();
+ final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(entityId).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("federation_entity", Collections.emptyMap()));
+ final SignedJWT signedJwt = TrustChainTestUtil.signedJwt(
+ JWSAlgorithm.RS256, leafKey, "entity-statement+jwt", builder.build());
+ final JWEObject jwe = new JWEObject(new JWEHeader.Builder(JWEAlgorithm.RSA_OAEP_256, EncryptionMethod.A256GCM)
+ .contentType("JWT")
+ .build(),
+ new Payload(signedJwt));
+ try {
+ jwe.encrypt(new RSAEncrypter(leafKey.toRSAKey()));
+ } catch (JOSEException e) {
+ Assert.fail("Encryption failed", e);
+ }
+ final String entityConfiguration = jwe.serialize();
+
+ mapResponse(entityConfigurationUrl(entityId), mockResponse(entityConfiguration));
+ assertNoEntityConfiguration(entityId);
+ }
+
+ @Test
+ public void testWithUnrecognizedCriticalClaim()
+ throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+ final String entityId = uniqueClientId();
+ final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(entityId).subject(entityId)
+ .issueTime(Date.from(Instant.now()))
+ .expirationTime(Date.from(Instant.now().plusSeconds(300)))
+ .claim("jwks", new JWKSet(leafKey).toJSONObject(true))
+ .claim("crit", List.of("subordinate_crit"))
+ .claim("metadata", Map.of("federation_entity", Collections.emptyMap()));
+ final String entityConfiguration = TrustChainTestUtil.signedJwt(
+ JWSAlgorithm.RS256, leafKey, "entity-statement+jwt", builder.build()).serialize();
+
+ mapResponse(entityConfigurationUrl(entityId), mockResponse(entityConfiguration));
+ assertNoEntityConfiguration(entityId);
+ }
+
+ @Test
+ public void testWithStandardClaimAsCriticalClaim()
+ throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+ final String entityId = uniqueClientId();
+ final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(entityId).subject(entityId)
+ .issueTime(Date.from(Instant.now()))
+ .expirationTime(Date.from(Instant.now().plusSeconds(300)))
+ .claim("jwks", new JWKSet(leafKey).toJSONObject(true))
+ .claim("crit", List.of("jwks"))
+ .claim("metadata", Map.of("federation_entity", Collections.emptyMap()));
+ final String entityConfiguration = TrustChainTestUtil.signedJwt(
+ JWSAlgorithm.RS256, leafKey, "entity-statement+jwt", builder.build()).serialize();
+
+ mapResponse(entityConfigurationUrl(entityId), mockResponse(entityConfiguration));
+ assertNoEntityConfiguration(entityId);
+ }
+
+ @Test
+ public void testSignatureWithNonMathchingKey()
+ throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+ final String entityId = uniqueClientId();
+ final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(entityId).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("federation_entity", Collections.emptyMap()));
+ final String entityConfiguration = TrustChainTestUtil.signedJwt(
+ JWSAlgorithm.RS256, anchorKey, "entity-statement+jwt", builder.build()).serialize();
+
+ mapResponse(entityConfigurationUrl(entityId), mockResponse(entityConfiguration));
+ assertNoEntityConfiguration(entityId);
+ }
+
+ @Test
+ public void testNonMatchingSubjectVsIssuer()
+ throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+ final String entityId = uniqueClientId();
+ final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(uniqueClientId()).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("federation_entity", Collections.emptyMap()));
+ final String entityConfiguration = TrustChainTestUtil.signedJwt(
+ JWSAlgorithm.RS256, leafKey, "entity-statement+jwt", builder.build()).serialize();
+
+ mapResponse(entityConfigurationUrl(entityId), mockResponse(entityConfiguration));
+ assertNoEntityConfiguration(entityId);
+ }
+
+ @Test
+ public void testValidClaims_nonMatchingSubject()
+ throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+ final String entityId = uniqueClientId();
+ final String claimsEntityId = uniqueClientId();
+ final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(claimsEntityId).subject(claimsEntityId)
+ .issueTime(Date.from(Instant.now()))
+ .expirationTime(Date.from(Instant.now().plusSeconds(300)))
+ .claim("jwks", new JWKSet(leafKey).toJSONObject(true))
+ .claim("metadata", Map.of("federation_entity", Collections.emptyMap()));
+ final String entityConfiguration = TrustChainTestUtil.signedJwt(
+ JWSAlgorithm.RS256, leafKey, "entity-statement+jwt", builder.build()).serialize();
+
+ mapResponse(entityConfigurationUrl(entityId), mockResponse(entityConfiguration));
+ assertNoEntityConfiguration(entityId);
+ }
+
+ @Test
+ public void testValidClaims_invalidTypeHeader()
+ throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+ final String entityId = uniqueClientId();
+ final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(entityId).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("federation_entity", Collections.emptyMap()));
+ final String entityConfiguration = TrustChainTestUtil.signedJwt(
+ JWSAlgorithm.RS256, leafKey, "jwt", builder.build()).serialize();
+
+ mapResponse(entityConfigurationUrl(entityId), mockResponse(entityConfiguration));
+ assertNoEntityConfiguration(entityId);
+ }
+
+ @Test
+ public void testIssuedInFuture()
+ throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+ final String entityId = uniqueClientId();
+ final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(entityId).subject(entityId)
+ .issueTime(Date.from(Instant.now().plusSeconds(300)))
+ .expirationTime(Date.from(Instant.now().plusSeconds(300)))
+ .claim("jwks", new JWKSet(leafKey).toJSONObject(true))
+ .claim("metadata", Map.of("federation_entity", Collections.emptyMap()));
+ final String entityConfiguration = TrustChainTestUtil.signedJwt(
+ JWSAlgorithm.RS256, leafKey, "entity-statement+jwt", builder.build()).serialize();
+
+ mapResponse(entityConfigurationUrl(entityId), mockResponse(entityConfiguration));
+ assertNoEntityConfiguration(entityId);
+ }
+
+ @Test
+ public void testMissingSub()
+ throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+ final String entityId = uniqueClientId();
+ final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(entityId)
+ .issueTime(Date.from(Instant.now()))
+ .expirationTime(Date.from(Instant.now().plusSeconds(300)))
+ .claim("jwks", new JWKSet(leafKey).toJSONObject(true))
+ .claim("metadata", Map.of("federation_entity", Collections.emptyMap()));
+ final String entityConfiguration = TrustChainTestUtil.signedJwt(
+ JWSAlgorithm.RS256, leafKey, "entity-statement+jwt", builder.build()).serialize();
+
+ mapResponse(entityConfigurationUrl(entityId), mockResponse(entityConfiguration));
+ assertNoEntityConfiguration(entityId);
+ }
+
+ @Test
+ public void testExpired()
+ throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+ final String entityId = uniqueClientId();
+ final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(entityId).subject(entityId)
+ .issueTime(Date.from(Instant.now()))
+ .expirationTime(Date.from(Instant.now().minusSeconds(300)))
+ .claim("jwks", new JWKSet(leafKey).toJSONObject(true))
+ .claim("metadata", Map.of("federation_entity", Collections.emptyMap()));
+ final String entityConfiguration = TrustChainTestUtil.signedJwt(
+ JWSAlgorithm.RS256, leafKey, "entity-statement+jwt", builder.build()).serialize();
+
+ mapResponse(entityConfigurationUrl(entityId), mockResponse(entityConfiguration));
+ assertNoEntityConfiguration(entityId);
+ }
+
+ @Test
+ public void testMissingJwks()
+ throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+ final String entityId = uniqueClientId();
+ final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(entityId).subject(entityId)
+ .issueTime(Date.from(Instant.now()))
+ .expirationTime(Date.from(Instant.now().plusSeconds(300)))
+ .claim("metadata", Map.of("federation_entity", Collections.emptyMap()));
+ final String entityConfiguration = TrustChainTestUtil.signedJwt(
+ JWSAlgorithm.RS256, leafKey, "entity-statement+jwt", builder.build()).serialize();
+
+ mapResponse(entityConfigurationUrl(entityId), mockResponse(entityConfiguration));
+ assertNoEntityConfiguration(entityId);
+ }
+
+ @Test
+ public void testInvalidJwksClaim()
+ throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+ final String entityId = uniqueClientId();
+ final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(entityId).subject(entityId)
+ .issueTime(Date.from(Instant.now()))
+ .expirationTime(Date.from(Instant.now().plusSeconds(300)))
+ .claim("jwks", Map.of("federation_entity", Collections.emptyMap()))
+ .claim("metadata", Map.of("federation_entity", Collections.emptyMap()));
+ final String entityConfiguration = TrustChainTestUtil.signedJwt(
+ JWSAlgorithm.RS256, leafKey, "entity-statement+jwt", builder.build()).serialize();
+
+ mapResponse(entityConfigurationUrl(entityId), mockResponse(entityConfiguration));
+ assertNoEntityConfiguration(entityId);
+ }
+
+ @Test
+ public void testValidClaims_forbiddenHeader_trustChain()
+ throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+ final String entityId = uniqueClientId();
+ final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(entityId).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("federation_entity", Collections.emptyMap()));
+ final JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.RS256)
+ .type(new JOSEObjectType("entity-statement+jwt"))
+ .keyID(leafKey.getKeyID())
+ .customParam("trust_chain", "forbidden")
+ .build();
+ final String entityConfiguration = TrustChainTestUtil.signedJwt(
+ JWSAlgorithm.RS256, leafKey, header, builder.build()).serialize();
+
+ mapResponse(entityConfigurationUrl(entityId), mockResponse(entityConfiguration));
+ assertNoEntityConfiguration(entityId);
+ }
+
+ @Test
+ public void testValidClaims_forbiddenHeader_peerTrustChain()
+ throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+ final String entityId = uniqueClientId();
+ final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(entityId).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("federation_entity", Collections.emptyMap()));
+ final JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.RS256)
+ .type(new JOSEObjectType("entity-statement+jwt"))
+ .keyID(leafKey.getKeyID())
+ .customParam("peer_trust_chain", "forbidden")
+ .build();
+ final String entityConfiguration = TrustChainTestUtil.signedJwt(
+ JWSAlgorithm.RS256, leafKey, header, builder.build()).serialize();
+
+ mapResponse(entityConfigurationUrl(entityId), mockResponse(entityConfiguration));
+ assertNoEntityConfiguration(entityId);
+ }
+
+ @Test
+ public void testEmptyAuthorityHintsClaim()
+ throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+ final String entityId = uniqueClientId();
+ final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(entityId).subject(entityId)
+ .issueTime(Date.from(Instant.now()))
+ .expirationTime(Date.from(Instant.now().plusSeconds(300)))
+ .claim("jwks", new JWKSet(leafKey).toJSONObject(true))
+ .claim("authority_hints", CollectionSupport.emptyList())
+ .claim("metadata", Map.of("federation_entity", Collections.emptyMap()));
+ final String entityConfiguration = TrustChainTestUtil.signedJwt(
+ JWSAlgorithm.RS256, leafKey, "entity-statement+jwt", builder.build()).serialize();
+
+ mapResponse(entityConfigurationUrl(entityId), mockResponse(entityConfiguration));
+ assertNoEntityConfiguration(entityId);
+ }
+
+ @Test
+ public void testEmptyTrustAnchorHintsClaim()
+ throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+ final String entityId = uniqueClientId();
+ final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(entityId).subject(entityId)
+ .issueTime(Date.from(Instant.now()))
+ .expirationTime(Date.from(Instant.now().plusSeconds(300)))
+ .claim("jwks", new JWKSet(leafKey).toJSONObject(true))
+ .claim("trust_anchor_hints", CollectionSupport.emptyList())
+ .claim("metadata", Map.of("federation_entity", Collections.emptyMap()));
+ final String entityConfiguration = TrustChainTestUtil.signedJwt(
+ JWSAlgorithm.RS256, leafKey, "entity-statement+jwt", builder.build()).serialize();
+
+ mapResponse(entityConfigurationUrl(entityId), mockResponse(entityConfiguration));
+ assertNoEntityConfiguration(entityId);
+ }
+
+ @Test
+ public void testNullFederationEntityMetadata()
+ throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+ final String entityId = uniqueClientId();
+ final Map<String, Object> metadata = new HashMap<>();
+ metadata.put("federation_entity", null);
+ final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(entityId).subject(entityId)
+ .issueTime(Date.from(Instant.now()))
+ .expirationTime(Date.from(Instant.now().plusSeconds(300)))
+ .claim("jwks", new JWKSet(leafKey).toJSONObject(true))
+ .claim("metadata", metadata);
+ final String entityConfiguration = TrustChainTestUtil.signedJwt(
+ JWSAlgorithm.RS256, leafKey, "entity-statement+jwt", builder.build()).serialize();
+
+ mapResponse(entityConfigurationUrl(entityId), mockResponse(entityConfiguration));
+ assertNoEntityConfiguration(entityId);
+ }
+
+ @Test
+ public void testNullCustomMetadata()
+ throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+ final String entityId = uniqueClientId();
+ final Map<String, Object> metadata = new HashMap<>();
+ metadata.put("federation_entity", Collections.emptyMap());
+ metadata.put("custom_type", null);
+ final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(entityId).subject(entityId)
+ .issueTime(Date.from(Instant.now()))
+ .expirationTime(Date.from(Instant.now().plusSeconds(300)))
+ .claim("jwks", new JWKSet(leafKey).toJSONObject(true))
+ .claim("metadata", metadata);
+ final String entityConfiguration = TrustChainTestUtil.signedJwt(
+ JWSAlgorithm.RS256, leafKey, "entity-statement+jwt", builder.build()).serialize();
+
+ mapResponse(entityConfigurationUrl(entityId), mockResponse(entityConfiguration));
+ assertNoEntityConfiguration(entityId);
+ }
+
+ @Test
+ public void testNullResponse()
+ throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+ final String entityId = uniqueClientId();
+ mapResponse(entityConfigurationUrl(entityId), null);
+ assertNoEntityConfiguration(entityId);
+ }
+
+ @Test
+ public void testExceptionResponse()
+ throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+ final String entityId = uniqueClientId();
+ when(federationHttpClient.executeOpen(any(), argThat(new RequestUriMatcher(entityConfigurationUrl(entityId))),
+ any())).thenThrow(IOException.class);
+ assertNoEntityConfiguration(entityId);
+ }
+
+ @Test
+ public void testInvalidtTrustMark_nonMatchingType()
+ throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+ final String entityId = uniqueClientId();
+ final String trustMark = TrustChainTestUtil.trustMark(JWSAlgorithm.RS256, trustMarkIssuerKey, trustMarkIssuerId,
+ entityId, "https://example.org/email-allowing-trust-mark", Instant.now().plusSeconds(300)).serialize();
+ final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(entityId).subject(entityId)
+ .issueTime(Date.from(Instant.now()))
+ .expirationTime(Date.from(Instant.now().plusSeconds(300)))
+ .claim("jwks", new JWKSet(leafKey).toJSONObject(true))
+ .claim("trust_marks", List.of(Map.of(
+ "trust_mark_type", "https://example.org/non-matching-type",
+ "trust_mark", trustMark)))
+ .claim("metadata", Map.of("federation_entity", Collections.emptyMap()));
+ final String entityConfiguration = TrustChainTestUtil.signedJwt(
+ JWSAlgorithm.RS256, leafKey, "entity-statement+jwt", builder.build()).serialize();
+
+ mapResponse(entityConfigurationUrl(entityId), mockResponse(entityConfiguration));
+ assertNoEntityConfiguration(entityId);
+ }
+
+ @Test
+ public void testInvalidtTrustMarkIssuers_invalidFormat()
+ throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+ final String entityId = uniqueClientId();
+ final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(entityId).subject(entityId)
+ .issueTime(Date.from(Instant.now()))
+ .expirationTime(Date.from(Instant.now().plusSeconds(300)))
+ .claim("jwks", new JWKSet(leafKey).toJSONObject(true))
+ .claim("trust_mark_issuers", List.of(trustMarkIssuerId))
+ .claim("metadata", Map.of("federation_entity", Collections.emptyMap()));
+ final String entityConfiguration = TrustChainTestUtil.signedJwt(
+ JWSAlgorithm.RS256, leafKey, "entity-statement+jwt", builder.build()).serialize();
+
+ mapResponse(entityConfigurationUrl(entityId), mockResponse(entityConfiguration));
+ assertNoEntityConfiguration(entityId);
+ }
+
+ @Test
+ public void testInvalidtTrustMarkOwners_invalidJwks()
+ throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+ final String entityId = uniqueClientId();
+ final Map<String,Object> trustMarkOwner = new HashMap<>();
+ trustMarkOwner.put("sub", trustMarkIssuerId);
+ trustMarkOwner.put("jwks", Collections.emptyMap());
+ final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(entityId).subject(entityId)
+ .issueTime(Date.from(Instant.now()))
+ .expirationTime(Date.from(Instant.now().plusSeconds(300)))
+ .claim("jwks", new JWKSet(leafKey).toJSONObject(true))
+ .claim("trust_mark_owners", Map.of("https://example.org/email-allowing-trust-mark", trustMarkOwner))
+ .claim("metadata", Map.of("federation_entity", Collections.emptyMap()));
+ final String entityConfiguration = TrustChainTestUtil.signedJwt(
+ JWSAlgorithm.RS256, leafKey, "entity-statement+jwt", builder.build()).serialize();
+
+ mapResponse(entityConfigurationUrl(entityId), mockResponse(entityConfiguration));
+ assertNoEntityConfiguration(entityId);
+ }
+
+ @Test
+ public void testInvalidtTrustMarkOwners_missingJwks()
+ throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+ final String entityId = uniqueClientId();
+ final Map<String,Object> trustMarkOwner = new HashMap<>();
+ trustMarkOwner.put("sub", trustMarkIssuerId);
+ final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(entityId).subject(entityId)
+ .issueTime(Date.from(Instant.now()))
+ .expirationTime(Date.from(Instant.now().plusSeconds(300)))
+ .claim("jwks", new JWKSet(leafKey).toJSONObject(true))
+ .claim("trust_mark_owners", Map.of("https://example.org/email-allowing-trust-mark", trustMarkOwner))
+ .claim("metadata", Map.of("federation_entity", Collections.emptyMap()));
+ final String entityConfiguration = TrustChainTestUtil.signedJwt(
+ JWSAlgorithm.RS256, leafKey, "entity-statement+jwt", builder.build()).serialize();
+
+ mapResponse(entityConfigurationUrl(entityId), mockResponse(entityConfiguration));
+ assertNoEntityConfiguration(entityId);
+ }
+
+ @Test
+ public void testInvalidtTrustMarkOwners_missingSub()
+ throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+ final String entityId = uniqueClientId();
+ final Map<String,Object> trustMarkOwner = new HashMap<>();
+ trustMarkOwner.put("jwks", new JWKSet(leafKey).toJSONObject(true));
+ final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(entityId).subject(entityId)
+ .issueTime(Date.from(Instant.now()))
+ .expirationTime(Date.from(Instant.now().plusSeconds(300)))
+ .claim("jwks", new JWKSet(leafKey).toJSONObject(true))
+ .claim("trust_mark_owners", Map.of("https://example.org/email-allowing-trust-mark", trustMarkOwner))
+ .claim("metadata", Map.of("federation_entity", Collections.emptyMap()));
+ final String entityConfiguration = TrustChainTestUtil.signedJwt(
+ JWSAlgorithm.RS256, leafKey, "entity-statement+jwt", builder.build()).serialize();
+
+ mapResponse(entityConfigurationUrl(entityId), mockResponse(entityConfiguration));
+ assertNoEntityConfiguration(entityId);
+ }
+
+ protected void assertNoEntityConfiguration(final String entityId) {
+ try {
+ final List<EntityConfigurationContainer> result =
+ entityConfigurationCache.get(new CriteriaSet(new SubjectEntityIDCriterion(entityId),
+ new ResponseContainerExpirationCriterion(Instant.now().plusSeconds(300))));
+ Assert.assertNotNull(result);
+ Assert.assertEquals(result.size(), 0);
+ } catch (MetadataCacheException e) {
+ Assert.fail("Could not resolve entity configuration", e);
+ }
+
+ }
+}
diff --git a/oidfed-common-conf-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/cache/SignedKeysetMetadataCacheTest.java b/oidfed-common-conf-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/cache/SignedKeysetMetadataCacheTest.java
new file mode 100644
index 0000000..a0bd44a
--- /dev/null
+++ b/oidfed-common-conf-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/cache/SignedKeysetMetadataCacheTest.java
@@ -0,0 +1,233 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.profile.flow.oidfed.cache;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.time.Instant;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.jwk.JWK;
+import com.nimbusds.jose.jwk.JWKSet;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientMetadata;
+
+import net.shibboleth.idp.plugin.oidc.op.profile.flow.oidfed.AbstractFederationFlowTest;
+import net.shibboleth.idp.plugin.oidc.op.profile.flow.oidfed.EntityConfigurationFlowTest;
+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.SignedKeyset;
+import net.shibboleth.oidfed.metadata.cache.SubjectEntityStatementCriterion;
+import net.shibboleth.oidfed.metadata.cache.keyset.SignedKeysetContainer;
+import net.shibboleth.oidfed.metadata.cache.keyset.SubjectSignedKeysetUriCriterion;
+import net.shibboleth.oidfed.test.TrustChainTestUtil;
+import net.shibboleth.shared.resolver.CriteriaSet;
+
+/**
+ * Unit tests for the default signed keysetn metadata cache.
+ */
+ at SuppressWarnings("null")
+public class SignedKeysetMetadataCacheTest extends AbstractFederationFlowTest {
+
+ protected SignedKeysetMetadataCacheTest() {
+ super(EntityConfigurationFlowTest.FLOW_ID);
+ }
+
+ @Autowired
+ @Qualifier("shibboleth.oidfed.SignedKeysetMetadataCache")
+ MetadataCache<SignedKeysetContainer> signedKeysetCache;
+
+ @Test
+ public void testValidSignedKeyset()
+ throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+ final String entityId = uniqueClientId();
+ final String signedJwksUri = entityId + "/jwks.jwt";
+ final OIDCClientMetadata metadata = new OIDCClientMetadata();
+ metadata.setRedirectionURI(new URI(redirectUri));
+ metadata.setCustomField("signed_jwks_uri", signedJwksUri);
+ final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(entityId).subject(entityId)
+ .issueTime(Date.from(Instant.now()))
+ .claim("keys", new JWKSet(rpKey).toJSONObject(true).get("keys"));
+ final String signedKeyset = TrustChainTestUtil.signedJwt(
+ JWSAlgorithm.RS256, leafKey, "jwk-set+jwt", builder.build()).serialize();
+
+ mapResponse(signedJwksUri, mockResponse(200, "application/jwk-set+jwt", signedKeyset));
+ try {
+ final List<SignedKeysetContainer> result =
+ signedKeysetCache.get(new CriteriaSet(
+ new SubjectSignedKeysetUriCriterion(signedJwksUri),
+ new SubjectEntityStatementCriterion(buildEntityConfiguration(entityId, metadata))));
+ Assert.assertNotNull(result);
+ Assert.assertEquals(result.size(), 1);
+ final SignedKeyset statement = result.get(0).getStatement();
+ Assert.assertNotNull(statement);
+ assert statement != null;
+ final List<JWK> keys = statement.getParsedPayload().getKeys();
+ Assert.assertNotNull(keys);
+ Assert.assertEquals(keys.size(), 1);
+ Assert.assertEquals(keys.get(0), rpKey.toPublicJWK());
+ } catch (MetadataCacheException e) {
+ Assert.fail("Could not resolve entity configuration", e);
+ }
+ }
+
+ @Test
+ public void testValidSignedKeyset_optionalExpiration()
+ throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+ final String entityId = uniqueClientId();
+ final String signedJwksUri = entityId + "/jwks.jwt";
+ final OIDCClientMetadata metadata = new OIDCClientMetadata();
+ metadata.setRedirectionURI(new URI(redirectUri));
+ metadata.setCustomField("signed_jwks_uri", signedJwksUri);
+ final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(entityId).subject(entityId)
+ .issueTime(Date.from(Instant.now()))
+ .expirationTime(Date.from(Instant.now().plusSeconds(300)))
+ .claim("keys", new JWKSet(rpKey).toJSONObject(true).get("keys"));
+ final String signedKeyset = TrustChainTestUtil.signedJwt(
+ JWSAlgorithm.RS256, leafKey, "jwk-set+jwt", builder.build()).serialize();
+
+ mapResponse(signedJwksUri, mockResponse(200, "application/jwk-set+jwt", signedKeyset));
+ try {
+ final List<SignedKeysetContainer> result =
+ signedKeysetCache.get(new CriteriaSet(
+ new SubjectSignedKeysetUriCriterion(signedJwksUri),
+ new SubjectEntityStatementCriterion(buildEntityConfiguration(entityId, metadata))));
+ Assert.assertNotNull(result);
+ Assert.assertEquals(result.size(), 1);
+ final SignedKeyset statement = result.get(0).getStatement();
+ Assert.assertNotNull(statement);
+ assert statement != null;
+ final List<JWK> keys = statement.getParsedPayload().getKeys();
+ Assert.assertNotNull(keys);
+ Assert.assertEquals(keys.size(), 1);
+ Assert.assertEquals(keys.get(0), rpKey.toPublicJWK());
+ } catch (MetadataCacheException e) {
+ Assert.fail("Could not resolve entity configuration", e);
+ }
+ }
+
+ @Test
+ public void testInvalidSignedKeyset_expired()
+ throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+ final String entityId = uniqueClientId();
+ final String signedJwksUri = entityId + "/jwks.jwt";
+ final OIDCClientMetadata metadata = new OIDCClientMetadata();
+ metadata.setRedirectionURI(new URI(redirectUri));
+ metadata.setCustomField("signed_jwks_uri", signedJwksUri);
+ final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(entityId).subject(entityId)
+ .issueTime(Date.from(Instant.now()))
+ .expirationTime(Date.from(Instant.now().minusSeconds(300)))
+ .claim("keys", new JWKSet(rpKey).toJSONObject(true).get("keys"));
+ final String signedKeyset = TrustChainTestUtil.signedJwt(
+ JWSAlgorithm.RS256, leafKey, "jwk-set+jwt", builder.build()).serialize();
+
+ mapResponse(signedJwksUri, mockResponse(200, "application/jwk-set+jwt", signedKeyset));
+ assertNoSignedKeyset(signedJwksUri, buildEntityConfiguration(entityId, metadata));
+ }
+
+ @Test
+ public void testInvalidSignedKeyset_invalidStatementType()
+ throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+ final String entityId = uniqueClientId();
+ final String signedJwksUri = entityId + "/jwks.jwt";
+ final OIDCClientMetadata metadata = new OIDCClientMetadata();
+ metadata.setRedirectionURI(new URI(redirectUri));
+ metadata.setCustomField("signed_jwks_uri", signedJwksUri);
+ final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(entityId).subject(entityId)
+ .issueTime(Date.from(Instant.now()))
+ .expirationTime(Date.from(Instant.now().plusSeconds(300)))
+ .claim("keys", new JWKSet(rpKey).toJSONObject(true).get("keys"));
+ final String signedKeyset = TrustChainTestUtil.signedJwt(
+ JWSAlgorithm.RS256, leafKey, "entity-statement+jwt", builder.build()).serialize();
+
+ mapResponse(signedJwksUri, mockResponse(200, "application/jwk-set+jwt", signedKeyset));
+ assertNoSignedKeyset(signedJwksUri, buildEntityConfiguration(entityId, metadata));
+ }
+
+ @Test
+ public void testInvalidSignedKeyset_invalidResponseContentType()
+ throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+ final String entityId = uniqueClientId();
+ final String signedJwksUri = entityId + "/jwks.jwt";
+ final OIDCClientMetadata metadata = new OIDCClientMetadata();
+ metadata.setRedirectionURI(new URI(redirectUri));
+ metadata.setCustomField("signed_jwks_uri", signedJwksUri);
+ final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(entityId).subject(entityId)
+ .issueTime(Date.from(Instant.now()))
+ .expirationTime(Date.from(Instant.now().plusSeconds(300)))
+ .claim("keys", new JWKSet(rpKey).toJSONObject(true).get("keys"));
+ final String signedKeyset = TrustChainTestUtil.signedJwt(
+ JWSAlgorithm.RS256, leafKey, "jwk-set+jwt", builder.build()).serialize();
+
+ mapResponse(signedJwksUri, mockResponse(200, "application/entity-statement+jwt", signedKeyset));
+ assertNoSignedKeyset(signedJwksUri, buildEntityConfiguration(entityId, metadata));
+ }
+
+ @Test
+ public void testInvalidSignedKeyset_wrongKey()
+ throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+ final String entityId = uniqueClientId();
+ final String signedJwksUri = entityId + "/jwks.jwt";
+ final OIDCClientMetadata metadata = new OIDCClientMetadata();
+ metadata.setRedirectionURI(new URI(redirectUri));
+ metadata.setCustomField("signed_jwks_uri", signedJwksUri);
+ final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(entityId).subject(entityId)
+ .issueTime(Date.from(Instant.now()))
+ .expirationTime(Date.from(Instant.now().minusSeconds(300)))
+ .claim("keys", new JWKSet(rpKey).toJSONObject(true).get("keys"));
+ final String signedKeyset = TrustChainTestUtil.signedJwt(
+ JWSAlgorithm.RS256, rpKey, "jwk-set+jwt", builder.build()).serialize();
+
+ mapResponse(signedJwksUri, mockResponse(200, "application/jwk-set+jwt", signedKeyset));
+ assertNoSignedKeyset(signedJwksUri, buildEntityConfiguration(entityId, metadata));
+ }
+
+ protected EntityConfiguration buildEntityConfiguration(final String clientId, final OIDCClientMetadata metadata) {
+ final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(clientId).subject(clientId)
+ .issueTime(Date.from(Instant.now()))
+ .expirationTime(Date.from(Instant.now().plusSeconds(300)))
+ .claim("jwks", new JWKSet(leafKey).toJSONObject(true))
+ .claim("metadata", Map.of("openid_relying_party", metadata.toJSONObject()))
+ .claim("authority_hints", new String[] { anchorId });
+ final EntityStatement<?> rpConfiguration =
+ TrustChainTestUtil.entityStatement(JWSAlgorithm.RS256, leafKey, builder.build());
+ return (EntityConfiguration) rpConfiguration;
+ }
+
+ protected void assertNoSignedKeyset(final String signedJwksUri, final EntityConfiguration entityConfiguration) {
+ try {
+ final List<SignedKeysetContainer> result =
+ signedKeysetCache.get(new CriteriaSet(
+ new SubjectSignedKeysetUriCriterion(signedJwksUri),
+ new SubjectEntityStatementCriterion(entityConfiguration)));
+ Assert.assertNotNull(result);
+ Assert.assertEquals(result.size(), 0);
+ } catch (MetadataCacheException e) {
+ Assert.fail("Could not resolve signed keyset", e);
+ }
+
+ }
+}
diff --git a/oidfed-common-conf-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/cache/SubordinateStatementMetadataCacheTest.java b/oidfed-common-conf-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/cache/SubordinateStatementMetadataCacheTest.java
new file mode 100644
index 0000000..dda1519
--- /dev/null
+++ b/oidfed-common-conf-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/cache/SubordinateStatementMetadataCacheTest.java
@@ -0,0 +1,665 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.profile.flow.oidfed.cache;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.Mockito.when;
+
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.time.Instant;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.nimbusds.jose.EncryptionMethod;
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JOSEObjectType;
+import com.nimbusds.jose.JWEAlgorithm;
+import com.nimbusds.jose.JWEHeader;
+import com.nimbusds.jose.JWEObject;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.JWSHeader;
+import com.nimbusds.jose.Payload;
+import com.nimbusds.jose.crypto.RSAEncrypter;
+import com.nimbusds.jose.jwk.JWKSet;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.PlainJWT;
+import com.nimbusds.jwt.SignedJWT;
+
+import net.shibboleth.idp.plugin.oidc.op.profile.flow.oidfed.AbstractFederationFlowTest;
+import net.shibboleth.idp.plugin.oidc.op.profile.flow.oidfed.EntityConfigurationFlowTest;
+import net.shibboleth.idp.plugin.oidc.op.profile.flow.oidfed.support.CustomSubordinateStatementFilterStrategy;
+import net.shibboleth.oidc.metadata.cache.MetadataCache;
+import net.shibboleth.oidc.metadata.cache.MetadataCacheException;
+import net.shibboleth.oidfed.metadata.EntityStatement;
+import net.shibboleth.oidfed.metadata.SubordinateStatement;
+import net.shibboleth.oidfed.metadata.cache.IssuerEntityIDCriterion;
+import net.shibboleth.oidfed.metadata.cache.ResponseContainerExpirationCriterion;
+import net.shibboleth.oidfed.metadata.cache.SubjectEntityIDCriterion;
+import net.shibboleth.oidfed.metadata.cache.subordinate.SubordinateStatementContainer;
+import net.shibboleth.oidfed.test.TrustChainTestUtil;
+import net.shibboleth.shared.resolver.CriteriaSet;
+
+/**
+ * Unit tests for the default entity configuration metadata cache.
+ */
+ at SuppressWarnings("null")
+public class SubordinateStatementMetadataCacheTest extends AbstractFederationFlowTest {
+
+ protected SubordinateStatementMetadataCacheTest() {
+ super(EntityConfigurationFlowTest.FLOW_ID);
+ }
+
+ @Autowired
+ @Qualifier("shibboleth.oidfed.SubordinateEntityStatementMetadataCache")
+ MetadataCache<SubordinateStatementContainer> subordinateStatementCache;
+
+ @Test
+ public void testValidSubordinateStatement()
+ throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+ final String entityId = uniqueClientId();
+ final JWTClaimsSet.Builder builder = 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("federation_entity", Collections.emptyMap()));
+ final String subordinateStatement = TrustChainTestUtil.signedJwt(
+ JWSAlgorithm.RS256, trustedAnchorKey, "entity-statement+jwt", builder.build()).serialize();
+
+ mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+ mapResponse(subordinateStatementUrl(anchorFetchEndpoint, entityId),
+ mockResponse(subordinateStatement));
+ try {
+ final List<SubordinateStatementContainer> result =
+ subordinateStatementCache.get(new CriteriaSet(new SubjectEntityIDCriterion(entityId),
+ new ResponseContainerExpirationCriterion(Instant.now().plusSeconds(300)),
+ new IssuerEntityIDCriterion(anchorId)));
+ Assert.assertNotNull(result);
+ Assert.assertEquals(result.size(), 1);
+ final SubordinateStatement statement = result.get(0).getStatement();
+ Assert.assertNotNull(statement);
+ assert statement != null;
+ Assert.assertNull(statement.getParsedPayload().getCustomClaims()
+ .get(CustomSubordinateStatementFilterStrategy.CUSTOM_CLAIM_NAME));
+ } catch (MetadataCacheException e) {
+ Assert.fail("Could not resolve entity configuration", e);
+ }
+ }
+
+ @Test
+ public void testValidSubordinateStatement_clientAuthentication()
+ throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+ final String entityId = uniqueClientId();
+ final String intermediateId = uniqueIntermediateId();
+ final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(intermediateId).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("federation_entity", Collections.emptyMap()));
+ final String subordinateStatement = TrustChainTestUtil.signedJwt(
+ JWSAlgorithm.RS256, intermediateKey, "entity-statement+jwt", builder.build()).serialize();
+
+ final JWTClaimsSet.Builder ecBuilder = new JWTClaimsSet.Builder().issuer(intermediateId).subject(intermediateId)
+ .issueTime(Date.from(Instant.now()))
+ .expirationTime(Date.from(Instant.now().plusSeconds(300)))
+ .claim("jwks", new JWKSet(intermediateKey).toJSONObject(true))
+ .claim("metadata", Map.of("federation_entity", Map.of(
+ "federation_fetch_endpoint", intermediateId + "/fetch",
+ "federation_fetch_endpoint_auth_methods", List.of("private_key_jwt"),
+ "endpoint_auth_signing_alg_values_supported", List.of("ES512"))));
+ final EntityStatement<?> intermediateConfiguration =
+ TrustChainTestUtil.entityStatement(JWSAlgorithm.RS256, intermediateKey, ecBuilder.build());
+
+
+ mapResponse(entityConfigurationUrl(intermediateId), mockResponse(intermediateConfiguration.getJwt().serialize()));
+ // raw anchorFetchEndpoint URL as the request is POST
+ mapResponse(intermediateId + "/fetch", mockResponse(subordinateStatement));
+
+ try {
+ final List<SubordinateStatementContainer> result =
+ subordinateStatementCache.get(new CriteriaSet(new SubjectEntityIDCriterion(entityId),
+ new ResponseContainerExpirationCriterion(Instant.now().plusSeconds(300)),
+ new IssuerEntityIDCriterion(intermediateId)));
+ Assert.assertNotNull(result);
+ Assert.assertEquals(result.size(), 1);
+ final SubordinateStatement statement = result.get(0).getStatement();
+ Assert.assertNotNull(statement);
+ assert statement != null;
+ Assert.assertNull(statement.getParsedPayload().getCustomClaims()
+ .get(CustomSubordinateStatementFilterStrategy.CUSTOM_CLAIM_NAME));
+ } catch (MetadataCacheException e) {
+ Assert.fail("Could not resolve entity configuration", e);
+ }
+ }
+
+ @Test
+ public void testValidSubordinateStatement_customCriticalClaim()
+ throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+ final String entityId = uniqueClientId();
+ final JWTClaimsSet.Builder builder = 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("crit", List.of("subordinate_crit"))
+ .claim("metadata", Map.of("federation_entity", Collections.emptyMap()));
+ final String subordinateStatement = TrustChainTestUtil.signedJwt(
+ JWSAlgorithm.RS256, trustedAnchorKey, "entity-statement+jwt", builder.build()).serialize();
+
+ mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+ mapResponse(subordinateStatementUrl(anchorFetchEndpoint, entityId),
+ mockResponse(subordinateStatement));
+ try {
+ final List<SubordinateStatementContainer> result =
+ subordinateStatementCache.get(new CriteriaSet(new SubjectEntityIDCriterion(entityId),
+ new ResponseContainerExpirationCriterion(Instant.now().plusSeconds(300)),
+ new IssuerEntityIDCriterion(anchorId)));
+ Assert.assertNotNull(result);
+ Assert.assertEquals(result.size(), 1);
+ final SubordinateStatement statement = result.get(0).getStatement();
+ Assert.assertNotNull(statement);
+ assert statement != null;
+ Assert.assertEquals(statement.getParsedPayload().getCustomClaims()
+ .get(CustomSubordinateStatementFilterStrategy.CUSTOM_CLAIM_NAME),
+ CustomSubordinateStatementFilterStrategy.CUSTOM_CLAIM_VALUE);
+ } catch (MetadataCacheException e) {
+ Assert.fail("Could not resolve entity configuration", e);
+ }
+ }
+
+ @Test
+ public void testUnsignedSubordinateStatement()
+ throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+ final String entityId = uniqueClientId();
+ final JWTClaimsSet.Builder builder = 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("federation_entity", Collections.emptyMap()));
+ final String subordinateStatement = new PlainJWT(builder.build()).serialize();
+
+ mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+ mapResponse(subordinateStatementUrl(anchorFetchEndpoint, entityId),
+ mockResponse(subordinateStatement));
+ assertNoSubordinateStatement(entityId);
+ }
+
+ @Test
+ public void testEncryptedSubordinateStatement()
+ throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+ final String entityId = uniqueClientId();
+ final JWTClaimsSet.Builder builder = 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("federation_entity", Collections.emptyMap()));
+ final SignedJWT signedJwt = TrustChainTestUtil.signedJwt(
+ JWSAlgorithm.RS256, leafKey, "entity-statement+jwt", builder.build());
+ final JWEObject jwe = new JWEObject(new JWEHeader.Builder(JWEAlgorithm.RSA_OAEP_256, EncryptionMethod.A256GCM)
+ .contentType("JWT")
+ .build(),
+ new Payload(signedJwt));
+ try {
+ jwe.encrypt(new RSAEncrypter(leafKey.toRSAKey()));
+ } catch (JOSEException e) {
+ Assert.fail("Encryption failed", e);
+ }
+ final String subordinateStatement = jwe.serialize();
+
+ mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+ mapResponse(subordinateStatementUrl(anchorFetchEndpoint, entityId),
+ mockResponse(subordinateStatement));
+ assertNoSubordinateStatement(entityId);
+ }
+
+ @Test
+ public void testWithUnrecognizedCriticalClaim()
+ throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+ final String entityId = uniqueClientId();
+ final JWTClaimsSet.Builder builder = 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("crit", List.of("default_crit"))
+ .claim("metadata", Map.of("federation_entity", Collections.emptyMap()));
+ final String subordinateStatement = TrustChainTestUtil.signedJwt(
+ JWSAlgorithm.RS256, trustedAnchorKey, "entity-statement+jwt", builder.build()).serialize();
+
+ mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+ mapResponse(subordinateStatementUrl(anchorFetchEndpoint, entityId),
+ mockResponse(subordinateStatement));
+ assertNoSubordinateStatement(entityId);
+ }
+
+ @Test
+ public void testWithStandardClaimAsCriticalClaim()
+ throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+ final String entityId = uniqueClientId();
+ final JWTClaimsSet.Builder builder = 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("crit", List.of("jwks"))
+ .claim("metadata", Map.of("federation_entity", Collections.emptyMap()));
+ final String subordinateStatement = TrustChainTestUtil.signedJwt(
+ JWSAlgorithm.RS256, trustedAnchorKey, "entity-statement+jwt", builder.build()).serialize();
+
+ mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+ mapResponse(subordinateStatementUrl(anchorFetchEndpoint, entityId),
+ mockResponse(subordinateStatement));
+ assertNoSubordinateStatement(entityId);
+ }
+
+ @Test
+ public void testSignatureWithNonMathchingKey()
+ throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+ final String entityId = uniqueClientId();
+ final JWTClaimsSet.Builder builder = 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("federation_entity", Collections.emptyMap()));
+ final String subordinateStatement = TrustChainTestUtil.signedJwt(
+ JWSAlgorithm.RS256, leafKey, "entity-statement+jwt", builder.build()).serialize();
+
+ mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+ mapResponse(subordinateStatementUrl(anchorFetchEndpoint, entityId),
+ mockResponse(subordinateStatement));
+ assertNoSubordinateStatement(entityId);
+ }
+
+ @Test
+ public void testValidClaims_nonMatchingSubject()
+ throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+ final String entityId = uniqueClientId();
+ final String claimsEntityId = uniqueClientId();
+ final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(anchorId).subject(claimsEntityId)
+ .issueTime(Date.from(Instant.now()))
+ .expirationTime(Date.from(Instant.now().plusSeconds(300)))
+ .claim("jwks", new JWKSet(leafKey).toJSONObject(true))
+ .claim("metadata", Map.of("federation_entity", Collections.emptyMap()));
+ final String subordinateStatement = TrustChainTestUtil.signedJwt(
+ JWSAlgorithm.RS256, trustedAnchorKey, "entity-statement+jwt", builder.build()).serialize();
+
+ mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+ mapResponse(subordinateStatementUrl(anchorFetchEndpoint, entityId),
+ mockResponse(subordinateStatement));
+ assertNoSubordinateStatement(entityId);
+ }
+
+ @Test
+ public void testValidClaims_invalidTypeHeader()
+ throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+ final String entityId = uniqueClientId();
+ final JWTClaimsSet.Builder builder = 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("federation_entity", Collections.emptyMap()));
+ final String subordinateStatement = TrustChainTestUtil.signedJwt(
+ JWSAlgorithm.RS256, trustedAnchorKey, "jwt", builder.build()).serialize();
+
+ mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+ mapResponse(subordinateStatementUrl(anchorFetchEndpoint, entityId),
+ mockResponse(subordinateStatement));
+
+ assertNoSubordinateStatement(entityId);
+ }
+
+ @Test
+ public void testValidClaims_forbiddenHeader_trustChain()
+ throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+ final String entityId = uniqueClientId();
+ final JWTClaimsSet.Builder builder = 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("federation_entity", Collections.emptyMap()));
+ final JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.RS256)
+ .type(new JOSEObjectType("entity-statement+jwt"))
+ .keyID(trustedAnchorKey.getKeyID())
+ .customParam("trust_chain", "forbidden")
+ .build();
+ final String subordinateStatement = TrustChainTestUtil.signedJwt(
+ JWSAlgorithm.RS256, trustedAnchorKey, header, builder.build()).serialize();
+
+ mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+ mapResponse(subordinateStatementUrl(anchorFetchEndpoint, entityId),
+ mockResponse(subordinateStatement));
+
+ assertNoSubordinateStatement(entityId);
+ }
+
+ @Test
+ public void testValidClaims_forbiddenHeader_peerTrustChain()
+ throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+ final String entityId = uniqueClientId();
+ final JWTClaimsSet.Builder builder = 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("federation_entity", Collections.emptyMap()));
+ final JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.RS256)
+ .type(new JOSEObjectType("entity-statement+jwt"))
+ .keyID(trustedAnchorKey.getKeyID())
+ .customParam("peer_trust_chain", "forbidden")
+ .build();
+ final String subordinateStatement = TrustChainTestUtil.signedJwt(
+ JWSAlgorithm.RS256, trustedAnchorKey, header, builder.build()).serialize();
+
+ mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+ mapResponse(subordinateStatementUrl(anchorFetchEndpoint, entityId),
+ mockResponse(subordinateStatement));
+
+ assertNoSubordinateStatement(entityId);
+ }
+
+ @Test
+ public void testIssuedInFuture()
+ throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+ final String entityId = uniqueClientId();
+ final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(anchorId).subject(entityId)
+ .issueTime(Date.from(Instant.now().plusSeconds(300)))
+ .expirationTime(Date.from(Instant.now().plusSeconds(300)))
+ .claim("jwks", new JWKSet(leafKey).toJSONObject(true))
+ .claim("metadata", Map.of("federation_entity", Collections.emptyMap()));
+ final String subordinateStatement = TrustChainTestUtil.signedJwt(
+ JWSAlgorithm.RS256, trustedAnchorKey, "entity-statement+jwt", builder.build()).serialize();
+
+ mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+ mapResponse(subordinateStatementUrl(anchorFetchEndpoint, entityId),
+ mockResponse(subordinateStatement));
+ assertNoSubordinateStatement(entityId);
+ }
+
+ @Test
+ public void testMissingSub()
+ throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+ final String entityId = uniqueClientId();
+ final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(anchorId)
+ .issueTime(Date.from(Instant.now()))
+ .expirationTime(Date.from(Instant.now().plusSeconds(300)))
+ .claim("jwks", new JWKSet(leafKey).toJSONObject(true))
+ .claim("metadata", Map.of("federation_entity", Collections.emptyMap()));
+ final String subordinateStatement = TrustChainTestUtil.signedJwt(
+ JWSAlgorithm.RS256, trustedAnchorKey, "entity-statement+jwt", builder.build()).serialize();
+
+ mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+ mapResponse(subordinateStatementUrl(anchorFetchEndpoint, entityId),
+ mockResponse(subordinateStatement));
+ assertNoSubordinateStatement(entityId);
+ }
+
+ @Test
+ public void testExpired()
+ throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+ final String entityId = uniqueClientId();
+ final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(anchorId).subject(entityId)
+ .issueTime(Date.from(Instant.now()))
+ .expirationTime(Date.from(Instant.now().minusSeconds(300)))
+ .claim("jwks", new JWKSet(leafKey).toJSONObject(true))
+ .claim("metadata", Map.of("federation_entity", Collections.emptyMap()));
+ final String subordinateStatement = TrustChainTestUtil.signedJwt(
+ JWSAlgorithm.RS256, trustedAnchorKey, "entity-statement+jwt", builder.build()).serialize();
+
+ mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+ mapResponse(subordinateStatementUrl(anchorFetchEndpoint, entityId),
+ mockResponse(subordinateStatement));
+ assertNoSubordinateStatement(entityId);
+ }
+
+ @Test
+ public void testMissingJwks()
+ throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+ final String entityId = uniqueClientId();
+ final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(anchorId).subject(entityId)
+ .issueTime(Date.from(Instant.now()))
+ .expirationTime(Date.from(Instant.now().plusSeconds(300)))
+ .claim("metadata", Map.of("federation_entity", Collections.emptyMap()));
+ final String subordinateStatement = TrustChainTestUtil.signedJwt(
+ JWSAlgorithm.RS256, trustedAnchorKey, "entity-statement+jwt", builder.build()).serialize();
+
+ mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+ mapResponse(subordinateStatementUrl(anchorFetchEndpoint, entityId),
+ mockResponse(subordinateStatement));
+ assertNoSubordinateStatement(entityId);
+ }
+
+ @Test
+ public void testInvalidConstraints_maxPathLength()
+ throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+ final String entityId = uniqueClientId();
+ final JWTClaimsSet.Builder builder = 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("constraints", Map.of("max_path_length", "non_integer"))
+ .claim("metadata", Map.of("federation_entity", Collections.emptyMap()));
+ final String subordinateStatement = TrustChainTestUtil.signedJwt(
+ JWSAlgorithm.RS256, trustedAnchorKey, "entity-statement+jwt", builder.build()).serialize();
+
+ mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+ mapResponse(subordinateStatementUrl(anchorFetchEndpoint, entityId),
+ mockResponse(subordinateStatement));
+ assertNoSubordinateStatement(entityId);
+ }
+
+ @Test
+ public void testInvalidConstraints_namingConstraint()
+ throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+ final String entityId = uniqueClientId();
+ final JWTClaimsSet.Builder builder = 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("constraints", Map.of("naming_constraints", "not_map"))
+ .claim("metadata", Map.of("federation_entity", Collections.emptyMap()));
+ final String subordinateStatement = TrustChainTestUtil.signedJwt(
+ JWSAlgorithm.RS256, trustedAnchorKey, "entity-statement+jwt", builder.build()).serialize();
+
+ mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+ mapResponse(subordinateStatementUrl(anchorFetchEndpoint, entityId),
+ mockResponse(subordinateStatement));
+ assertNoSubordinateStatement(entityId);
+ }
+
+ @Test
+ public void testInvalidConstraints_allowedEntityTypes()
+ throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+ final String entityId = uniqueClientId();
+ final JWTClaimsSet.Builder builder = 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("constraints", Map.of("allowed_entity_types", "not_list"))
+ .claim("metadata", Map.of("federation_entity", Collections.emptyMap()));
+ final String subordinateStatement = TrustChainTestUtil.signedJwt(
+ JWSAlgorithm.RS256, trustedAnchorKey, "entity-statement+jwt", builder.build()).serialize();
+
+ mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+ mapResponse(subordinateStatementUrl(anchorFetchEndpoint, entityId),
+ mockResponse(subordinateStatement));
+ assertNoSubordinateStatement(entityId);
+ }
+
+ @Test
+ public void testValidConstraints()
+ throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+ final String entityId = uniqueClientId();
+ final Map<String,Object> constraints = new HashMap<>();
+ constraints.put("max_path_length", 5);
+ constraints.put("allowed_entity_types", List.of("openid_relying_party"));
+ constraints.put("naming_constraints", Map.of("permitted", List.of(".example.com")));
+ final JWTClaimsSet.Builder builder = 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("constraints", constraints)
+ .claim("metadata", Map.of("federation_entity", Collections.emptyMap()));
+ final String subordinateStatement = TrustChainTestUtil.signedJwt(
+ JWSAlgorithm.RS256, trustedAnchorKey, "entity-statement+jwt", builder.build()).serialize();
+
+ mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+ mapResponse(subordinateStatementUrl(anchorFetchEndpoint, entityId),
+ mockResponse(subordinateStatement));
+ try {
+ final List<SubordinateStatementContainer> result =
+ subordinateStatementCache.get(new CriteriaSet(new SubjectEntityIDCriterion(entityId),
+ new ResponseContainerExpirationCriterion(Instant.now().plusSeconds(300)),
+ new IssuerEntityIDCriterion(anchorId)));
+ Assert.assertNotNull(result);
+ Assert.assertEquals(result.size(), 1);
+ final SubordinateStatement statement = result.get(0).getStatement();
+ Assert.assertNotNull(statement);
+ assert statement != null;
+ Assert.assertNull(statement.getParsedPayload().getCustomClaims()
+ .get(CustomSubordinateStatementFilterStrategy.CUSTOM_CLAIM_NAME));
+ } catch (MetadataCacheException e) {
+ Assert.fail("Could not resolve entity configuration", e);
+ }
+ }
+
+ @Test
+ public void testForbiddenClaim_trustMark()
+ throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+ final String entityId = uniqueClientId();
+ final String trustMark = TrustChainTestUtil.trustMark(JWSAlgorithm.RS256, trustMarkIssuerKey, trustMarkIssuerId,
+ entityId, "https://example.org/email-allowing-trust-mark", Instant.now().plusSeconds(300)).serialize();
+ final JWTClaimsSet.Builder builder = 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("trust_marks", List.of(Map.of(
+ "trust_mark_type", "https://example.org/email-allowing-trust-mark",
+ "trust_mark", trustMark)))
+ .claim("metadata", Map.of("federation_entity", Collections.emptyMap()));
+ final String subordinateStatement = TrustChainTestUtil.signedJwt(
+ JWSAlgorithm.RS256, trustedAnchorKey, "entity-statement+jwt", builder.build()).serialize();
+
+ mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+ mapResponse(subordinateStatementUrl(anchorFetchEndpoint, entityId),
+ mockResponse(subordinateStatement));
+ assertNoSubordinateStatement(entityId);
+ }
+
+ @Test
+ public void testForbiddenClaim_trustMarkIssuers()
+ throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+ final String entityId = uniqueClientId();
+ final JWTClaimsSet.Builder builder = 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("trust_mark_issuers", List.of(trustMarkIssuerId))
+ .claim("metadata", Map.of("federation_entity", Collections.emptyMap()));
+ final String subordinateStatement = TrustChainTestUtil.signedJwt(
+ JWSAlgorithm.RS256, trustedAnchorKey, "entity-statement+jwt", builder.build()).serialize();
+
+ mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+ mapResponse(subordinateStatementUrl(anchorFetchEndpoint, entityId),
+ mockResponse(subordinateStatement));
+ assertNoSubordinateStatement(entityId);
+ }
+
+ @Test
+ public void testForbiddenClaim_trustMarkOwners()
+ throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+ final String entityId = uniqueClientId();
+ final Map<String,Object> trustMarkOwner = new HashMap<>();
+ trustMarkOwner.put("sub", trustMarkIssuerId);
+ trustMarkOwner.put("jwks", new JWKSet(leafKey).toJSONObject(true));
+ final JWTClaimsSet.Builder builder = 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("trust_mark_owners", Map.of("https://example.org/email-allowing-trust-mark", trustMarkOwner))
+ .claim("metadata", Map.of("federation_entity", Collections.emptyMap()));
+ final String subordinateStatement = TrustChainTestUtil.signedJwt(
+ JWSAlgorithm.RS256, trustedAnchorKey, "entity-statement+jwt", builder.build()).serialize();
+
+ mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+ mapResponse(subordinateStatementUrl(anchorFetchEndpoint, entityId),
+ mockResponse(subordinateStatement));
+ assertNoSubordinateStatement(entityId);
+ }
+
+ @Test
+ public void testInvalidJwksClaim()
+ throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+ final String entityId = uniqueClientId();
+ final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(anchorId).subject(entityId)
+ .issueTime(Date.from(Instant.now()))
+ .expirationTime(Date.from(Instant.now().plusSeconds(300)))
+ .claim("jwks", Map.of("federation_entity", Collections.emptyMap()))
+ .claim("metadata", Map.of("federation_entity", Collections.emptyMap()));
+ final String subordinateStatement = TrustChainTestUtil.signedJwt(
+ JWSAlgorithm.RS256, trustedAnchorKey, "entity-statement+jwt", builder.build()).serialize();
+
+ mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+ mapResponse(subordinateStatementUrl(anchorFetchEndpoint, entityId),
+ mockResponse(subordinateStatement));
+ assertNoSubordinateStatement(entityId);
+ }
+
+ @Test
+ public void testNullResponse()
+ throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+ final String entityId = uniqueClientId();
+ mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+ assertNoSubordinateStatement(entityId, false);
+ }
+
+ @Test
+ public void testExceptionResponse()
+ throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+ final String entityId = uniqueClientId();
+ mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+ when(federationHttpClient.executeOpen(any(),
+ argThat(new RequestUriMatcher(subordinateStatementUrl(anchorFetchEndpoint, entityId))),
+ any())).thenThrow(IOException.class);
+ assertNoSubordinateStatement(entityId, false);
+ }
+
+ protected void assertNoSubordinateStatement(final String entityId) {
+ assertNoSubordinateStatement(entityId, true);
+ }
+
+ protected void assertNoSubordinateStatement(final String entityId, final boolean containerExists) {
+ try {
+ final List<SubordinateStatementContainer> result =
+ subordinateStatementCache.get(new CriteriaSet(new SubjectEntityIDCriterion(entityId),
+ new ResponseContainerExpirationCriterion(Instant.now().plusSeconds(300)),
+ new IssuerEntityIDCriterion(anchorId)));
+ Assert.assertNotNull(result);
+ if (containerExists) {
+ Assert.assertEquals(result.size(), 1);
+ Assert.assertNull(result.get(0).getStatement());
+ } else {
+ Assert.assertEquals(result.size(), 0);
+ }
+ } catch (MetadataCacheException e) {
+ Assert.fail("Could not resolve entity configuration", e);
+ }
+
+ }
+}
diff --git a/oidfed-common-conf-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/cache/TrustChainMetadataCacheTest.java b/oidfed-common-conf-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/cache/TrustChainMetadataCacheTest.java
new file mode 100644
index 0000000..d543502
--- /dev/null
+++ b/oidfed-common-conf-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/cache/TrustChainMetadataCacheTest.java
@@ -0,0 +1,274 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.profile.flow.oidfed.cache;
+
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.time.Instant;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.jwk.JWKSet;
+import com.nimbusds.jwt.JWTClaimsSet;
+
+import net.shibboleth.idp.plugin.oidc.op.profile.flow.oidfed.AbstractFederationFlowTest;
+import net.shibboleth.idp.plugin.oidc.op.profile.flow.oidfed.EntityConfigurationFlowTest;
+import net.shibboleth.oidc.metadata.cache.MetadataCache;
+import net.shibboleth.oidc.metadata.cache.MetadataCacheException;
+import net.shibboleth.oidfed.metadata.cache.IssuerEntityIDCriterion;
+import net.shibboleth.oidfed.metadata.cache.ResponseContainerExpirationCriterion;
+import net.shibboleth.oidfed.metadata.cache.SubjectEntityIDCriterion;
+import net.shibboleth.oidfed.metadata.cache.trustchain.TrustChainsContainer;
+import net.shibboleth.oidfed.test.TrustChainTestUtil;
+import net.shibboleth.shared.resolver.CriteriaSet;
+
+/**
+ * Unit tests for the default entity configuration metadata cache.
+ */
+ at SuppressWarnings("null")
+public class TrustChainMetadataCacheTest extends AbstractFederationFlowTest {
+
+ protected TrustChainMetadataCacheTest() {
+ super(EntityConfigurationFlowTest.FLOW_ID);
+ }
+
+ @Autowired
+ @Qualifier("shibboleth.oidfed.TrustChainMetadataCache")
+ MetadataCache<TrustChainsContainer> trustChainCache;
+
+ @Test
+ public void testValidTrustChainResolved()
+ throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+ final String entityId = uniqueClientId();
+
+ final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(entityId).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("federation_entity", Collections.emptyMap()))
+ .claim("authority_hints", List.of(anchorId));
+ final String entityConfiguration = TrustChainTestUtil.signedJwt(
+ JWSAlgorithm.RS256, leafKey, "entity-statement+jwt", builder.build()).serialize();
+
+ mapResponse(entityConfigurationUrl(entityId), mockResponse(entityConfiguration));
+
+ final JWTClaimsSet.Builder builder2 = 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("federation_entity", Collections.emptyMap()));
+ final String subordinateStatement = TrustChainTestUtil.signedJwt(
+ JWSAlgorithm.RS256, trustedAnchorKey, "entity-statement+jwt", builder2.build()).serialize();
+
+ mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+ mapResponse(subordinateStatementUrl(anchorFetchEndpoint, entityId),
+ mockResponse(subordinateStatement));
+ try {
+ final List<TrustChainsContainer> result =
+ trustChainCache.get(new CriteriaSet(new SubjectEntityIDCriterion(entityId),
+ new ResponseContainerExpirationCriterion(Instant.now().plusSeconds(300)),
+ new IssuerEntityIDCriterion(anchorId)));
+ Assert.assertNotNull(result);
+ Assert.assertEquals(result.size(), 1);
+ Assert.assertNotNull(result.get(0).getTrustChains());
+ Assert.assertEquals(result.get(0).getTrustChains().size(), 1);
+ } catch (MetadataCacheException e) {
+ Assert.fail("Could not resolve entity configuration", e);
+ }
+ }
+
+ @Test(enabled = false)
+ //TODO: enable once the entity configuration cache can be cleaned before/after the test
+ //otherwise the non-compatible trust anchor configuration may be in the cache
+ public void testTrustAnchorConfigurationSignatureWithUntrustedKey()
+ throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+ final String entityId = uniqueClientId();
+
+ final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(entityId).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("federation_entity", Collections.emptyMap()))
+ .claim("authority_hints", List.of(anchorId));
+ final String entityConfiguration = TrustChainTestUtil.signedJwt(
+ JWSAlgorithm.RS256, leafKey, "entity-statement+jwt", builder.build()).serialize();
+
+ mapResponse(entityConfigurationUrl(entityId), mockResponse(entityConfiguration));
+
+ final JWTClaimsSet.Builder builder2 = 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("federation_entity", Collections.emptyMap()));
+ final String subordinateStatement = TrustChainTestUtil.signedJwt(
+ JWSAlgorithm.RS256, trustedAnchorKey, "entity-statement+jwt", builder2.build()).serialize();
+
+ mapResponse(entityConfigurationUrl(anchorId),
+ mockResponse(trustedAnchorConfiguration(null, null, anchorKey, new JWKSet(anchorKey))));
+ mapResponse(subordinateStatementUrl(anchorFetchEndpoint, entityId),
+ mockResponse(subordinateStatement));
+ try {
+ final List<TrustChainsContainer> result =
+ trustChainCache.get(new CriteriaSet(new SubjectEntityIDCriterion(entityId),
+ new ResponseContainerExpirationCriterion(Instant.now().plusSeconds(300)),
+ new IssuerEntityIDCriterion(anchorId)));
+ Assert.assertNotNull(result);
+ Assert.assertEquals(result.size(), 1);
+ Assert.assertNotNull(result.get(0).getTrustChains());
+ Assert.assertEquals(result.get(0).getTrustChains().size(), 0);
+ } catch (MetadataCacheException e) {
+ Assert.fail("Could not resolve entity configuration", e);
+ }
+ }
+
+ @Test(enabled = false)
+ //TODO: enable once the trust anchor cache can be cleaned before/after the test
+ //otherwise the non-compatible trust anchor configuration may be in the cache
+ public void testTrustAnchorSubordinateSignatureWithUntrustedKey()
+ throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+ final String entityId = uniqueClientId();
+
+ final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(entityId).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("federation_entity", Collections.emptyMap()))
+ .claim("authority_hints", List.of(anchorId));
+ final String entityConfiguration = TrustChainTestUtil.signedJwt(
+ JWSAlgorithm.RS256, leafKey, "entity-statement+jwt", builder.build()).serialize();
+
+ mapResponse(entityConfigurationUrl(entityId), mockResponse(entityConfiguration));
+
+ final JWTClaimsSet.Builder builder2 = 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("federation_entity", Collections.emptyMap()));
+ final String subordinateStatement = TrustChainTestUtil.signedJwt(
+ JWSAlgorithm.RS256, anchorKey, "entity-statement+jwt", builder2.build()).serialize();
+
+ mapResponse(entityConfigurationUrl(anchorId),
+ mockResponse(trustedAnchorConfiguration(null, null, trustedAnchorKey,
+ new JWKSet(List.of(trustedAnchorKey, anchorKey)))));
+ mapResponse(subordinateStatementUrl(anchorFetchEndpoint, entityId),
+ mockResponse(subordinateStatement));
+ try {
+ final List<TrustChainsContainer> result =
+ trustChainCache.get(new CriteriaSet(new SubjectEntityIDCriterion(entityId),
+ new ResponseContainerExpirationCriterion(Instant.now().plusSeconds(300)),
+ new IssuerEntityIDCriterion(anchorId)));
+ Assert.assertNotNull(result);
+ Assert.assertEquals(result.size(), 1);
+ Assert.assertNotNull(result.get(0).getTrustChains());
+ Assert.assertEquals(result.get(0).getTrustChains().size(), 0);
+ } catch (MetadataCacheException e) {
+ Assert.fail("Could not resolve entity configuration", e);
+ }
+ }
+
+ @Test
+ public void testValidTrustChainResolved_intermediateWithBrokenAuthorityHint()
+ throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+ final String entityId = uniqueClientId();
+
+ final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(entityId).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("federation_entity", Collections.emptyMap()))
+ .claim("authority_hints", List.of(trustedIntermediateId));
+ final String entityConfiguration = TrustChainTestUtil.signedJwt(
+ JWSAlgorithm.RS256, leafKey, "entity-statement+jwt", builder.build()).serialize();
+
+ mapResponse(entityConfigurationUrl(entityId), mockResponse(entityConfiguration));
+ mapResponse(entityConfigurationUrl(trustedIntermediateId),
+ mockResponse(intermediateConfiguration(trustedIntermediateId,
+ new JWKSet(List.of(intermediateKey, trustedIntermediateKey)), trustedIntermediateKey)));
+
+ final JWTClaimsSet.Builder builder2 = new JWTClaimsSet.Builder().issuer(trustedIntermediateId)
+ .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("federation_entity", Collections.emptyMap()));
+ final String subordinateStatement = TrustChainTestUtil.signedJwt(
+ JWSAlgorithm.RS256, trustedIntermediateKey, "entity-statement+jwt", builder2.build()).serialize();
+
+ mapResponse(subordinateStatementUrl(trustedIntermediateId + "/fetch", entityId),
+ mockResponse(subordinateStatement));
+
+ mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+
+ try {
+ final List<TrustChainsContainer> result =
+ trustChainCache.get(new CriteriaSet(new SubjectEntityIDCriterion(entityId)));
+ Assert.assertNotNull(result);
+ Assert.assertEquals(result.size(), 1);
+ Assert.assertNotNull(result.get(0).getTrustChains());
+ Assert.assertEquals(result.get(0).getTrustChains().size(), 1);
+ } catch (MetadataCacheException e) {
+ Assert.fail("Could not resolve entity configuration", e);
+ }
+ }
+
+ @Test
+ public void testValidTrustChainResolved_customFilterReturnsNull()
+ throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+ final String entityId = uniqueClientId();
+
+ final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(entityId).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("federation_entity", Collections.emptyMap()))
+ .claim("crit", List.of("default_crit"))
+ .claim("authority_hints", List.of(anchorId));
+ final String entityConfiguration = TrustChainTestUtil.signedJwt(
+ JWSAlgorithm.RS256, leafKey, "entity-statement+jwt", builder.build()).serialize();
+
+ mapResponse(entityConfigurationUrl(entityId), mockResponse(entityConfiguration));
+
+ final JWTClaimsSet.Builder builder2 = 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("federation_entity", Collections.emptyMap()));
+ final String subordinateStatement = TrustChainTestUtil.signedJwt(
+ JWSAlgorithm.RS256, trustedAnchorKey, "entity-statement+jwt", builder2.build()).serialize();
+
+ mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+ mapResponse(subordinateStatementUrl(anchorFetchEndpoint, entityId),
+ mockResponse(subordinateStatement));
+ try {
+ final List<TrustChainsContainer> result =
+ trustChainCache.get(new CriteriaSet(new SubjectEntityIDCriterion(entityId),
+ new ResponseContainerExpirationCriterion(Instant.now().plusSeconds(300)),
+ new IssuerEntityIDCriterion(anchorId)));
+ Assert.assertNotNull(result);
+ Assert.assertEquals(result.size(), 0);
+ } catch (MetadataCacheException e) {
+ Assert.fail("Could not resolve entity configuration", e);
+ }
+ }
+
+}
diff --git a/oidfed-common-conf-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/support/CustomEntityConfigurationFilterStrategy.java b/oidfed-common-conf-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/support/CustomEntityConfigurationFilterStrategy.java
new file mode 100644
index 0000000..040a963
--- /dev/null
+++ b/oidfed-common-conf-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/support/CustomEntityConfigurationFilterStrategy.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.profile.flow.oidfed.support;
+
+import java.time.Instant;
+import java.util.Optional;
+import java.util.function.BiFunction;
+
+import net.shibboleth.oidc.metadata.filter.MetadataFilterContext;
+import net.shibboleth.oidfed.metadata.EntityConfiguration;
+import net.shibboleth.oidfed.metadata.cache.configuration.EntityConfigurationContainer;
+import net.shibboleth.oidfed.metadata.impl.EntityConfigurationImpl;
+import net.shibboleth.oidfed.metadata.payload.impl.EntityConfigurationPayloadImpl;
+
+/**
+ * Custom filter strategy for entity configuration container.
+ */
+public class CustomEntityConfigurationFilterStrategy implements
+ BiFunction<EntityConfigurationContainer, MetadataFilterContext, EntityConfigurationContainer>{
+
+ public static final String CUSTOM_CLAIM_NAME = "custom_claim";
+ public static final String CUSTOM_CLAIM_VALUE = "custom_value";
+
+ /** {@inheritDoc} */
+ @Override
+ public EntityConfigurationContainer apply(final EntityConfigurationContainer container,
+ final MetadataFilterContext context) {
+ final EntityConfiguration statement = container.getStatement();
+ if (statement == null) {
+ return null;
+ }
+ if (Optional.ofNullable(statement.getParsedPayload().getCritical())
+ .map(list -> list.contains("default_crit"))
+ .orElse(false)) {
+ final EntityConfigurationPayloadImpl payload =
+ new EntityConfigurationPayloadImpl(statement.getParsedPayload());
+ payload.setCustomClaims(CUSTOM_CLAIM_NAME, CUSTOM_CLAIM_VALUE);
+ final Instant expiration = Instant.now().plusSeconds(300);
+ assert expiration != null;
+ return new EntityConfigurationContainer(statement.getSubject(),
+ new EntityConfigurationImpl(statement.getJwt(), payload), expiration, expiration);
+ }
+ return container;
+ }
+
+}
diff --git a/oidfed-common-conf-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/support/CustomEntityConfigurationMetadataDecorator.java b/oidfed-common-conf-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/support/CustomEntityConfigurationMetadataDecorator.java
new file mode 100644
index 0000000..cc9d43a
--- /dev/null
+++ b/oidfed-common-conf-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/support/CustomEntityConfigurationMetadataDecorator.java
@@ -0,0 +1,46 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.profile.flow.oidfed.support;
+
+import java.util.Map;
+
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import net.shibboleth.oidfed.profile.EntityConfigurationMetadataDecorator;
+import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
+import net.shibboleth.shared.logic.ConstraintViolationException;
+
+public class CustomEntityConfigurationMetadataDecorator extends AbstractIdentifiableInitializableComponent
+ implements EntityConfigurationMetadataDecorator {
+
+ public final static String CUSTOM_ENTITY_TYPE = "custom_entity_type";
+
+ /** {@inheritDoc} */
+ @Override
+ public void accept(@Nullable final Map<String, Map<String, Object>> metadata,
+ @Nullable final ProfileRequestContext profileRequestContext) {
+ if (metadata == null) {
+ throw new ConstraintViolationException("Metadata cannot be null");
+ }
+ if (metadata.containsKey(CUSTOM_ENTITY_TYPE)) {
+ metadata.get(CUSTOM_ENTITY_TYPE).putAll(Map.of("key1", "value1", "key2", "value2"));
+ } else {
+ metadata.put(CUSTOM_ENTITY_TYPE, Map.of("key1", "value1", "key2", "value2"));
+ }
+ }
+
+}
diff --git a/oidfed-common-conf-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/support/CustomSubordinateStatementFilterStrategy.java b/oidfed-common-conf-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/support/CustomSubordinateStatementFilterStrategy.java
new file mode 100644
index 0000000..a5396cf
--- /dev/null
+++ b/oidfed-common-conf-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/support/CustomSubordinateStatementFilterStrategy.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.profile.flow.oidfed.support;
+
+import java.time.Instant;
+import java.util.Optional;
+import java.util.function.BiFunction;
+
+import net.shibboleth.oidc.metadata.filter.MetadataFilterContext;
+import net.shibboleth.oidfed.metadata.SubordinateStatement;
+import net.shibboleth.oidfed.metadata.cache.subordinate.SubordinateStatementContainer;
+import net.shibboleth.oidfed.metadata.impl.SubordinateStatementImpl;
+import net.shibboleth.oidfed.metadata.payload.impl.SubordinateStatementPayloadImpl;
+
+/**
+ * Custom filter strategy for subordinate statement container.
+ */
+public class CustomSubordinateStatementFilterStrategy implements
+ BiFunction<SubordinateStatementContainer, MetadataFilterContext, SubordinateStatementContainer>{
+
+ public static final String CUSTOM_CLAIM_NAME = "custom_so_claim";
+ public static final String CUSTOM_CLAIM_VALUE = "custom_so_value";
+
+ /** {@inheritDoc} */
+ @Override
+ public SubordinateStatementContainer apply(final SubordinateStatementContainer container,
+ final MetadataFilterContext context) {
+ final SubordinateStatement statement = container.getStatement();
+ if (statement == null) {
+ return null;
+ }
+ if (Optional.ofNullable(statement.getParsedPayload().getCritical())
+ .map(list -> list.contains("subordinate_crit"))
+ .orElse(false)) {
+ final SubordinateStatementPayloadImpl payload =
+ new SubordinateStatementPayloadImpl(statement.getParsedPayload());
+ payload.setCustomClaims(CUSTOM_CLAIM_NAME, CUSTOM_CLAIM_VALUE);
+ final Instant expiration = Instant.now().plusSeconds(300);
+ assert expiration != null;
+ return new SubordinateStatementContainer(container.getIdentifier(),
+ new SubordinateStatementImpl(statement.getJwt(), payload), expiration, expiration);
+ }
+ return container;
+ }
+
+}
diff --git a/oidfed-common-conf-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/support/CustomTrustChainFilterStrategy.java b/oidfed-common-conf-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/support/CustomTrustChainFilterStrategy.java
new file mode 100644
index 0000000..d4bbe65
--- /dev/null
+++ b/oidfed-common-conf-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/support/CustomTrustChainFilterStrategy.java
@@ -0,0 +1,47 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.profile.flow.oidfed.support;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.function.BiFunction;
+
+import net.shibboleth.oidc.metadata.filter.MetadataFilterContext;
+import net.shibboleth.oidfed.metadata.EntityStatement;
+import net.shibboleth.oidfed.metadata.cache.trustchain.TrustChainsContainer;
+import net.shibboleth.shared.collection.CollectionSupport;
+
+/**
+ * Custom filter strategy for trust chains container.
+ */
+public class CustomTrustChainFilterStrategy implements
+ BiFunction<TrustChainsContainer, MetadataFilterContext, TrustChainsContainer>{
+
+ /** {@inheritDoc} */
+ @Override
+ public TrustChainsContainer apply(final TrustChainsContainer container,
+ final MetadataFilterContext context) {
+ final List<List<EntityStatement<?>>> trustChains =
+ Optional.ofNullable(container.getTrustChains()).orElse(CollectionSupport.emptyList());
+ for (final List<EntityStatement<?>> trustChain : trustChains) {
+ final List<String> critical = trustChain.get(0).getParsedPayload().getCritical();
+ if (critical != null && critical.contains("default_crit")) {
+ return null;
+ }
+ }
+ return container;
+ }
+
+}
diff --git a/oidfed-common-conf-impl/src/test/java/net/shibboleth/oidfed/test/TrustChainTestUtil.java b/oidfed-common-conf-impl/src/test/java/net/shibboleth/oidfed/test/TrustChainTestUtil.java
new file mode 100644
index 0000000..833f26f
--- /dev/null
+++ b/oidfed-common-conf-impl/src/test/java/net/shibboleth/oidfed/test/TrustChainTestUtil.java
@@ -0,0 +1,164 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.oidfed.test;
+
+import java.text.ParseException;
+import java.time.Instant;
+import java.util.Date;
+import java.util.List;
+
+import javax.annotation.Nonnull;
+
+import org.testng.Assert;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.module.SimpleAbstractTypeResolver;
+import com.fasterxml.jackson.databind.module.SimpleModule;
+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.JWK;
+import com.nimbusds.jose.jwk.JWKSet;
+import com.nimbusds.jose.jwk.KeyUse;
+import com.nimbusds.jose.jwk.RSAKey;
+import com.nimbusds.jose.jwk.gen.RSAKeyGenerator;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.SignedJWT;
+
+import net.shibboleth.oidc.metadata.policy.MetadataPolicy;
+import net.shibboleth.oidfed.metadata.EntityStatement;
+import net.shibboleth.oidfed.metadata.impl.EntityConfigurationImpl;
+import net.shibboleth.oidfed.metadata.impl.SubordinateStatementImpl;
+import net.shibboleth.oidfed.metadata.jackson.InstantDeserializer;
+import net.shibboleth.oidfed.metadata.jackson.JWKSetDeserializer;
+import net.shibboleth.oidfed.metadata.payload.claim.Metadata;
+import net.shibboleth.oidfed.metadata.payload.claim.impl.MetadataImpl;
+import net.shibboleth.oidfed.metadata.policy.FederationMetadataPolicyDeserializer;
+
+/**
+ * Various utility methods for testing trust chains.
+ */
+public class TrustChainTestUtil {
+
+ public static List<EntityStatement<?>> chainWithIntermediate(final EntityStatement<?> leaf, final String anchorId,
+ final String intermediateId) {
+ try {
+ final RSAKey anchorKey = new RSAKeyGenerator(2048)
+ .keyID("mockTrustAnchorKey")
+ .keyUse(KeyUse.SIGNATURE)
+ .generate();
+ final RSAKey intermediateKey = new RSAKeyGenerator(2048)
+ .keyID("mockIntermediateKey")
+ .keyUse(KeyUse.SIGNATURE)
+ .generate();
+ final EntityStatement<?> trustAnchor = trustAnchor(JWSAlgorithm.RS256, anchorKey, anchorId);
+ final EntityStatement<?> intermediateStatement = entityStatement(JWSAlgorithm.RS256, anchorKey,
+ new JWTClaimsSet.Builder()
+ .subject(intermediateId)
+ .issueTime(new Date())
+ .issuer(anchorId)
+ .build());
+ final EntityStatement<?> leafSubordinateStatement = entityStatement(JWSAlgorithm.RS256, intermediateKey,
+ new JWTClaimsSet.Builder(leaf.getJwt().getJWTClaimsSet())
+ .subject(leaf.getSubject())
+ .issueTime(new Date())
+ .issuer(intermediateId)
+ .build());
+ return List.of(leaf, leafSubordinateStatement, intermediateStatement, trustAnchor);
+ } catch (final JOSEException | ParseException e) {
+ Assert.fail("Could not construct trust chain", e);
+ }
+ return null;
+ }
+
+ public static EntityStatement<?> trustAnchor(final JWSAlgorithm algorithm, final JWK jwk, final String entityId) {
+ return entityStatement(algorithm, jwk,
+ new JWTClaimsSet.Builder()
+ .subject(entityId)
+ .issueTime(new Date())
+ .issuer(entityId)
+ .build());
+ }
+
+ public static EntityStatement<?> entityStatement(final JWSAlgorithm algorithm, final JWK jwk,
+ final JWTClaimsSet claimsSet) {
+ return entityStatement(signedJwt(algorithm, jwk, "entity-statement+jwt", claimsSet));
+ }
+
+ public static EntityStatement<?> entityStatement(final SignedJWT jwt) {
+ try {
+ if (jwt.getJWTClaimsSet().getSubject().equals(jwt.getJWTClaimsSet().getIssuer())) {
+ return EntityConfigurationImpl.parse(jwt, payloadObjectMapper());
+ }
+ return SubordinateStatementImpl.parse(jwt, payloadObjectMapper());
+ } catch (JsonProcessingException | ParseException e) {
+ Assert.fail("Could not construct entity configuration", e);
+ }
+ return null;
+
+ }
+
+ @Nonnull public static ObjectMapper payloadObjectMapper() {
+ final SimpleModule jacksonModule = new SimpleModule();
+ jacksonModule.addDeserializer(MetadataPolicy.class, new FederationMetadataPolicyDeserializer("scope"));
+ jacksonModule.addDeserializer(JWKSet.class, new JWKSetDeserializer());
+ jacksonModule.addDeserializer(Instant.class, new InstantDeserializer());
+ final SimpleAbstractTypeResolver jacksonResolver = new SimpleAbstractTypeResolver();
+ jacksonResolver.addMapping(Metadata.class, MetadataImpl.class);
+ jacksonModule.setAbstractTypes(jacksonResolver);
+ final ObjectMapper objectMapper = new ObjectMapper();
+ objectMapper.registerModule(jacksonModule);
+ return objectMapper;
+ }
+
+ public static SignedJWT trustMark(final JWSAlgorithm algorithm, final JWK jwk, final String iss, final String sub,
+ final String id, final Instant exp) {
+ final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder()
+ .subject(sub)
+ .issuer(iss)
+ .claim("trust_mark_type", id)
+ .issueTime(new Date())
+ .expirationTime(exp == null ? null : Date.from(exp));
+ return signedJwt(algorithm, jwk, "trust-mark+jwt", builder.build());
+ }
+
+ public static SignedJWT signedJwt(final JWSAlgorithm algorithm, final JWK jwk, final String type,
+ final JWTClaimsSet claimsSet) {
+ return signedJwt(algorithm, jwk,
+ new JWSHeader.Builder(algorithm).type(new JOSEObjectType(type)).keyID(jwk.getKeyID()).build(),
+ claimsSet);
+ }
+
+ public static SignedJWT signedJwt(final JWSAlgorithm algorithm, final JWK jwk, final JWSHeader header,
+ final JWTClaimsSet claimsSet) {
+ final SignedJWT signedJwt = new SignedJWT(header, claimsSet);
+ try {
+ if (JWSAlgorithm.Family.RSA.contains(algorithm)) {
+ signedJwt.sign(new RSASSASigner(jwk.toRSAKey()));
+ } else {
+ signedJwt.sign(new ECDSASigner(jwk.toECKey()));
+ }
+ return signedJwt;
+ } catch (final JOSEException e) {
+ Assert.fail("Could not construct signed JWT", e);
+ }
+ return null;
+ }
+
+}
diff --git a/oidfed-common-conf-impl/src/test/resources/credentials/fed-local-anchor.jwk b/oidfed-common-conf-impl/src/test/resources/credentials/fed-local-anchor.jwk
new file mode 100644
index 0000000..8601a78
--- /dev/null
+++ b/oidfed-common-conf-impl/src/test/resources/credentials/fed-local-anchor.jwk
@@ -0,0 +1,13 @@
+{
+ "alg": "RS256",
+ "kid": "locallyTrustedAnchorKey",
+ "kty": "RSA",
+ "p": "y_ul6_wF0dnPzfK_BW0q5dM2aKRFOekotB6mFmWXE4VY_EVRpPAcHQppBbOUOBMkg_NSmjRK3x8wZYhLeavqfsFNcpGMAMsL1LA_FtPFo1R2B8n-FpcA9uRimjDxlzIQKrbEYM1OGPA7jLCE57fNzta2YM4e1sjP3W7-TfOa7v0",
+ "q": "x--fmMMzN9CRakq-pi6I0ClQq4PXJnJROGzVSjMGKs-YbaFXaWPnF_rHqJunmFavTiiSwCv36kiseEAA8fTl-Ms75MXp7LGo30e0F7JJ3vXDD5MwvapswwoiWJWCgJxCOnWJdWvm_iZApK9xcaM_ngTWQuQEq1JhPq6rEB4wck0",
+ "d": "AnBEysAXveM6Lkt2U4RaRoq30upD1EwX7GBE0xCEyzXHESoZIhriDICL9DcCnH7wz0GHittysTrnzEcOWA37mMRKtMJ9ZX3P4QXhfcxGPVvJoDXuH3hR1rbXsu-b9-aYsGNeO6fZSafqhXBeXYU3q1CJFFjoe72drw3DJ3xwCRdYv3y94YBw09kAQYOA4zAtxrUF3ZhlLH9K4sfFLAEzztJX-OuY3a2UFeNC1FkpNDKovbXU0yrkP_y5jjSPrjI2lBQg4yFN5hM3YM7bvG001E796CEhqRYsnLazNDpRXAIQc_jbs_3YrdrlaWR-abMj1zr0zw44a8JZ5Tvd7imZaQ",
+ "e": "AQAB",
+ "qi": "OlB4Yfvc6MBNsFUPpFGXmuYjVDkgTtcAEG60n1TDsrh4C2ir0-ZyaQg1gV3D3EabD96lnp7IiO3uX6X6e6AJgC2AW052bc1EuRQkhyyXeEYN23u3elF1y2F4eQUbNgfc2ghZfUqx5xhZDWFXbrMC7NaOCn1UOIfBe2kuvqSL8Es",
+ "dp": "ebkddgjaYDOd8cPdgZt3cdXsLd15AenExFdVvR-6W4fDZibnZYly_VFtAl37IMsriyH0NNjnpOWzt6LxhxWzxRgM40U_SmngEXdq7nBJDAImvNcorMpHZQ08Wc7DG_pf811FKo7Y_8C7iGT9qljgk4FFK9dUR89lWzoUvueTmPE",
+ "dq": "UBHQ8pbJ_kJS2iSQ8XCVbff9zJKCKW2CxXwgdxS0FZUJ0G3a2eQeemX-a7HajpG4py5shvWU1YjBOW84ca3II7kQhXAVXKtRnAnVP-Aw4U-_DI-_51VHNVzroFpP5z2s8Eh-Aj5yRboADXQNlJryMVBylltG222kcDv3Wf8dG8k",
+ "n": "n0-NFV06ZDKLo1v8KrSJsQ8bbLEffVJw1F5jGXqrKh_4PpBt9FmyWY3gIA9aK1p1WneMaWRNlM1EObierCr0EdXCQbgpKorrPqxiwyl6cOMIH4fN_9uWGqD2HlyGcjcESrNjZz75tNr_9oegh6fWSMgrxyySpU38ALWUX1ZuNS8A4tj8XdJSbSHqftf7qOdgzuy0yaD5h7NwoBCRPOIY88vOLHkcQ4nYdkk8GLSIf5GgGb7JFiPuFHN7pK---LNnFBifag2wbEZ9nnAcAol4jc2gF7zq2mqhMSlbIVmTRj4Y9wxh3DPbmC8xZ-8nbhPmgi4vlij9JWJGEvfLuXaMGQ"
+}
\ No newline at end of file
diff --git a/oidfed-common-conf-impl/src/test/resources/credentials/fed-local-intermediate.jwk b/oidfed-common-conf-impl/src/test/resources/credentials/fed-local-intermediate.jwk
new file mode 100644
index 0000000..7ba5321
--- /dev/null
+++ b/oidfed-common-conf-impl/src/test/resources/credentials/fed-local-intermediate.jwk
@@ -0,0 +1,12 @@
+{
+ "p":"vGxtVhQO5fnZ3FRA48ECw8y0Oojketpf0kM8dtTZpao3BYR5xZDJ7tt8ebGCpRfMWmy48PJqIR2-hAAe_i6GnX9mmlIx_fkur0aoMAx-M1kNHY3Dkr_NL6ulBUBj0b-1G_36a4BoLybflhNaFxmL2mznqUgELOPbu5oDyTyCqMk",
+ "kty":"RSA",
+ "q":"xbJas9_2ntoPXuLNX-MUFj6uxmuU1atmHO6VtC6tL7wp3uuHq1QuPiBt6Ik0Emh0ShuES9mrnXrBbc8vVw9Z_HeeGrW6t26tYN10mNXJsO69_nOiN1IL1Sb_sPRsYcxPi02gZAPKxXcqg4IfKY2JGh5Z-_UukGjUlPVENK3eM-s",
+ "d":"JGCJMehmnZScKLpK2IXtRsGGIbSvb0CG5kP8L8w-CN-K6FlbeKRGqHyNY7fojVjnbi-hR5O_TmFvhXmwgJOwGfX550HRU3sMXyi7WhAXzYfoJQivElFqUosWForIETHaU5mhZGpT-emIcxgOpPUw_3zGdUzzGFhxn4IIFEELX0C5JzZqi8fAx-d3UCp7bHJKgh8qF_agkXnbU4aHdHpq9NEuATTY8U9Qos6clGfAK3BlRMnb5I-uFGOyrbjUYnbDVyQi26yUhJ6yQtttFGWam6yDAjpLFJYXpGdcbf4ibGPtl-32b7eDRuQsOqSU_kXBktT_8DlIZ1n-U7BrhrQB",
+ "e":"AQAB",
+ "kid":"locallyTrustedIntermediateKey",
+ "qi":"l1mhiM0lvZMZongGLwv9r-GOzu1_LhYxq3KBXi_BHgoM7DR3aVGyCEOthFQTNnKb2laALaxVyqrTGGl4zaPGani1D7YzvIWOk4610U1V7Tykr86H7geud7PC8stF_XzwDwx9pYu-sMH7EdmTPQQPo3OQdHECEoVHD36Gp609RUo",
+ "dp":"FV-PP3ZjAj9HMTD1c2BPefpcb09b63ud5vHth-U5Ewut1hhi38A-x7Np-Tvjf-qlKZSvndVBqKQBGmQRH_ATIQZ0kwjD1vVPEF7JcTnZjuWJEVMlXh4XnlKwE1pseDxwxM4Ye91C9CZKsnFbhMdHD-3OkWsz3guyvyMHTupoP9E",
+ "dq":"BArNh_gAVucmgU4p3NgLxRirAiuY83V5tQW70d9SczNpt8EhGQOznlmTZbnIcsfn-MvvPI3K0IF-Cvy9fw41TNA1T15_3thIez6L78QPR6rZ_6XlnzPyQf12JOwaezVuOu7vZJwfPUJegCnc8UCPmRUdyeUNeq5qClGpf1o-vNk",
+ "n":"kYK2ScZiF-zm9VnOS0MXMrszjOtEj7_YmyuJzyG3VHZPH5EPR_79MwzXgRUxFQXTHFw93NQbiIL3m6TX6RDCtphPdO7iVTF_aNRqCy3Hdn7Ws8NQbAHyj9WihoN6b-euFc_T-C7hkPYJcoIG9A9X0rcXyE2hlS4n386eWSR-gTHjvoZQ_M63Eoo4FZVx_8213Qmx0Pwypi-iZ1mW745TI1FvuYzZfZk0xGdfxsgJzQLnXPm_hcQJKMcOFPxdqw0xLqKdByp2LKjrqlOIoZKgTOeWvFr_TV21vKC5ijp1Nh4POwkI-urAl6fmELx_Mn_z7ZvwUhefzznnRgL6xt77gw"
+}
diff --git a/oidfed-common-conf-impl/src/test/resources/credentials/fed-signing-es256.jwk b/oidfed-common-conf-impl/src/test/resources/credentials/fed-signing-es256.jwk
new file mode 100644
index 0000000..a70c96c
--- /dev/null
+++ b/oidfed-common-conf-impl/src/test/resources/credentials/fed-signing-es256.jwk
@@ -0,0 +1,10 @@
+{
+ "kty": "EC",
+ "d": "CO-ctmQcB-hS042i2omOIPpaaAaKkBAU6s_v4W09oA0",
+ "use": "sig",
+ "crv": "P-256",
+ "kid": "fedtestkeyES256",
+ "x": "2uzfE1oK0cf1_c11SFc9vFdGLnJoH3e0AKTrGPAmUis",
+ "y": "14410NGKqwLM58b26ZcvGOruFixpHt_SJTw8I5wwgLQ",
+ "alg": "ES256"
+}
diff --git a/oidfed-common-conf-impl/src/test/resources/credentials/fed-signing-es384.jwk b/oidfed-common-conf-impl/src/test/resources/credentials/fed-signing-es384.jwk
new file mode 100644
index 0000000..c42f913
--- /dev/null
+++ b/oidfed-common-conf-impl/src/test/resources/credentials/fed-signing-es384.jwk
@@ -0,0 +1,9 @@
+{
+ "kty": "EC",
+ "d": "e65hCxxbNq5gubmkgZD73A1cDf_GfGzkl4KZtbRg0GxAktztyDg4pI4bcxXaUNOb",
+ "use": "sig",
+ "crv": "P-384",
+ "kid": "fedtestkeyES384",
+ "x": "uVsAjiFw4Hv0Kcwl2532baUKPTzDht2966ar_pJ8ZdAzquFwJPdRjCfpbkqZUi46",
+ "y": "yp3W3Cmc1QQptLC3s072Iy69l1ubx_WSFRivMYqCpK4Ec89HKvYh3mTKcfjHvk2l"
+}
diff --git a/oidfed-common-conf-impl/src/test/resources/credentials/fed-signing-es521.jwk b/oidfed-common-conf-impl/src/test/resources/credentials/fed-signing-es521.jwk
new file mode 100644
index 0000000..b42d56a
--- /dev/null
+++ b/oidfed-common-conf-impl/src/test/resources/credentials/fed-signing-es521.jwk
@@ -0,0 +1,9 @@
+{
+ "kty": "EC",
+ "d": "ADaJK1sgPtlu4xAFGmb8scq8XGujamVjP3z7Xr4xErwuurSynn8sNtZKX8SfoId9syS27VLFHe12CbeBR6nbReFv",
+ "use": "sig",
+ "crv": "P-521",
+ "kid": "fedtestkeyES512",
+ "x": "AKObj9VTXWndDB7RC9dqSEkEsCqYgOHxq9AgvlDA8XBKxPzp39XrnBD0CMFy0C1HFvoiFKh9lPXJewkkruAOLW-6",
+ "y": "AMG6cRDBekWfD8imLDkBCmm-mtI16mFbifxZ06bgI5GwdyRTIMYUaBizmOzRK038Am4h6EjF8RCFr7383iKcqGZt"
+}
\ No newline at end of file
diff --git a/oidfed-common-conf-impl/src/test/resources/credentials/fed-signing-rs.jwk b/oidfed-common-conf-impl/src/test/resources/credentials/fed-signing-rs.jwk
new file mode 100644
index 0000000..a6297ee
--- /dev/null
+++ b/oidfed-common-conf-impl/src/test/resources/credentials/fed-signing-rs.jwk
@@ -0,0 +1,8 @@
+{
+ "kty": "RSA",
+ "d": "gv7aqFcXV86jDcCn6-JCqEEIRcv1Rh1AEv4dKziFzQal1nROliDdtkJjELpOYlFY9CgI-xAXt8ivwJ4q1eA_G9WTId7qLxPdcQW4QjfRl8VVEPUhka6Gc8y95WUO4VONEwzZnZ4V7KobE0QGADXvXUw3MtIZdGgvRCS-6avQXITjhTnlkUONxeqpy2BE6l0cI8GSM1vlLy66vjsQ06aAizMB-g3yMMpbKNd73oYgrdpEjAtddH3-sLhv_TG7pMlbB_etnPGkWKdIbpvTKr2P2oZN_8Qvq7G4ETIe9nIv7i8T7GXZfTxWspYkszbrpRACM9Ic8fSctvil2j013JeSgQ",
+ "e": "AQAB",
+ "use": "sig",
+ "kid": "fedtestkeyRS",
+ "n": "pNf03ghVzMAw5sWrwDAMAZdSYNY2q7OVlxMInljMgz8XB5mf8XKH3EtP7AKrb8IAf7rGhfuH3T1N1C7F-jwIeYjXxMm2nIAZ0hXApgbccvBpf4n2H7IZflMjt4A3tt587QQSxQ069drCP4sYevxhTcLplJy6RWA0cLj-5CHyWy94zPeeA4GRd6xgHFLz0RNiSF0pF0kE4rmRgQVZ-b4_BmD9SsWnIpwhms5Ihciw36WyAGQUeZqULGsfwAMwlNLIaTCBLAoRgv370p-XsLrgz86pTkNBJqXP5GwI-ZfgiLmJuHjQ9l85KqHM87f-QdsqiV8KoRcslgXPqb6VOTJBVw"
+}
diff --git a/oidfed-common-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/credentials.xml b/oidfed-common-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/credentials.xml
new file mode 100644
index 0000000..89e39c6
--- /dev/null
+++ b/oidfed-common-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/credentials.xml
@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans"
+ xmlns:context="http://www.springframework.org/schema/context"
+ xmlns:util="http://www.springframework.org/schema/util"
+ xmlns:p="http://www.springframework.org/schema/p"
+ xmlns:c="http://www.springframework.org/schema/c"
+ 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">
+
+ <import resource="oidfed/oidfed-credentials.xml" />
+
+ <!--
+ NOTE: if you're using a legacy relying-party.xml file from a V2 configuration, this file is ignored.
+
+ This defines the signing and encryption key and certificate pairs referenced by your relying-party.xml
+ configuration. You don't normally need to touch this, unless you have advanced requirements such as
+ supporting multiple sets of keys for different relying parties, in which case you may want to define
+ all your credentials here for convenience.
+ -->
+
+ <!--
+ The list of ALL of your IdP's signing credentials. If you define additional signing credentials,
+ for example for specific relying parties or different key types, make sure to include them within this list.
+ -->
+ <util:list id="shibboleth.SigningCredentials">
+ <ref bean="shibboleth.DefaultSigningCredential" />
+ </util:list>
+
+ <!-- Your IdP's default signing key, set via property file. -->
+ <bean id="shibboleth.DefaultSigningCredential"
+ class="net.shibboleth.idp.profile.spring.factory.BasicX509CredentialFactoryBean"
+ p:privateKeyResource="%{idp.signing.key}"
+ p:certificateResource="%{idp.signing.cert}"
+ p:entityId-ref="entityID" />
+
+ <!-- Your IdP's default client TLS credential, by default the same as the default signing credential. -->
+ <alias alias="shibboleth.DefaultClientTLSCredential" name="shibboleth.DefaultSigningCredential" />
+
+ <!--
+ The list of ALL of your IdP's encryption credentials. By default this is just an alias
+ for 'shibboleth.DefaultEncryptionCredentials'. It could be re-defined as
+ a list with additional credentials if needed.
+ -->
+ <alias alias="shibboleth.EncryptionCredentials" name="shibboleth.DefaultEncryptionCredentials" />
+
+ <!-- Your IdP's default encryption (really decryption) keys, set via property file. -->
+ <util:list id="shibboleth.DefaultEncryptionCredentials">
+ <bean class="net.shibboleth.idp.profile.spring.factory.BasicX509CredentialFactoryBean"
+ p:privateKeyResource="%{idp.encryption.key}"
+ p:certificateResource="%{idp.encryption.cert}"
+ p:entityId-ref="entityID" />
+
+ <!--
+ For key rollover, uncomment and point to your original keypair, and use the one above
+ to point to your new keypair. Once metadata has propagated, comment this one out again.
+ -->
+ <!--
+ <bean class="net.shibboleth.idp.profile.spring.factory.BasicX509CredentialFactoryBean"
+ p:privateKeyResource="%{idp.encryption.key.2}"
+ p:certificateResource="%{idp.encryption.cert.2}"
+ p:entityId-ref="entityID" />
+ -->
+ </util:list>
+
+</beans>
diff --git a/oidfed-common-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/global.xml b/oidfed-common-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/global.xml
new file mode 100644
index 0000000..d6f592e
--- /dev/null
+++ b/oidfed-common-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/global.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans"
+ xmlns:context="http://www.springframework.org/schema/context"
+ xmlns:util="http://www.springframework.org/schema/util"
+ xmlns:p="http://www.springframework.org/schema/p"
+ xmlns:c="http://www.springframework.org/schema/c"
+ 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="CustomEntityConfigurationMetadataDecorator"
+ class="net.shibboleth.idp.plugin.oidc.op.profile.flow.oidfed.support.CustomEntityConfigurationMetadataDecorator" />
+
+ <bean id="MockitoMockFactory" class="org.mockito.Mockito" />
+
+ <bean id="shibboleth.oidfed.HttpClient"
+ factory-bean="MockitoMockFactory"
+ factory-method="mock">
+ <constructor-arg value="#{T(org.apache.hc.client5.http.classic.HttpClient)}" />
+ </bean>
+
+ <bean id="shibboleth.oidc.NonBrowser.HttpClient"
+ factory-bean="MockitoMockFactory"
+ factory-method="mock">
+ <constructor-arg value="#{T(org.apache.hc.client5.http.classic.HttpClient)}" />
+ </bean>
+
+ <util:list id="CustomEntityConfigurationFilters">
+ <bean class="net.shibboleth.idp.plugin.oidc.op.profile.flow.oidfed.support.CustomEntityConfigurationFilterStrategy"/>
+ </util:list>
+
+ <util:list id="CustomSubordinateStatementFilters">
+ <bean class="net.shibboleth.idp.plugin.oidc.op.profile.flow.oidfed.support.CustomSubordinateStatementFilterStrategy"/>
+ </util:list>
+
+ <util:list id="CustomTrustChainFilters">
+ <bean class="net.shibboleth.idp.plugin.oidc.op.profile.flow.oidfed.support.CustomTrustChainFilterStrategy"/>
+ </util:list>
+
+ <util:set id="testbed.MetadataIndexes">
+ <bean class="org.opensaml.saml.metadata.resolver.index.impl.SAMLArtifactMetadataIndex" />
+ </util:set>
+
+ <util:list id="testbed.MetadataResolverResources">
+ <value>%{idp.home}/conf/metadata-providers.xml</value>
+ <value>%{idp.home}/conf/metadata-filters.xml</value>
+ </util:list>
+
+</beans>
diff --git a/oidfed-common-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/idp.properties b/oidfed-common-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/idp.properties
new file mode 100644
index 0000000..07392d5
--- /dev/null
+++ b/oidfed-common-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/idp.properties
@@ -0,0 +1,210 @@
+# Auto-load all files matching conf/**/*.properties
+# Disable if you want to manually maintain a list of sources.
+#idp.searchForProperties = false
+
+# Load any additional property resources from a comma-delimited list
+idp.additionalProperties = /conf/ldap.properties, \
+ /conf/saml-nameid.properties, \
+ /conf/services.properties, \
+ /conf/admin/admin.properties, \
+ /conf/authn/authn.properties, \
+ /conf/authn/duo.properties, \
+ /conf/oidc.properties, \
+ /credentials/secrets.properties, \
+ /conf/oidfed/oidfed.properties
+
+# In most cases (and unless noted in the surrounding comments) the
+# commented settings in the distributed files are the default
+# behavior for V3. Uncomment them and change the value to change
+# functionality.
+#
+# Uncommented properties are either required or ship non-defaulted.
+
+# Set the entityID of the IdP
+idp.entityID = https://idp.example.org
+
+# Set the file path which backs the IdP's own metadata publishing endpoint at /shibboleth.
+# Set to empty value to disable and return a 404.
+#idp.entityID.metadataFile=%{idp.home}/metadata/idp-metadata.xml
+
+# Set the scope used in the attribute resolver for scoped attributes
+idp.scope = example.org
+
+# General cookie properties (maxAge only applies to persistent cookies)
+# Note the default for idp.cookie.secure, you will usually want it set.
+#idp.cookie.secure = false
+#idp.cookie.httpOnly = true
+#idp.cookie.domain =
+#idp.cookie.path =
+#idp.cookie.maxAge = 31536000
+
+# HSTS/CSP response headers
+#idp.hsts = max-age=0
+# X-Frame-Options value, set to DENY or SAMEORIGIN to block framing
+#idp.frameoptions = DENY
+# Content-Security-Policy value, set to match X-Frame-Options default
+#idp.csp = frame-ancestors 'none';
+
+# Set the location of user-supplied web flow definitions
+#idp.webflows = %{idp.home}/flows
+
+# Set the location of Velocity view templates
+#idp.views = %{idp.home}/views
+
+# Settings for internal AES encryption key
+#idp.sealer.storeType = JCEKS
+#idp.sealer.updateInterval = PT15M
+#idp.sealer.aliasBase = secret
+idp.sealer.storeResource = %{idp.home}/credentials/sealer.jks
+idp.sealer.versionResource = %{idp.home}/credentials/sealer.kver
+idp.sealer.storePassword = password
+idp.sealer.keyPassword = password
+
+# Settings for public/private signing and encryption key(s)
+# During decryption key rollover, point the ".2" properties at a second
+# keypair, uncomment in credentials.xml, then publish it in your metadata.
+idp.signing.key = %{idp.home}/credentials/idp-signing.key
+idp.signing.cert = %{idp.home}/credentials/idp-signing.crt
+idp.encryption.key = %{idp.home}/credentials/idp-encryption.key
+idp.encryption.cert = %{idp.home}/credentials/idp-encryption.crt
+#idp.encryption.key.2 = %{idp.home}/credentials/idp-encryption-old.key
+#idp.encryption.cert.2 = %{idp.home}/credentials/idp-encryption-old.crt
+
+# Sets the bean ID to use as a default security configuration set
+#idp.security.config = shibboleth.DefaultSecurityConfiguration
+
+# To downgrade to SHA-1, set to shibboleth.SigningConfiguration.SHA1
+#idp.signing.config = shibboleth.SigningConfiguration.SHA256
+
+# To upgrade to AES-GCM encryption, set to shibboleth.EncryptionConfiguration.GCM
+# This is unlikely to work for all SPs, but this is a quick way to test them.
+#idp.encryption.config = shibboleth.EncryptionConfiguration.CBC
+
+# Configures trust evaluation of keys used by services at runtime
+# Defaults to supporting both explicit key and PKIX using SAML metadata.
+#idp.trust.signatures = shibboleth.ChainingSignatureTrustEngine
+# To pick only one set to one of:
+# shibboleth.ExplicitKeySignatureTrustEngine, shibboleth.PKIXSignatureTrustEngine
+#idp.trust.certificates = shibboleth.ChainingX509TrustEngine
+# To pick only one set to one of:
+# shibboleth.ExplicitKeyX509TrustEngine, shibboleth.PKIXX509TrustEngine
+
+# If true, encryption will happen whenever a key to use can be located, but
+# failure to encrypt won't result in request failure.
+#idp.encryption.optional = false
+
+# Configuration of client- and server-side storage plugins
+#idp.storage.cleanupInterval = PT10M
+idp.storage.htmlLocalStorage = true
+
+# Set to true to expose more detailed errors in responses to SPs
+#idp.errors.detailed = false
+# Set to false to skip signing of SAML response messages that signal errors
+#idp.errors.signed = true
+# Name of bean containing a list of Java exception classes to ignore
+#idp.errors.excludedExceptions = ExceptionClassListBean
+# Name of bean containing a property set mapping exception names to views
+#idp.errors.exceptionMappings = ExceptionToViewPropertyBean
+# Set if a different default view name for events and exceptions is needed
+#idp.errors.defaultView = error
+
+# Set to false to disable the IdP session layer
+#idp.session.enabled = true
+
+# Set to "shibboleth.StorageService" for server-side storage of user sessions
+idp.session.StorageService = shibboleth.StorageService
+
+# Size of session IDs
+#idp.session.idSize = 32
+# Bind sessions to IP addresses
+#idp.session.consistentAddress = true
+# Inactivity timeout
+#idp.session.timeout = PT60M
+# Extra time to store sessions for logout
+#idp.session.slop = PT0S
+# Tolerate storage-related errors
+#idp.session.maskStorageFailure = false
+# Track information about SPs logged into
+idp.session.trackSPSessions = true
+# Support lookup by SP for SAML logout
+idp.session.secondaryServiceIndex = true
+# Length of time to track SP sessions
+#idp.session.defaultSPlifetime = PT2H
+
+# Set to "shibboleth.StorageService" or custom bean for alternate storage of consent
+#idp.consent.StorageService = shibboleth.ClientPersistentStorageService
+idp.consent.StorageService = shibboleth.StorageService
+
+# Set to "shibboleth.consent.AttributeConsentStorageKey" to use an attribute
+# to key user consent storage records (and set the attribute name)
+#idp.consent.attribute-release.userStorageKey = shibboleth.consent.PrincipalConsentStorageKey
+#idp.consent.attribute-release.userStorageKeyAttribute = uid
+#idp.consent.terms-of-use.userStorageKey = shibboleth.consent.PrincipalConsentStorageKey
+#idp.consent.terms-of-use.userStorageKeyAttribute = uid
+
+# Suffix of message property used as value of consent storage records when idp.consent.compareValues is true.
+# Defaults to text displayed to the user.
+#idp.consent.terms-of-use.consentValueMessageCodeSuffix = .text
+
+# Flags controlling how built-in attribute consent feature operates
+#idp.consent.allowDoNotRemember = true
+#idp.consent.allowGlobal = true
+#idp.consent.allowPerAttribute = false
+
+# Whether attribute values and terms of use text are compared
+#idp.consent.compareValues = false
+# Maximum number of consent records for space-limited storage (e.g. cookies)
+#idp.consent.maxStoredRecords = 10
+# Maximum number of consent records for larger/server-side storage (0 = no limit)
+#idp.consent.expandedMaxStoredRecords = 0
+
+# Time in milliseconds to expire consent storage records.
+#idp.consent.storageRecordLifetime = P1Y
+
+# Whether to lookup metadata, etc. for every SP involved in a logout
+# for use by user interface logic; adds overhead so off by default.
+#idp.logout.elaboration = false
+
+# Whether to require logout requests/responses be signed/authenticated.
+#idp.logout.authenticated = true
+
+# Bean to determine whether user should be allowed to cancel logout
+#idp.logout.promptUser=shibboleth.Conditions.FALSE
+
+# Message freshness and replay cache tuning
+#idp.policy.messageLifetime = PT3M
+#idp.policy.clockSkew = PT3M
+
+# Set to custom bean for alternate storage of replay cache
+#idp.replayCache.StorageService = shibboleth.StorageService
+#idp.replayCache.strict = true
+
+# Toggles whether to allow outbound messages via SAML artifact
+#idp.artifact.enabled = true
+# Suppresses typical signing/encryption when artifact binding used
+#idp.artifact.secureChannel = true
+# May differ to direct SAML 2 artifact lookups to specific server nodes
+#idp.artifact.endpointIndex = 2
+# Set to custom bean for alternate storage of artifact map state
+#idp.artifact.StorageService = shibboleth.StorageService
+
+# Comma-delimited languages to use if not match can be found with the
+# browser-supported languages, defaults to an empty list.
+idp.ui.fallbackLanguages=en,fr,de
+
+# Storage service used by CAS protocol
+# Defaults to shibboleth.StorageService (in-memory)
+# MUST be server-side storage (e.g. in-memory, memcached, database)
+# NOTE that idp.session.StorageService requires server-side storage
+# when CAS protocol is enabled
+#idp.cas.StorageService=shibboleth.StorageService
+
+# CAS service registry implementation class
+#idp.cas.serviceRegistryClass=net.shibboleth.idp.cas.service.PatternServiceRegistry
+
+# F-TICKS auditing - set a salt to include hashed username
+#idp.fticks.federation=MyFederation
+#idp.fticks.algorithm=SHA-256
+#idp.fticks.salt=somethingsecret
+#idp.fticks.loghost=localhost
+#idp.fticks.logport=514
\ No newline at end of file
diff --git a/oidfed-common-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/logback.xml b/oidfed-common-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/logback.xml
new file mode 100644
index 0000000..2e38cbb
--- /dev/null
+++ b/oidfed-common-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/logback.xml
@@ -0,0 +1,197 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<configuration>
+
+ <!--
+ Variables for simplifying logging configuration.
+ http://logback.qos.ch/manual/configuration.html#variableSubstitution
+ -->
+
+ <!--
+ If you want to use custom properties in this config file,
+ we load the main property file for you.
+ -->
+ <variable file="src/test/resources/conf/local-log-config.properties" />
+
+ <!-- Location and retention. -->
+
+ <variable name="idp.logfiles" value="${idp.logfiles:-${idp.home}/logs}" />
+ <variable name="idp.loghistory" value="${idp.loghistory:-180}" />
+
+ <!-- Much higher performance if you operate on DEBUG. -->
+ <!-- <variable name="idp.process.appender" value="ASYNC_PROCESS" /> -->
+
+ <!-- Logging level shortcuts. -->
+ <variable name="idp.loglevel.idp" value="${idp.loglevel.idp:-OFF}" />
+ <variable name="idp.loglevel.ldap" value="${idp.loglevel.ldap:-OFF}" />
+ <variable name="idp.loglevel.messages" value="${idp.loglevel.messages:-OFF}" />
+ <variable name="idp.loglevel.encryption" value="${idp.loglevel.encryption:-OFF}" />
+ <variable name="idp.loglevel.opensaml" value="${idp.loglevel.opensaml:-OFF}" />
+ <variable name="idp.loglevel.props" value="${idp.loglevel.props:-OFF}" />
+ <variable name="idp.loglevel.httpclient" value="${idp.loglevel.httpclient:-OFF}" />
+
+ <variable name="idp.loglevel.oidc" value="${idp.loglevel.oidc:-OFF}" />
+ <variable name="idp.loglevel.oidc-op" value="${idp.loglevel.oidc-op:-OFF}" />
+
+ <!-- Don't turn these up unless you want a *lot* of noise. -->
+ <variable name="idp.loglevel.spring" value="${idp.loglevel.spring:-OFF}" />
+ <variable name="idp.loglevel.container" value="${idp.loglevel.container:-OFF}" />
+ <variable name="idp.loglevel.xmlsec" value="${idp.loglevel.xmlsec:-OFF}" />
+
+ <!-- =========================================================== -->
+ <!-- ============== Logging Categories and Levels ============== -->
+ <!-- =========================================================== -->
+
+ <!-- Logs IdP, but not OpenSAML, messages -->
+ <logger name="net.shibboleth" level="${idp.loglevel.idp}"/>
+ <logger name="net.shibboleth.oidc" level="${idp.loglevel.oidc}"/>
+ <logger name="net.shibboleth.idp.plugin.oidc.op" level="${idp.loglevel.oidc-op}"/>
+
+ <!-- Logs OpenSAML, but not IdP, messages -->
+ <logger name="org.opensaml.saml" level="${idp.loglevel.opensaml}"/>
+
+ <!-- Logs LDAP related messages -->
+ <logger name="org.ldaptive" level="${idp.loglevel.ldap}"/>
+
+ <!-- Logs embedded HTTP client messages -->
+ <logger name="org.apache.http" level="${idp.loglevel.httpclient}"/>
+
+ <!-- Logs inbound and outbound protocols messages at DEBUG level -->
+ <logger name="PROTOCOL_MESSAGE" level="${idp.loglevel.messages}" />
+
+ <!-- Logs unencrypted SAML at DEBUG level -->
+ <logger name="org.opensaml.saml.saml2.encryption.Encrypter" level="${idp.loglevel.encryption}" />
+ <logger name="org.opensaml.saml.saml2.encryption.Decrypter" level="${idp.loglevel.encryption}" />
+
+ <!-- Logs system properties during startup at DEBUG level -->
+ <logger name="net.shibboleth.idp.log.LogbackLoggingService" level="${idp.loglevel.props}" />
+
+ <!-- Especially chatty. -->
+ <logger name="org.apache.xml.security" level="${idp.loglevel.xmlsec}" />
+ <logger name="org.springframework" level="${idp.loglevel.spring}"/>
+ <logger name="org.apache.catalina" level="${idp.loglevel.container}"/>
+ <logger name="org.eclipse.jetty" level="${idp.loglevel.container}"/>
+
+
+ <!-- =========================================================== -->
+ <!-- ============== Low Level Details or Changes =============== -->
+ <!-- =========================================================== -->
+
+ <!-- Process log. -->
+ <appender name="IDP_PROCESS" class="ch.qos.logback.core.rolling.RollingFileAppender">
+ <File>${idp.logfiles}/idp-process.log</File>
+
+ <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+ <fileNamePattern>${idp.logfiles}/idp-process-%d{yyyy-MM-dd}.log.gz</fileNamePattern>
+ <maxHistory>${idp.loghistory}</maxHistory>
+ </rollingPolicy>
+
+ <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+ <charset>UTF-8</charset>
+ <Pattern>%date{ISO8601} - %mdc{idp.remote_addr} - %level [%logger:%line] - %msg%n%ex{short}</Pattern>
+ </encoder>
+
+ <!-- Ignore Velocity status page error. -->
+ <filter class="ch.qos.logback.core.filter.EvaluatorFilter">
+ <evaluator>
+ <matcher>
+ <Name>VelocityStatusMatcher</Name>
+ <regex>ResourceManager\s*: unable to find resource 'status\.vm' in any resource loader\.</regex>
+ </matcher>
+ <expression>VelocityStatusMatcher.matches(formattedMessage)</expression>
+ </evaluator>
+ <OnMatch>DENY</OnMatch>
+ </filter>
+ </appender>
+
+ <appender name="ASYNC_PROCESS" class="ch.qos.logback.classic.AsyncAppender">
+ <appender-ref ref="IDP_PROCESS" />
+ <discardingThreshold>0</discardingThreshold>
+ </appender>
+
+ <appender name="IDP_WARN" class="ch.qos.logback.core.rolling.RollingFileAppender">
+ <!-- Suppress anything below WARN. -->
+ <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
+ <level>WARN</level>
+ </filter>
+
+ <File>${idp.logfiles}/idp-warn.log</File>
+
+ <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+ <fileNamePattern>${idp.logfiles}/idp-warn-%d{yyyy-MM-dd}.log.gz</fileNamePattern>
+ <maxHistory>${idp.loghistory}</maxHistory>
+ </rollingPolicy>
+
+ <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+ <charset>UTF-8</charset>
+ <Pattern>%date{ISO8601} - %mdc{idp.remote_addr} - %level [%logger:%line] - %msg%n%ex{full}</Pattern>
+ </encoder>
+
+ <!-- Ignore Velocity status page error. -->
+ <filter class="ch.qos.logback.core.filter.EvaluatorFilter">
+ <evaluator>
+ <matcher>
+ <Name>VelocityStatusMatcher</Name>
+ <regex>ResourceManager\s*: unable to find resource 'status\.vm' in any resource loader\.</regex>
+ </matcher>
+ <expression>VelocityStatusMatcher.matches(formattedMessage)</expression>
+ </evaluator>
+ <OnMatch>DENY</OnMatch>
+ </filter>
+ </appender>
+
+ <!-- Audit log. -->
+ <appender name="IDP_AUDIT" class="ch.qos.logback.core.rolling.RollingFileAppender">
+ <File>${idp.logfiles}/idp-audit.log</File>
+
+ <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+ <fileNamePattern>${idp.logfiles}/idp-audit-%d{yyyy-MM-dd}.log.gz</fileNamePattern>
+ <maxHistory>${idp.loghistory}</maxHistory>
+ </rollingPolicy>
+
+ <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+ <charset>UTF-8</charset>
+ <Pattern>%msg%n</Pattern>
+ </encoder>
+ </appender>
+
+ <!-- Consent audit log. -->
+ <appender name="IDP_CONSENT_AUDIT" class="ch.qos.logback.core.rolling.RollingFileAppender">
+ <File>${idp.logfiles}/idp-consent-audit.log</File>
+
+ <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+ <fileNamePattern>${idp.logfiles}/idp-consent-audit-%d{yyyy-MM-dd}.log.gz</fileNamePattern>
+ <maxHistory>${idp.loghistory}</maxHistory>
+ </rollingPolicy>
+
+ <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+ <charset>UTF-8</charset>
+ <Pattern>%msg%n</Pattern>
+ </encoder>
+ </appender>
+
+ <!-- F-TICKS syslog destination. -->
+ <appender name="IDP_FTICKS" class="ch.qos.logback.classic.net.SyslogAppender">
+ <syslogHost>${idp.fticks.loghost:-localhost}</syslogHost>
+ <port>${idp.fticks.logport:-514}</port>
+ <facility>AUTH</facility>
+ <suffixPattern>[%thread] %logger %msg</suffixPattern>
+ </appender>
+
+ <logger name="Shibboleth-Audit" level="${idp.loglevel.audit:-OFF}">
+ <appender-ref ref="${idp.audit.appender:-IDP_AUDIT}"/>
+ </logger>
+
+ <logger name="Shibboleth-FTICKS" level="${idp.loglevel.fticks:-OFF}" additivity="false">
+ <appender-ref ref="${idp.fticks.appender:-IDP_FTICKS}"/>
+ </logger>
+
+ <logger name="Shibboleth-Consent-Audit" level="${idp.loglevel.consent-audit:-OFF}">
+ <appender-ref ref="${idp.consent.appender:-IDP_CONSENT_AUDIT}"/>
+ </logger>
+
+ <root level="${idp.loglevel.root:-OFF}">
+ <appender-ref ref="${idp.process.appender:-IDP_PROCESS}"/>
+ <appender-ref ref="${idp.warn.appender:-IDP_WARN}" />
+ </root>
+
+</configuration>
diff --git a/oidfed-common-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/metadata-providers.xml b/oidfed-common-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/metadata-providers.xml
new file mode 100644
index 0000000..3b78e27
--- /dev/null
+++ b/oidfed-common-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/metadata-providers.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- This file is an EXAMPLE metadata configuration file. -->
+<MetadataProvider id="ShibbolethMetadata" xsi:type="ChainingMetadataProvider"
+ xmlns="urn:mace:shibboleth:2.0:metadata" xmlns:resource="urn:mace:shibboleth:2.0:resource"
+ xmlns:security="urn:mace:shibboleth:2.0:security"
+ xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata"
+ xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
+ xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="urn:mace:shibboleth:2.0:metadata http://shibboleth.net/schema/idp/shibboleth-metadata.xsd
+ urn:mace:shibboleth:2.0:resource http://shibboleth.net/schema/idp/shibboleth-resource.xsd
+ urn:mace:shibboleth:2.0:security http://shibboleth.net/schema/idp/shibboleth-security.xsd
+ urn:oasis:names:tc:SAML:2.0:assertion http://docs.oasis-open.org/security/saml/v2.0/saml-schema-assertion-2.0.xsd
+ urn:oasis:names:tc:SAML:2.0:metadata http://docs.oasis-open.org/security/saml/v2.0/saml-schema-metadata-2.0.xsd">
+
+ <!-- ========================================== -->
+ <!-- Metadata Configuration -->
+ <!-- ========================================== -->
+
+ <MetadataProvider id="SP1MD" xsi:type="ResourceBackedMetadataProvider"
+ maxRefreshDelay="PT5M" indexesRef="testbed.MetadataIndexes"
+ resourceRef="exampleMetadata-saml-oidc-clientsecret" />
+
+ <MetadataProvider id="SP2MD" xsi:type="ResourceBackedMetadataProvider"
+ maxRefreshDelay="PT5M" indexesRef="testbed.MetadataIndexes"
+ resourceRef="exampleMetadata-saml-oauth2-resource" />
+
+</MetadataProvider>
diff --git a/oidfed-common-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/oidfed/oidfed-credentials.xml b/oidfed-common-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/oidfed/oidfed-credentials.xml
new file mode 100644
index 0000000..8d920ed
--- /dev/null
+++ b/oidfed-common-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/oidfed/oidfed-credentials.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans"
+ xmlns:context="http://www.springframework.org/schema/context"
+ xmlns:util="http://www.springframework.org/schema/util" xmlns:p="http://www.springframework.org/schema/p"
+ xmlns:c="http://www.springframework.org/schema/c" 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">
+
+ <!-- This file contains default oidfed signing credentials. This file should be imported to credentials.xml -->
+
+ <bean id="shibboleth.oidfed.DefaultRSSigningCredential"
+ class="net.shibboleth.oidc.security.credential.BasicJWKCredentialFactoryBean"
+ p:resource="/credentials/fed-signing-rs.jwk" />
+
+ <bean id="shibboleth.oidfed.DefaultES256SigningCredential"
+ class="net.shibboleth.oidc.security.credential.BasicJWKCredentialFactoryBean"
+ p:resource="/credentials/fed-signing-es256.jwk" />
+
+ <bean id="shibboleth.oidfed.DefaultES384SigningCredential"
+ class="net.shibboleth.oidc.security.credential.BasicJWKCredentialFactoryBean"
+ p:resource="/credentials/fed-signing-es384.jwk" />
+
+ <bean id="shibboleth.oidfed.DefaultES521SigningCredential"
+ class="net.shibboleth.oidc.security.credential.BasicJWKCredentialFactoryBean"
+ p:resource="/credentials/fed-signing-es521.jwk" />
+
+ <util:list id="shibboleth.oidfed.SigningCredentials">
+ <ref bean="shibboleth.oidfed.DefaultRSSigningCredential" />
+ <ref bean="shibboleth.oidfed.DefaultES256SigningCredential" />
+ <ref bean="shibboleth.oidfed.DefaultES384SigningCredential" />
+ <ref bean="shibboleth.oidfed.DefaultES521SigningCredential" />
+ </util:list>
+
+</beans>
diff --git a/oidfed-common-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/oidfed/oidfed-entity-configuration-claims.xml b/oidfed-common-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/oidfed/oidfed-entity-configuration-claims.xml
new file mode 100644
index 0000000..9a2accb
--- /dev/null
+++ b/oidfed-common-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/oidfed/oidfed-entity-configuration-claims.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans"
+ xmlns:context="http://www.springframework.org/schema/context"
+ xmlns:util="http://www.springframework.org/schema/util" xmlns:p="http://www.springframework.org/schema/p"
+ xmlns:c="http://www.springframework.org/schema/c" 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="ExampleTrustedTrustMarkIssuer"
+ class="net.shibboleth.oidfed.profile.TrustedRemoteEntity"
+ c:entity="https://dyn-trust-mark-issuer.federation.local"/>
+
+ <util:map id="shibboleth.oidfed.EntityConfigurationClaimsLookupStrategies"
+ value-type="java.util.function.Function">
+ <entry key="trust_anchor_hints" value-ref="#{'%{idp.oidfed.entity-configuration.trustAnchoHintsLookup:DefaultTrustAnchorHintsLookupStrategy}'.trim()}"/>
+ <entry key="trust_marks">
+ <bean class="net.shibboleth.oidfed.profile.navigate.DefaultEntityConfigurationTrustMarksLookupStrategy">
+ <property name="trustMarkLookupStrategies">
+ <util:list value-type="java.util.function.Function">
+ <bean parent="shibboleth.oidfed.RemoteTrustMark"
+ p:trustMarkType="https://dyn-trust-mark-issuer.federation.local/example"
+ p:trustedEntity-ref="ExampleTrustedTrustMarkIssuer" />
+ <bean parent="shibboleth.Functions.Constant">
+ <constructor-arg name="target">
+ <util:map key-type="java.lang.String" value-type="java.lang.String">
+ <entry
+ key="trust_mark_type"
+ value="https://example.org/a-trust-mark" />
+ <entry
+ key="trust_mark"
+ value="eyJraWQiOiJtb2NrVHJ1c3RNYXJrSXNzdWVyS2V5IiwidHlwIjoidHJ1c3QtbWFyaytqd3QiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczovL3RydXN0LW1hcmstaXNzdWVyLmZlZGVyYXRpb24ubG9jYWwiLCJzdWIiOiJodHRwczovL29wLmV4YW1wbGUub3JnIiwidHJ1c3RfbWFya190eXBlIjoiaHR0cHM6Ly9leGFtcGxlLm9yZy9hLXRydXN0LW1hcmsiLCJleHAiOjQ5MTgzNjczMzYsImlhdCI6MTc2NDc2NzMzNn0.smmtxeU_vCh2XFHLCxGHtwr_ZQ9A0-T7V9Poq5tNqwuU7_QlMAUJG1CJcprqQ9hH2oNSSQPIfUk7fOB1VUEY66U_bGBQ-KNQiIj-j25IQs7JalOCT1qjzcsMkq6i [...]
+ </util:map>
+ </constructor-arg>
+ </bean>
+ </util:list>
+ </property>
+ </bean>
+ </entry>
+ </util:map>
+
+</beans>
\ No newline at end of file
diff --git a/oidfed-common-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/oidfed/oidfed-entity-configuration-metadata.json b/oidfed-common-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/oidfed/oidfed-entity-configuration-metadata.json
new file mode 100644
index 0000000..44b57aa
--- /dev/null
+++ b/oidfed-common-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/oidfed/oidfed-entity-configuration-metadata.json
@@ -0,0 +1,11 @@
+{
+ "federation_entity": {
+ "organization_name" : "Example organization",
+ "organization_uri" : "https://org.example.org",
+ "contacts" : [ "contact at example.org" ]
+ },
+ "custom_entity_type": {
+ "key0" : "static_value",
+ "key1" : "static_value"
+ }
+}
\ No newline at end of file
diff --git a/oidfed-common-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/oidfed/oidfed-trust-anchors.json b/oidfed-common-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/oidfed/oidfed-trust-anchors.json
new file mode 100644
index 0000000..ff5a6b9
--- /dev/null
+++ b/oidfed-common-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/oidfed/oidfed-trust-anchors.json
@@ -0,0 +1,24 @@
+{
+ "https://trust-anchor.federation.local": {
+ "keys": [
+ {
+ "alg": "RS256",
+ "kty":"RSA",
+ "e":"AQAB",
+ "kid":"locallyTrustedAnchorKey",
+ "n":"n0-NFV06ZDKLo1v8KrSJsQ8bbLEffVJw1F5jGXqrKh_4PpBt9FmyWY3gIA9aK1p1WneMaWRNlM1EObierCr0EdXCQbgpKorrPqxiwyl6cOMIH4fN_9uWGqD2HlyGcjcESrNjZz75tNr_9oegh6fWSMgrxyySpU38ALWUX1ZuNS8A4tj8XdJSbSHqftf7qOdgzuy0yaD5h7NwoBCRPOIY88vOLHkcQ4nYdkk8GLSIf5GgGb7JFiPuFHN7pK---LNnFBifag2wbEZ9nnAcAol4jc2gF7zq2mqhMSlbIVmTRj4Y9wxh3DPbmC8xZ-8nbhPmgi4vlij9JWJGEvfLuXaMGQ"
+ }
+ ]
+ },
+ "https://local-trusted-intermediate-authority.federation.local": {
+ "keys": [
+ {
+ "alg": "RS256",
+ "kty":"RSA",
+ "e":"AQAB",
+ "kid":"locallyTrustedIntermediateKey",
+ "n":"kYK2ScZiF-zm9VnOS0MXMrszjOtEj7_YmyuJzyG3VHZPH5EPR_79MwzXgRUxFQXTHFw93NQbiIL3m6TX6RDCtphPdO7iVTF_aNRqCy3Hdn7Ws8NQbAHyj9WihoN6b-euFc_T-C7hkPYJcoIG9A9X0rcXyE2hlS4n386eWSR-gTHjvoZQ_M63Eoo4FZVx_8213Qmx0Pwypi-iZ1mW745TI1FvuYzZfZk0xGdfxsgJzQLnXPm_hcQJKMcOFPxdqw0xLqKdByp2LKjrqlOIoZKgTOeWvFr_TV21vKC5ijp1Nh4POwkI-urAl6fmELx_Mn_z7ZvwUhefzznnRgL6xt77gw"
+ }
+ ]
+ }
+}
diff --git a/oidfed-common-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/oidfed/oidfed-trustchain-resolver.xml b/oidfed-common-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/oidfed/oidfed-trustchain-resolver.xml
new file mode 100644
index 0000000..7840bec
--- /dev/null
+++ b/oidfed-common-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/oidfed/oidfed-trustchain-resolver.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans"
+ xmlns:context="http://www.springframework.org/schema/context"
+ xmlns:util="http://www.springframework.org/schema/util" xmlns:p="http://www.springframework.org/schema/p"
+ xmlns:c="http://www.springframework.org/schema/c" 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.DefaultTrustedRemoteResolverEntitiesLookupStrategy" parent="shibboleth.Functions.Constant">
+ <constructor-arg name="target">
+ <util:list value-type="net.shibboleth.oidfed.profile.TrustedRemoteResolverEntity">
+ <bean class="net.shibboleth.oidfed.profile.TrustedRemoteResolverEntity"
+ c:entity="https://trust-anchor.federation.local"
+ c:anchors="https://notworking.local,https://neither.another.local" />
+ <bean class="net.shibboleth.oidfed.profile.TrustedRemoteResolverEntity"
+ c:entity="https://trust-anchor.federation.local"
+ c:anchors="https://trust-anchor.federation.local" />
+ </util:list>
+ </constructor-arg>
+ </bean>
+
+</beans>
\ No newline at end of file
diff --git a/oidfed-common-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/oidfed/oidfed.properties b/oidfed-common-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/oidfed/oidfed.properties
new file mode 100644
index 0000000..9eb3c72
--- /dev/null
+++ b/oidfed-common-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/oidfed/oidfed.properties
@@ -0,0 +1,13 @@
+idp.oidfed.entityID = https://op.example.org
+idp.oidfed.entity.authorityHints = https://anchor1.example.org, https://anchor2.example.org
+
+idp.oidfed.cache.entityConfiguration.invalidContainerLifetime = PT0S
+idp.oidfed.cache.entityConfiguration.minRefreshDelay = PT0S
+idp.oidfed.cache.signedKeyset.invalidContainerLifetime = PT0S
+idp.oidfed.cache.signedKeyset.minRefreshDelay = PT0S
+
+idp.oidfed.cache.default.critClaims = default_crit
+idp.oidfed.cache.entityConfiguration.customFilterStrategies = CustomEntityConfigurationFilters
+idp.oidfed.cache.subordinateStatement.customFilterStrategies = CustomSubordinateStatementFilters
+idp.oidfed.cache.subordinateStatement.critClaims = subordinate_crit
+idp.oidfed.cache.trustChain.customFilterStrategies = CustomTrustChainFilters
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
new file mode 100644
index 0000000..9ab8124
--- /dev/null
+++ b/oidfed-common-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/relying-party.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans"
+ xmlns:context="http://www.springframework.org/schema/context"
+ xmlns:util="http://www.springframework.org/schema/util"
+ xmlns:p="http://www.springframework.org/schema/p"
+ xmlns:c="http://www.springframework.org/schema/c"
+ 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.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">
+ </util:list>
+
+</beans>
diff --git a/oidfed-common-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/services.xml b/oidfed-common-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/services.xml
new file mode 100644
index 0000000..c2cf11c
--- /dev/null
+++ b/oidfed-common-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/services.xml
@@ -0,0 +1,92 @@
+<beans xmlns="http://www.springframework.org/schema/beans"
+ xmlns:context="http://www.springframework.org/schema/context"
+ xmlns:util="http://www.springframework.org/schema/util" xmlns:p="http://www.springframework.org/schema/p"
+ xmlns:c="http://www.springframework.org/schema/c" 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">
+
+ <!-- Advanced configuration of services from HTTP.
+
+ To use an HTTP resource you first need to configure the Apache HttpClient which will be used
+ to communicate with the web server. Any HttpClient can be used, but two Factory Beans allow simple
+ configuration of in-memory or file-based caching clients.
+
+ Examples are:
+
+ A resource which will be supplied from an in-memory cache for as long as the file on the webserver does not change.
+ If the webserver becomes unavailable the resource will be unavailable.
+
+ <bean id="inMemoryResource" class="net.shibboleth.ext.spring.resource.HTTPResource"
+ c:client-ref="shibboleth.MemoryCachingHttpClient"
+ c:url="http://example.org/path/to/file.xml" />
+
+ Two resources which will be supplied from an on disk cache (suitable for multiple or large files) for as long
+ as the file on the webserver does not change. If the webserver becomes unavailable the last used contents
+ of the file will be returned (even if that was in a previous IdP lifetime).
+
+ <bean id="fileResource" class="net.shibboleth.ext.spring.resource.FileBackedHTTPResource"
+ c:client-ref="shibboleth.FileCachingHttpClient"
+ c:url="http://example.org/path/to/file.xml"
+ c:backingFile="/var/shibboleth/caches/resourcecache/file.xml"/>
+
+ <bean id="otherFileResource" class="net.shibboleth.ext.spring.resource.FileBackedHTTPResource"
+ c:client-ref="shibboleth.FileCachingHttpClient"
+ c:url="http://another.server.example.org/path/to/different/file.xml"
+ c:backingFile="/var/shibboleth/caches/resourcecache/differentFile.xml"/>
+
+ In all cases you should review the "idp.httpclient.*" properties defined in services.properties
+ -->
+
+ <!--
+ Otherwise by default we look at resources whose names are derived from %{idp.home}. Services not configured
+ using native Spring syntax also need to load the property-placeholder file in order to pull settings from
+ property sources.
+ -->
+
+ <!-- This set of resources supports a native Spring relying-party.xml file. -->
+ <util:list id="shibboleth.RelyingPartyResolverResources">
+ <value>%{idp.home}/conf/relying-party.xml</value>
+ <value>%{idp.home}/conf/credentials.xml</value>
+ </util:list>
+
+ <util:list id="shibboleth.MetadataResolverResources">
+ <value>%{idp.home}/conf/metadata-providers.xml</value>
+ </util:list>
+
+ <!-- This set of resources uses only AttributeEncoders for compatibility. -->
+ <util:list id ="shibboleth.AttributeRegistryResources">
+ <value>%{idp.home}/conf/attribute-registry.xml</value>
+ <value>%{idp.home}/conf/attribute-resolver.xml</value>
+<!-- <value>%{idp.home}/conf/attributes/default-rules.xml</value> -->
+ </util:list>
+
+ <util:list id ="shibboleth.AttributeResolverResources">
+ <value>%{idp.home}/conf/attribute-resolver.xml</value>
+ </util:list>
+
+ <util:list id ="shibboleth.AttributeFilterResources">
+ <value>%{idp.home}/conf/attribute-filter.xml</value>
+ </util:list>
+
+ <util:list id ="shibboleth.NameIdentifierGenerationResources">
+ <value>%{idp.home}/conf/saml-nameid.xml</value>
+ </util:list>
+
+ <util:list id="shibboleth.AccessControlResources">
+ <value>%{idp.home}/conf/access-control.xml</value>
+ </util:list>
+
+ <!--
+ This collection of resources differs slightly in that it should not include the file extension.
+ Message sources are internationalized, and Spring will search for a compatible language extension
+ and fall back to one with only a .properties extension.
+ -->
+ <util:list id="shibboleth.MessageSourceResources">
+ <value>%{idp.home}/messages/messages</value>
+ </util:list>
+
+</beans>
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/AbstractBuildEntityStatementAction.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/AbstractBuildEntityStatementAction.java
new file mode 100644
index 0000000..d9f98fb
--- /dev/null
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/AbstractBuildEntityStatementAction.java
@@ -0,0 +1,301 @@
+/*
+ * 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.text.ParseException;
+import java.time.Instant;
+import java.util.Date;
+import java.util.Map;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.messaging.encoder.AbstractMessageEncoder;
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.OutboundMessageContextLookup;
+import org.slf4j.Logger;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.nimbusds.jwt.JWT;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.PlainJWT;
+
+import net.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.idp.profile.IdPEventIds;
+import net.shibboleth.oidfed.profile.config.navigate.EntityStatementClaimsSetManipulationStrategyLookupFunction;
+import net.shibboleth.profile.context.navigate.IssuerLookupFunction;
+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.logic.FunctionSupport;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.security.IdentifierGenerationStrategy;
+import net.shibboleth.shared.security.impl.SecureRandomIdentifierGenerationStrategy;
+
+/**
+ * Abstract action used by actions that build entity statements.
+ */
+public abstract class AbstractBuildEntityStatementAction extends AbstractProfileAction {
+
+ /** Class logger. */
+ @Nonnull private Logger log = LoggerFactory.getLogger(AbstractBuildEntityStatementAction.class);
+
+ /** Used to log protocol messages. */
+ @Nonnull protected Logger protocolMessageLog =
+ LoggerFactory.getLogger(AbstractMessageEncoder.BASE_PROTOCOL_MESSAGE_LOGGER_CATEGORY + ".OIDFED");
+
+ /** Strategy used to obtain the issuer value. */
+ @Nonnull private Function<ProfileRequestContext,String> issuerLookupStrategy;
+
+ /** Strategy used to obtain the subject value. */
+ @Nonnull private Function<ProfileRequestContext,String> subjectLookupStrategy;
+
+ /** Strategy used to locate the {@link IdentifierGenerationStrategy} to use. */
+ @Nonnull private Function<ProfileRequestContext,IdentifierGenerationStrategy> idGeneratorLookupStrategy;
+
+ /** Strategy used to locate the subcontext to hold the statement. */
+ @Nonnull private Function<ProfileRequestContext,EntityStatementContext> entityStatementContextLookupStrategy;
+
+ /** Lookup function to supply strategy bi-function for manipulating entity statement claims set. */
+ @Nonnull
+ private Function<ProfileRequestContext,BiFunction<ProfileRequestContext,Map<String,Object>,Map<String,Object>>>
+ entityStatementClaimsSetManipulationStrategyLookupStrategy;
+
+ /** The strategy used for manipulating the entity statement claims set. */
+ @Nullable private BiFunction<ProfileRequestContext,Map<String,Object>,Map<String,Object>> manipulationStrategy;
+
+ /** Object mapper used for pretty-printing JWT contents. */
+ @NonnullAfterInit private ObjectMapper objectMapper;
+
+ /** The generator to use. */
+ @NonnullBeforeExec protected IdentifierGenerationStrategy idGenerator;
+
+ /** Entity statement context. */
+ @NonnullBeforeExec protected EntityStatementContext entityStatementCtx;
+
+ /** Constructor. */
+ public AbstractBuildEntityStatementAction() {
+ issuerLookupStrategy = new IssuerLookupFunction();
+ subjectLookupStrategy = new IssuerLookupFunction();
+
+ idGeneratorLookupStrategy = FunctionSupport.constant(new SecureRandomIdentifierGenerationStrategy());
+
+ final Function<ProfileRequestContext,EntityStatementContext> escls =
+ new ChildContextLookup<>(EntityStatementContext.class, true).compose(
+ new OutboundMessageContextLookup());
+ assert escls != null;
+ entityStatementContextLookupStrategy = escls;
+
+ entityStatementClaimsSetManipulationStrategyLookupStrategy =
+ new EntityStatementClaimsSetManipulationStrategyLookupFunction();
+ }
+
+ /**
+ * Set the strategy used to locate the {@link IdentifierGenerationStrategy} to use.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setIdentifierGeneratorLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext,IdentifierGenerationStrategy> strategy) {
+ ifInitializedThrowUnmodifiabledComponentException();
+
+ idGeneratorLookupStrategy =
+ Constraint.isNotNull(strategy, "Identifier generation strategy cannot be null");
+ }
+
+ /**
+ * Set the strategy used to locate the issuer value to use.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setIssuerLookupStrategy(@Nonnull final Function<ProfileRequestContext,String> strategy) {
+ ifInitializedThrowUnmodifiabledComponentException();
+
+ issuerLookupStrategy = Constraint.isNotNull(strategy, "Issuer lookup strategy cannot be null");
+ }
+
+ /**
+ * Set the strategy used to locate the subject value to use.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setSubjectLookupStrategy(@Nonnull final Function<ProfileRequestContext,String> strategy) {
+ ifInitializedThrowUnmodifiabledComponentException();
+
+ subjectLookupStrategy = Constraint.isNotNull(strategy, "Subject lookup strategy cannot be null");
+ }
+
+ /**
+ * Set the strategy used to lookup the {@link EntityStatementContext} to use.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setEntityStatementContextLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext,EntityStatementContext> strategy) {
+ ifInitializedThrowUnmodifiabledComponentException();
+
+ entityStatementContextLookupStrategy =
+ Constraint.isNotNull(strategy, "EntityStatementContext lookup strategy cannot be null");
+ }
+
+ /**
+ * Set the lookup function to supply strategy bi-function for manipulating entity statement claims set.
+ *
+ * @param strategy What to set
+ */
+ public void setEntityStatementClaimsSetManipulationStrategyLookupStrategy(@Nonnull final
+ Function<ProfileRequestContext,BiFunction<ProfileRequestContext,Map<String,Object>,Map<String,Object>>>
+ strategy) {
+ ifInitializedThrowUnmodifiabledComponentException();
+
+ entityStatementClaimsSetManipulationStrategyLookupStrategy =
+ Constraint.isNotNull(strategy, "Manipulation strategy lookup strategy cannot be null");
+ }
+
+ /**
+ * Set the object mapper used for pretty-printing JWT contents.
+ *
+ * @param mapper What to set.
+ */
+ public void setObjectMapper(@Nonnull final ObjectMapper mapper) {
+ checkSetterPreconditions();
+ objectMapper = Constraint.isNotNull(mapper, "Object mapper cannot be null");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+
+ if (objectMapper == null) {
+ throw new ComponentInitializationException("Object mapper cannot be null");
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+ if (!super.doPreExecute(profileRequestContext)) {
+ return false;
+ }
+
+ idGenerator = idGeneratorLookupStrategy.apply(profileRequestContext);
+ if (idGenerator == null) {
+ log.error("{} No identifier generation strategy", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return false;
+ }
+
+ entityStatementCtx = entityStatementContextLookupStrategy.apply(profileRequestContext);
+ if (entityStatementCtx == null) {
+ log.error("{} Unable to fetch EntityStatementContext", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return false;
+ }
+
+ manipulationStrategy =
+ entityStatementClaimsSetManipulationStrategyLookupStrategy.apply(profileRequestContext);
+
+ return true;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+ final String issuer = issuerLookupStrategy.apply(profileRequestContext);
+ final String subject = subjectLookupStrategy.apply(profileRequestContext);
+
+ final Instant now = Instant.now();
+
+ final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder()
+ .issuer(issuer)
+ .subject(subject)
+ .issueTime(Date.from(now));
+ assert builder != null;
+ if (!populateClaimsSetBuilder(builder, profileRequestContext)) {
+ return;
+ }
+ final JWTClaimsSet claimsSet = builder.build();
+
+ assert claimsSet != null;
+ if (manipulationStrategy != null) {
+ log.debug("{} Manipulation strategy has been set, applying it to the claims set {}", getLogPrefix(),
+ claimsSet.toJSONObject());
+ assert manipulationStrategy != null;
+ final Map<String, Object> result = manipulationStrategy.apply(profileRequestContext,
+ claimsSet.toJSONObject());
+ if (result == null) {
+ log.debug("{} Manipulation strategy returned null, leaving statement claims set untouched.",
+ getLogPrefix());
+ } else {
+ log.debug("{} Applying the manipulated claims into the entity statement claims set", getLogPrefix());
+ try {
+ final JWTClaimsSet parsedSet = JWTClaimsSet.parse(result);
+ assert parsedSet != null;
+ logAndConstructEntityStatement(parsedSet);
+ return;
+ } catch (final ParseException e) {
+ log.error("{} The resulted claims set could not be transformed into ", getLogPrefix(), e);
+ ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_PROFILE_CONFIG);
+ return;
+ }
+ }
+ } else {
+ log.debug("{} No manipulation strategy configured", getLogPrefix());
+ }
+ logAndConstructEntityStatement(claimsSet);
+ }
+
+ /**
+ * Populates the claims set builder with claims specific to the action extending this abstract action. If any
+ * problem occures during population, the profile request context should be populated with an appropriate
+ * event.
+ *
+ * @param builder the claims set builder
+ * @param profileRequestContext profile request context
+ * @return true if population was successful, false otherwise
+ */
+ protected abstract boolean populateClaimsSetBuilder(@Nonnull final JWTClaimsSet.Builder builder,
+ @Nonnull final ProfileRequestContext profileRequestContext);
+
+ /**
+ * Logs the entity statement contents via protocol message logger and constructs a plain (i.e. non-signed) JWT out
+ * of it and includes it to the {@link EntityStatementContext#setJWT(JWT)}.
+ *
+ * @param claimsSet the claims set
+ */
+ protected void logAndConstructEntityStatement(@Nonnull final JWTClaimsSet claimsSet) {
+ log.trace("{} Building JWT from the claims set {}", getLogPrefix(), claimsSet);
+ assert objectMapper != null;
+ try {
+ final Object jsonObject = objectMapper.readValue(claimsSet.toString(), Object.class);
+ final String contents = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(jsonObject);
+ protocolMessageLog.trace("Entity statement payload contents:\n{}", contents);
+ } catch (final JsonProcessingException e) {
+ log.error("{} Could not construct protocol log message", getLogPrefix(), e);
+ }
+ assert entityStatementCtx != null;
+ final JWT jwt = new PlainJWT(claimsSet);
+ entityStatementCtx.setJWT(jwt);
+ }
+}
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/BuildEntityConfiguration.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/BuildEntityConfiguration.java
new file mode 100644
index 0000000..aace6ed
--- /dev/null
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/BuildEntityConfiguration.java
@@ -0,0 +1,215 @@
+/*
+ * 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.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.security.credential.Credential;
+import org.slf4j.Logger;
+
+import com.nimbusds.jose.jwk.JWK;
+import com.nimbusds.jose.jwk.JWKSet;
+import com.nimbusds.jwt.JWTClaimsSet;
+
+import net.shibboleth.idp.profile.IdPEventIds;
+import net.shibboleth.oidc.profile.config.navigate.JWTSignatureSigningConfigurationLookupFunction;
+import net.shibboleth.oidc.security.CredentialConversionUtil;
+import net.shibboleth.oidc.security.jose.SignatureSigningConfiguration;
+import net.shibboleth.oidfed.metadata.payload.claim.Metadata;
+import net.shibboleth.oidfed.profile.config.navigate.AuthorityHintsLookupFunction;
+import net.shibboleth.oidfed.profile.config.navigate.EntityStatementLifetimeLookupFunction;
+import net.shibboleth.oidfed.profile.config.navigate.OptionalClaimsLookupStrategiesLookupFunction;
+import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+import org.opensaml.profile.action.ActionSupport;
+
+/**
+ * Action that creates an Entity Statement, and stores it to an {@link EntityStatementContext}.
+ *
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#INVALID_PROFILE_CTX}
+ * @event {@link EventIds#IO_ERROR}
+ * @event {@link IdPEventIds#INVALID_PROFILE_CONFIG}
+ */
+public class BuildEntityConfiguration extends AbstractBuildEntityStatementAction {
+
+ /** Class logger. */
+ @Nonnull private Logger log = LoggerFactory.getLogger(BuildEntityConfiguration.class);
+
+ /** Strategy used to locate the {@link SignatureSigningConfiguration}s to fetch JWK set from. */
+ @Nonnull private
+ Function<ProfileRequestContext,List<SignatureSigningConfiguration>> signingConfigurationsLookupStrategy;
+
+ /** Strategy used to obtain the entity statement lifetime. */
+ @Nonnull private Function<ProfileRequestContext,Duration> entityConfigurationLifetimeLookupStrategy;
+
+ /** Strategy used to locate authority hints. */
+ @Nonnull private Function<ProfileRequestContext,List<String>> authorityHintsLookupStrategy;
+
+ /** Strategy used to locate strategies for optional claims. */
+ @Nonnull private Function<ProfileRequestContext,Map<String, Function<ProfileRequestContext,Object>>>
+ optionalClaimsLookupStrategiesLookupStrategy;
+
+ /** Metadata to publish. */
+ @NonnullBeforeExec private Metadata metadata;
+
+ /** Constructor. */
+ public BuildEntityConfiguration() {
+ signingConfigurationsLookupStrategy = new JWTSignatureSigningConfigurationLookupFunction();
+ entityConfigurationLifetimeLookupStrategy = new EntityStatementLifetimeLookupFunction();
+ authorityHintsLookupStrategy = new AuthorityHintsLookupFunction();
+ optionalClaimsLookupStrategiesLookupStrategy = new OptionalClaimsLookupStrategiesLookupFunction();
+ }
+
+ /**
+ * Set the strategy used to locate the {@link SignatureSigningConfiguration}s to fetch JWK set from.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setSigningConfigurationsLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext,List<SignatureSigningConfiguration>> strategy) {
+ checkSetterPreconditions();
+
+ signingConfigurationsLookupStrategy =
+ Constraint.isNotNull(strategy, "Signing configuration lookup strategy cannot be null");
+ }
+
+ /**
+ * Set the strategy used to obtain the entity configuration lifetime.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setEntityConfigurationLifetimeLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext,Duration> strategy) {
+ checkSetterPreconditions();
+
+ entityConfigurationLifetimeLookupStrategy =
+ Constraint.isNotNull(strategy, "Entity configuration lifetime lookup strategy cannot be null");
+ }
+
+ /**
+ * Set the strategy used to locate authority hints.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setAuthorityHintsLookupStrategy(@Nonnull final Function<ProfileRequestContext,List<String>> strategy) {
+ checkSetterPreconditions();
+
+ authorityHintsLookupStrategy = Constraint.isNotNull(strategy, "Authority hints lookup strategy cannot be null");
+ }
+
+ /**
+ * Set the strategy used to locate strategies for optional claims.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setOptionalClaimsLookupStrategiesLookupStrategy(@Nonnull final
+ Function<ProfileRequestContext, Map<String,Function<ProfileRequestContext,Object>>> strategy) {
+ checkSetterPreconditions();
+
+ optionalClaimsLookupStrategiesLookupStrategy =
+ Constraint.isNotNull(strategy, "Optional claims lookup strategies lookup strategy cannot be null");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+ if (!super.doPreExecute(profileRequestContext)) {
+ return false;
+ }
+
+ metadata = entityStatementCtx.getMetadata();
+ if (metadata == null) {
+ log.error("{} Could not resolve provider metadata", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return false;
+ }
+
+ return true;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected boolean populateClaimsSetBuilder(@Nonnull final JWTClaimsSet.Builder builder,
+ @Nonnull final ProfileRequestContext profileRequestContext) {
+ final List<SignatureSigningConfiguration> signingConfigurations =
+ signingConfigurationsLookupStrategy.apply(profileRequestContext);
+ if (signingConfigurations == null || signingConfigurations.isEmpty()) {
+ log.error("{} Could not fetch any signature signing configurations", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_PROFILE_CONFIG);
+ return false;
+ }
+
+ final List<JWK> jwks = new ArrayList<>();
+ for (final SignatureSigningConfiguration signingConfiguration : signingConfigurations) {
+ for (final Credential credential : signingConfiguration.getSigningCredentials()) {
+ final JWK jwk = CredentialConversionUtil.credentialToKey(credential);
+ if (jwk != null) {
+ jwks.add(jwk);
+ log.debug("{} Included {} to the keyset", getLogPrefix(), jwk.toJSONString());
+ }
+ }
+ }
+
+ final Duration lifetime = entityConfigurationLifetimeLookupStrategy.apply(profileRequestContext);
+ if (lifetime == null) {
+ log.error("{} No lifetime supplied for entity statement", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_PROFILE_CONFIG);
+ return false;
+ }
+ final Instant now = Instant.now();
+ final Instant dateExp = now.plus(lifetime);
+ assert dateExp != null;
+
+ builder.expirationTime(Date.from(dateExp));
+ builder.claim("jwks", new JWKSet(jwks).toJSONObject(true));
+ builder.claim("metadata", CollectionSupport.copyToMap(metadata.getAllClaims()));
+ final List<String> authorityHints = authorityHintsLookupStrategy.apply(profileRequestContext);
+ if (authorityHints != null && !authorityHints.isEmpty()) {
+ builder.claim("authority_hints", authorityHints);
+ }
+ final Map<String, Function<ProfileRequestContext, Object>> optionalClaimsLookupStrategies =
+ optionalClaimsLookupStrategiesLookupStrategy.apply(profileRequestContext);
+ if (optionalClaimsLookupStrategies != null) {
+ for (final String claim : optionalClaimsLookupStrategies.keySet()) {
+ log.trace("{} Looking up the value for clain {}", getLogPrefix(), claim);
+ final Function<ProfileRequestContext,Object> lookup = optionalClaimsLookupStrategies.get(claim);
+ final Object value = lookup.apply(profileRequestContext);
+ if (value != null) {
+ log.debug("{} Resolved value {} for clain {}", getLogPrefix(), value, claim);
+ builder.claim(claim, value);
+ } else {
+ log.debug("{} No value resolved for clain {}", getLogPrefix(), claim);
+ }
+ }
+ }
+
+ return true;
+ }
+
+}
\ No newline at end of file
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/DefaultTrustMarkFromMetadataCacheFetchingFunction.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/DefaultTrustMarkFromMetadataCacheFetchingFunction.java
new file mode 100644
index 0000000..79e225e
--- /dev/null
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/DefaultTrustMarkFromMetadataCacheFetchingFunction.java
@@ -0,0 +1,205 @@
+/*
+ * 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.URI;
+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.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+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.TrustMark;
+import net.shibboleth.oidfed.metadata.cache.FederationEndpointEntityStatementCriterion;
+import net.shibboleth.oidfed.metadata.cache.SubjectEntityIDCriterion;
+import net.shibboleth.oidfed.metadata.cache.configuration.EntityConfigurationContainer;
+import net.shibboleth.oidfed.metadata.cache.trustmark.TrustMarkCacheIdentifier;
+import net.shibboleth.oidfed.metadata.cache.trustmark.TrustMarkContainer;
+import net.shibboleth.oidfed.metadata.cache.trustmark.TrustMarkIdentifierCriterion;
+import net.shibboleth.oidfed.profile.TrustedRemoteEntity;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.resolver.CriteriaSet;
+
+/**
+ * Function to fetch a trust mark from the configured cache of trust marks. The configurable cache is used for
+ * fetching the trust_mark_endpoint of the trusted entity.
+ */
+public class DefaultTrustMarkFromMetadataCacheFetchingFunction extends AbstractIdentifiableInitializableComponent
+ implements Function<ProfileRequestContext, Map<String, String>> {
+
+ /** Class logger. */
+ @Nonnull private Logger log = LoggerFactory.getLogger(DefaultTrustMarkFromMetadataCacheFetchingFunction.class);
+
+ /** Cache used to fetch the issuer entity configuration from. */
+ @NonnullAfterInit private MetadataCache<EntityConfigurationContainer> entityConfigurationCache;
+
+ /** Cache containing responses from Trust Mark APIs. */
+ @NonnullAfterInit private MetadataCache<TrustMarkContainer> trustMarkCache;
+
+ /** Trusted trust mark API entity. */
+ @NonnullAfterInit private TrustedRemoteEntity trustedEntity;
+
+ /** Trust mark type. */
+ @NonnullAfterInit private String trustMarkType;
+
+ /** Subject of the trust mark. */
+ @NonnullAfterInit private String subject;
+
+ /**
+ * 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");
+ }
+
+ /**
+ * Set the cache containing responses from Trust Mark APIs.
+ *
+ * @param cache cache containing responses from Trust Mark APIs
+ */
+ public void setTrustMarkCache(@Nonnull final MetadataCache<TrustMarkContainer> cache) {
+ checkSetterPreconditions();
+ trustMarkCache = Constraint.isNotNull(cache, "Trust Mark cache cannot be null");
+ }
+
+ /**
+ * Set the trusted trust mark API entity.
+ *
+ * @param entity trusted trust mark API entity
+ */
+ public void setTrustedEntity(@Nonnull final TrustedRemoteEntity entity) {
+ checkSetterPreconditions();
+ trustedEntity = Constraint.isNotNull(entity, "Trusted entity cannot be null");
+ }
+
+ /**
+ * Set the trust mark type.
+ *
+ * @param type trust mark type
+ */
+ public void setTrustMarkType(@Nonnull @NotEmpty final String type) {
+ checkSetterPreconditions();
+ trustMarkType = Constraint.isNotEmpty(type, "Trust mark type cannot be empty");
+ }
+
+ /**
+ * Set the subject.
+ *
+ * @param sub subject
+ */
+ public void setSubject(@Nonnull @NotEmpty final String sub) {
+ checkSetterPreconditions();
+ subject = Constraint.isNotEmpty(sub, "Subject cannot be empty");
+ }
+
+ /** {@inheritDoc} */
+ protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+ if (entityConfigurationCache == null) {
+ throw new ComponentInitializationException("Entity configuration cache cannot be null");
+ }
+ if (trustMarkCache == null) {
+ throw new ComponentInitializationException("Trust Mark cache cannot be null");
+ }
+ if (trustedEntity == null) {
+ throw new ComponentInitializationException("Trusted entity cannot be null");
+ }
+ if (trustMarkType == null) {
+ throw new ComponentInitializationException("Trust mark type cannot be null");
+ }
+ if (subject == null) {
+ throw new ComponentInitializationException("Subject cannot be null");
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override @Nullable
+ public Map<String, String> apply(@Nullable final ProfileRequestContext profileRequestContext) {
+ checkComponentActive();
+ final String entityId = trustedEntity.getEntityId();
+ assert entityConfigurationCache != null;
+ final CriteriaSet criteria = new CriteriaSet(new SubjectEntityIDCriterion(entityId));
+ final EntityConfiguration configuration;
+ try {
+ final List<EntityConfigurationContainer> result = entityConfigurationCache.get(criteria);
+ if (!result.isEmpty()) {
+ configuration = Optional.ofNullable(result.get(0).getStatement()).orElse(null);
+ } else {
+ return null;
+ }
+ } catch (final MetadataCacheException e) {
+ log.debug("Error while fetching entity configuration for {}", entityId, e);
+ return null;
+ }
+
+ if (configuration == null) {
+ log.warn("Could not fetch entity configuration for {}", entityId);
+ return null;
+ }
+
+ final URI uri = Optional.ofNullable(configuration.getParsedPayload().getMetadata())
+ .map(metadata -> metadata.getFederationEntityMetadata())
+ .map(map -> map.get("trust_mark_endpoint"))
+ .filter(String.class::isInstance)
+ .map(String.class::cast)
+ .map(URI::create)
+ .orElse(null);
+
+ if (uri == null) {
+ log.warn("Could not fetch trust mark endpoint for {}", entityId);
+ return null;
+ }
+ final String uriValue = uri.toString();
+ assert uriValue != null; assert trustMarkType != null; assert subject != null;
+ final TrustMarkCacheIdentifier trustMarkIdentifier =
+ new TrustMarkCacheIdentifier(uriValue, trustMarkType, subject);
+ final CriteriaSet criteriaSet = new CriteriaSet(new TrustMarkIdentifierCriterion(trustMarkIdentifier),
+ new FederationEndpointEntityStatementCriterion(configuration));
+ final List<TrustMarkContainer> cacheResult;
+ try {
+ cacheResult = trustMarkCache.get(criteriaSet);
+ } catch (final MetadataCacheException e) {
+ log.warn("Could not resolve trust mark {} from {}", trustMarkType, trustedEntity, e);
+ return null;
+ }
+ if (cacheResult.isEmpty()) {
+ log.debug("No data resolved for {} from {}", trustMarkType, trustedEntity);
+ return null;
+ }
+ final TrustMark trustMark = cacheResult.get(0).getStatement();
+ if (trustMark != null) {
+ return Map.of("trust_mark_type", trustMarkType, "trust_mark", trustMark.getJwt().serialize());
+ } else {
+ log.debug("The cache container for {} did not contain trust mark", trustedEntity);
+ }
+ return null;
+ }
+}
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/EntityStatementContext.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/EntityStatementContext.java
new file mode 100644
index 0000000..c4e4091
--- /dev/null
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/EntityStatementContext.java
@@ -0,0 +1,133 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.oidfed.profile.impl;
+
+import java.time.Duration;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.BaseContext;
+
+import com.nimbusds.jose.jwk.JWKSet;
+import com.nimbusds.jwt.JWT;
+
+import net.shibboleth.oidfed.metadata.payload.claim.Metadata;
+
+/**
+ * Subcontext carrying information used to produce entity statements.
+ */
+public final class EntityStatementContext extends BaseContext {
+
+ /** Metadata. */
+ @Nullable private Metadata metadata;
+
+ /** Lifetime of the statement. */
+ @Nullable private Duration lifetime;
+
+ /** The entity statement. */
+ @Nullable private JWT jwt;
+
+ /** The keys claim for the entity statement. */
+ @Nullable private JWKSet keys;
+
+ /**
+ * Get the metadata.
+ *
+ * @return the metadata
+ */
+ @Nullable public Metadata getMetadata() {
+ return metadata;
+ }
+
+ /**
+ * Set the metadata.
+ *
+ * @param data the metadata
+ *
+ * @return this context
+ */
+ @Nonnull public EntityStatementContext setMetadata(@Nullable final Metadata data) {
+ metadata = data;
+ return this;
+ }
+
+ /**
+ * Get the entity statement JWT.
+ *
+ * <p>May be in various states prior to signing.</p>
+ *
+ * @return the JWT
+ */
+ @Nullable public JWT getJWT() {
+ return jwt;
+ }
+
+ /**
+ * Set the entity statement JWT.
+ *
+ * <p>May be in various states prior to signing.</p>
+ *
+ * @param token the JWT
+ *
+ * @return this context
+ */
+ @Nonnull public EntityStatementContext setJWT(@Nullable final JWT token) {
+ jwt = token;
+ return this;
+ }
+
+ /**
+ * Get the statement lifetime.
+ *
+ * @return lifetime
+ */
+ @Nullable public Duration getLifetime() {
+ return lifetime;
+ }
+
+ /**
+ * Set the statement lifetime.
+ *
+ * @param lt lifetime
+ *
+ * @return this context
+ */
+ @Nonnull public EntityStatementContext setLifetime(@Nullable final Duration lt) {
+ lifetime = lt;
+ return this;
+ }
+
+ /**
+ * Get the keys claim for the entity statement.
+ *
+ * @return keys
+ */
+ @Nullable public JWKSet getKeys() {
+ return keys;
+ }
+
+ /**
+ * Set the keys claim for the entity statement.
+ *
+ * @param jwks keys
+ *
+ * @return this context
+ */
+ @Nonnull public EntityStatementContext setKeys(@Nullable final JWKSet jwks) {
+ keys = jwks;
+ return this;
+ }
+}
\ No newline at end of file
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/EntityStatementUpdateStrategy.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/EntityStatementUpdateStrategy.java
new file mode 100644
index 0000000..514bf37
--- /dev/null
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/EntityStatementUpdateStrategy.java
@@ -0,0 +1,57 @@
+/*
+ * 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.function.BiConsumer;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+
+import com.nimbusds.jwt.JWT;
+
+/**
+ * Add the {@link JWT} back to the {@link EntityStatementContext}.
+ */
+public class EntityStatementUpdateStrategy implements BiConsumer<JWT, MessageContext> {
+
+ /** Strategy used to locate the subcontext with the statement. */
+ @Nonnull private Function<MessageContext,EntityStatementContext> entityStatementContextLookupStrategy;
+
+ /**
+ * Constructor.
+ */
+ public EntityStatementUpdateStrategy() {
+ final Function<MessageContext,EntityStatementContext> escls =
+ new ChildContextLookup<>(EntityStatementContext.class);
+ assert escls != null;
+ entityStatementContextLookupStrategy = escls;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void accept(final JWT jwt, final MessageContext messageContext) {
+ if (messageContext == null) {
+ return;
+ }
+ final EntityStatementContext entityStatementCtx = entityStatementContextLookupStrategy.apply(messageContext);
+ if (entityStatementCtx == null) {
+ return;
+ }
+ entityStatementCtx.setJWT(jwt);
+ }
+}
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/FormOutboundFederationConfigurationResponse.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/FormOutboundFederationConfigurationResponse.java
new file mode 100644
index 0000000..9d9bc6a
--- /dev/null
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/FormOutboundFederationConfigurationResponse.java
@@ -0,0 +1,214 @@
+/*
+ * 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 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.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.EntityConfigurationResponse;
+import net.shibboleth.oidfed.metadata.cache.ResponseContainerExpirationCriterion;
+import net.shibboleth.oidfed.metadata.cache.local.NimbusResponseContainer;
+import net.shibboleth.oidfed.metadata.cache.local.NimbusResponseCriterion;
+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 configuration request. The response contains an
+ * {@link SignedJWT} obtained from {@link EntityStatementContext#getJWT()}.
+ */
+public class FormOutboundFederationConfigurationResponse extends AbstractProfileAction {
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(FormOutboundFederationConfigurationResponse.class);
+
+ /** Metadata cache for cached response containers. */
+ @NonnullAfterInit private MetadataCache<NimbusResponseContainer> responseCache;
+
+ /** Strategy used to locate the cached message 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. */
+ @Nullable private SignedJWT jwt;
+
+ /** The resolve entity context to operate on. */
+ @NonnullBeforeExec private RelyingPartyCachedMessageContext cachedMessageContext;
+
+ /**
+ * Constructor.
+ */
+ public FormOutboundFederationConfigurationResponse() {
+ final Function<ProfileRequestContext,EntityStatementContext> escls =
+ new ChildContextLookup<>(EntityStatementContext.class).compose(
+ new OutboundMessageContextLookup());
+ assert escls != null;
+ entityStatementContextLookupStrategy = escls;
+ cachedMessageContextLookupStrategy = new ChildContextLookup<>(RelyingPartyCachedMessageContext.class);
+ 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<NimbusResponseContainer> 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 EntityConfigurationResponse response = new EntityConfigurationResponse(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);
+ final CriteriaSet criteria = new CriteriaSet(responseCriterion, expirationCriterion);
+ try {
+ final List<NimbusResponseContainer> result = responseCache.get(criteria);
+ if (result.size() != 1) {
+ log.error("{} Unexpected result (size={}) when storing response record into the metadata cache",
+ getLogPrefix(), result.size());
+ } else {
+ log.debug("{} Response stored into the cache", getLogPrefix());
+ }
+ } catch (final MetadataCacheException e) {
+ log.error("{} Could not store the response record into tht metadata cache", getLogPrefix(), e);
+ }
+
+ profileRequestContext.ensureOutboundMessageContext().setMessage(response);
+ }
+}
\ No newline at end of file
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/InitializeEntityStatementContext.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/InitializeEntityStatementContext.java
new file mode 100644
index 0000000..08129cf
--- /dev/null
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/InitializeEntityStatementContext.java
@@ -0,0 +1,165 @@
+/*
+ * 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.HashMap;
+import java.util.Map;
+import java.util.Optional;
+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.OutboundMessageContextLookup;
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.oidfed.metadata.payload.claim.impl.MetadataImpl;
+import net.shibboleth.oidfed.profile.EntityConfigurationMetadataDecorator;
+import net.shibboleth.oidfed.profile.EntityConfigurationMetadataDecoratorManager;
+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.primitive.NonnullSupplier;
+import net.shibboleth.shared.resolver.CriteriaSet;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.action.ActionSupport;
+
+/**
+ * Action that creates and initializes the {@link EntityStatementContext}.
+ *
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#INVALID_PROFILE_CTX}
+ * @event {@link EventIds#IO_ERROR}
+ */
+public class InitializeEntityStatementContext extends AbstractProfileAction {
+
+ /** Class logger. */
+ @Nonnull private Logger log = LoggerFactory.getLogger(InitializeEntityStatementContext.class);
+
+ /** Strategy used to locate skeleton for the metadata claim. */
+ @NonnullAfterInit
+ private Function<CriteriaSet,Map<String,Map<String,Object>>> metadataSkeletonLookupStrategy;
+
+ /** Strategy used to create the subcontext to hold the statement. */
+ @Nonnull private Function<ProfileRequestContext,EntityStatementContext> entityStatementContextCreationStrategy;
+
+ /** Manager for entity configuration metadata decorators. */
+ @NonnullAfterInit private EntityConfigurationMetadataDecoratorManager metadataDecoratorManager;
+
+ /** Entity statement context. */
+ @NonnullBeforeExec private EntityStatementContext entityStatementCtx;
+
+ /** Metadata skeleton. */
+ @NonnullBeforeExec private Map<String,Map<String,Object>> metadataSkeleton;
+
+ /** Constructor. */
+ public InitializeEntityStatementContext() {
+ final Function<ProfileRequestContext,EntityStatementContext> esccs =
+ new ChildContextLookup<>(EntityStatementContext.class, true).compose(
+ new OutboundMessageContextLookup());
+ assert esccs != null;
+ entityStatementContextCreationStrategy = esccs;
+ }
+
+ /**
+ * Set the strategy used to locate skeleton for the metadata claim.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setMetadataSkeletonLookupStrategy(
+ @Nonnull final Function<CriteriaSet,Map<String,Map<String,Object>>> strategy) {
+ checkSetterPreconditions();
+
+ metadataSkeletonLookupStrategy =
+ Constraint.isNotNull(strategy, "MetadataSkeletonLookupStrategy cannot be null");
+ }
+
+ /**
+ * Set the strategy used to create the {@link EntityStatementContext} to use.
+ *
+ * @param strategy creation strategy
+ */
+ public void setEntityStatementContextCreationStrategy(
+ @Nonnull final Function<ProfileRequestContext,EntityStatementContext> strategy) {
+ checkSetterPreconditions();
+
+ entityStatementContextCreationStrategy =
+ Constraint.isNotNull(strategy, "EntityStatementContextCreationStrategy cannot be null");
+ }
+
+ /**
+ * Set the manager for entity configuration metadata decorators.
+ *
+ * @param manager metadata decorator manager
+ */
+ public void setMetadataDecoratorManager(@Nonnull final EntityConfigurationMetadataDecoratorManager manager) {
+ checkSetterPreconditions();
+
+ metadataDecoratorManager = Constraint.isNotNull(manager, "MetadataDecoratorManager cannot be null");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+
+ if (metadataSkeletonLookupStrategy == null) {
+ throw new ComponentInitializationException("MetadataSkeletonLookupStrategy cannot be null");
+ }
+ if (metadataDecoratorManager == null) {
+ throw new ComponentInitializationException("MetadataDecoratorManager cannot be null");
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+ if (!super.doPreExecute(profileRequestContext)) {
+ return false;
+ }
+
+ entityStatementCtx = entityStatementContextCreationStrategy.apply(profileRequestContext);
+ if (entityStatementCtx == null) {
+ log.error("{} Unable to create EntityStatementContext", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return false;
+ }
+
+ metadataSkeleton = new HashMap<>(Optional.ofNullable(metadataSkeletonLookupStrategy.apply(new CriteriaSet()))
+ .orElseGet(NonnullSupplier.of(CollectionSupport.emptyMap())));
+
+ return true;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+ log.trace("{} Start decoration of metadata skeleton {}", getLogPrefix(), metadataSkeleton);
+ for (final EntityConfigurationMetadataDecorator decorator : metadataDecoratorManager.all()) {
+ log.debug("{} Decorating metadata skeleton with {}", getLogPrefix(), decorator.getId());
+ decorator.accept(metadataSkeleton, profileRequestContext);
+ }
+ log.trace("{} Metadata after all decorators have run: {}", getLogPrefix(), metadataSkeleton);
+ assert metadataSkeleton != null;
+ entityStatementCtx.setMetadata(new MetadataImpl(metadataSkeleton));
+ }
+
+}
\ No newline at end of file
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/JWTClaimsSetFromEntityStatementLookupFunction.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/JWTClaimsSetFromEntityStatementLookupFunction.java
new file mode 100644
index 0000000..bec62a8
--- /dev/null
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/JWTClaimsSetFromEntityStatementLookupFunction.java
@@ -0,0 +1,85 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.oidfed.profile.impl;
+
+import java.text.ParseException;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.slf4j.Logger;
+
+import com.nimbusds.jwt.JWT;
+import com.nimbusds.jwt.JWTClaimsSet;
+
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * Extract the {@link JWTClaimsSet} from the JWT in {@link EntityStatementContext}.
+ */
+public class JWTClaimsSetFromEntityStatementLookupFunction implements Function<MessageContext, JWTClaimsSet> {
+
+ /** Class logger. */
+ @Nonnull private Logger log = LoggerFactory.getLogger(JWTClaimsSetFromEntityStatementLookupFunction.class);
+
+ /** Strategy used to locate the subcontext with the token. */
+ @Nonnull private Function<MessageContext,EntityStatementContext> entityStatementContextLookupStrategy;
+
+ /**
+ * Constructor.
+ */
+ public JWTClaimsSetFromEntityStatementLookupFunction() {
+ // message context -> OIDC response context -> ATC
+ final Function<MessageContext,EntityStatementContext> escl = new ChildContextLookup<>(EntityStatementContext.class);
+ assert escl != null;
+ entityStatementContextLookupStrategy = escl;
+ }
+
+ /**
+ * Set the strategy used to lookup the {@link EntityStatementContext} to use.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setEntityStatementContextCreationStrategy(
+ @Nonnull final Function<MessageContext,EntityStatementContext> strategy) {
+ entityStatementContextLookupStrategy =
+ Constraint.isNotNull(strategy, "EntityStatementContext lookup strategy cannot be null");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ @Nullable public JWTClaimsSet apply(@Nullable final MessageContext messageContext) {
+ if (messageContext == null) {
+ return null;
+ }
+ final EntityStatementContext entityStatementCtx = entityStatementContextLookupStrategy.apply(messageContext);
+ if (entityStatementCtx == null) {
+ return null;
+ }
+ final JWT jwt = entityStatementCtx.getJWT();
+ try {
+ if (jwt != null) {
+ return jwt.getJWTClaimsSet();
+ }
+ } catch (final ParseException e) {
+ log.error("Could not fetch the claims set from entity statement", e);
+ }
+ return null;
+ }
+}
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/LookupCachedNimbusResponse.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/LookupCachedNimbusResponse.java
new file mode 100644
index 0000000..ff390fa
--- /dev/null
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/LookupCachedNimbusResponse.java
@@ -0,0 +1,141 @@
+/*
+ * 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.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.metadata.cache.local.NimbusResponseContainer;
+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 entity statement. 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 LookupCachedNimbusResponse extends AbstractProfileAction {
+
+ /** Class logger. */
+ @Nonnull private Logger log = LoggerFactory.getLogger(LookupCachedNimbusResponse.class);
+
+ /** Strategy used to create the cached message context. */
+ @Nonnull
+ private Function<ProfileRequestContext, RelyingPartyCachedMessageContext> cachedMessageContextCreationStrategy;
+
+ /** Metadata cache for cached response containers. */
+ @NonnullAfterInit private MetadataCache<NimbusResponseContainer> responseCache;
+
+ /** Cached message context to operate on. */
+ @NonnullBeforeExec private RelyingPartyCachedMessageContext cachedMessageContext;
+
+ /**
+ * Constructor.
+ */
+ public LookupCachedNimbusResponse() {
+ final Function<ProfileRequestContext, RelyingPartyCachedMessageContext> recls =
+ new ChildContextLookup<>(RelyingPartyCachedMessageContext.class, true);
+ assert recls != null;
+ cachedMessageContextCreationStrategy = recls;
+ }
+
+ /**
+ * Set the strategy used to create the cached message context
+ *
+ * @param strategy What to set.
+ */
+ public void setCachedMessageContextCreationStrategy(
+ @Nonnull final Function<ProfileRequestContext,RelyingPartyCachedMessageContext> strategy) {
+ checkSetterPreconditions();
+ cachedMessageContextCreationStrategy = 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<NimbusResponseContainer> 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 = cachedMessageContextCreationStrategy.apply(profileRequestContext);
+ if (cachedMessageContext == null) {
+ log.error("{} Could not create cached response context", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return false;
+ }
+
+ return true;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+ final CriteriaSet criteria = new CriteriaSet();
+ try {
+ final List<NimbusResponseContainer> result = responseCache.get(criteria);
+ if (result.size() != 1) {
+ log.debug("{} No cached response record found from the metadata cache", getLogPrefix(), result.size());
+ } else {
+ final NimbusResponseContainer 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/RelyingPartyCachedMessageContext.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/RelyingPartyCachedMessageContext.java
new file mode 100644
index 0000000..66226fe
--- /dev/null
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/RelyingPartyCachedMessageContext.java
@@ -0,0 +1,75 @@
+/*
+ * 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 javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.BaseContext;
+
+import com.nimbusds.oauth2.sdk.Request;
+import com.nimbusds.oauth2.sdk.Response;
+
+/**
+ * Subcontext carrying information for request and response messages related to a relying party.
+ */
+public final class RelyingPartyCachedMessageContext extends BaseContext {
+
+ /** Validated (possibly modified) request message. */
+ @Nullable private Request validatedRequest;
+
+ /** Cached response message. */
+ @Nullable private Response cachedResponse;
+
+ /**
+ * Get the validated (possibly modified) resolve entity request.
+ *
+ * @return the validated request
+ */
+ @Nullable public Request getValidatedRequest() {
+ return validatedRequest;
+ }
+
+ /**
+ * Set the the validated (possibly modified) resolve entity request.
+ *
+ * @param request the validated request
+ * @return this context
+ */
+ @Nonnull public RelyingPartyCachedMessageContext setValidatedRequest(@Nullable final Request request) {
+ validatedRequest = request;
+ return this;
+ }
+
+ /**
+ * Get the cached response message.
+ *
+ * @return the cached response
+ */
+ @Nullable public Response getCachedResponse() {
+ return cachedResponse;
+ }
+
+ /**
+ * Set the cached response message.
+ *
+ * @param response cached response
+ * @return this context
+ */
+ @Nonnull public RelyingPartyCachedMessageContext setCachedResponse(@Nullable final Response response) {
+ cachedResponse = response;
+ return this;
+ }
+}
\ No newline at end of file
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/navigate/DefaultEntityConfigurationTrustMarksLookupStrategy.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/navigate/DefaultEntityConfigurationTrustMarksLookupStrategy.java
new file mode 100644
index 0000000..d207f6b
--- /dev/null
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/navigate/DefaultEntityConfigurationTrustMarksLookupStrategy.java
@@ -0,0 +1,77 @@
+/*
+ * 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.ArrayList;
+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.profile.context.ProfileRequestContext;
+
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
+import net.shibboleth.shared.logic.Constraint;
+
+/**
+ * Default strategy to fetch trust marks to be included in the entity configuration.
+ */
+public class DefaultEntityConfigurationTrustMarksLookupStrategy extends AbstractIdentifiableInitializableComponent
+ implements Function<ProfileRequestContext,List<Map<String,String>>> {
+
+ /** Lookup strategies to fetch trust mark values to be included in the entity configuration. */
+ @Nonnull private List<Function<ProfileRequestContext,Map<String,String>>> trustMarkLookupStrategies;
+
+ /**
+ * Constructor.
+ */
+ public DefaultEntityConfigurationTrustMarksLookupStrategy() {
+ trustMarkLookupStrategies = CollectionSupport.emptyList();
+ }
+
+ /**
+ * Set the lookup strategies to fetch trust mark values to be included in the entity configuration.
+ *
+ * @param strategies lookup strategies
+ */
+ public void setTrustMarkLookupStrategies(
+ @Nonnull final List<Function<ProfileRequestContext,Map<String,String>>> strategies) {
+ checkSetterPreconditions();
+ Constraint.isNotNull(strategies, "Trust mark lookup strategies cannot be null");
+ trustMarkLookupStrategies = strategies;
+ }
+
+ /** {@inheritDoc} */
+ @Override @Nonnull
+ public List<Map<String, String>> apply(@Nullable final ProfileRequestContext profileRequestContext) {
+ checkComponentActive();
+ final List<Map<String, String>> trustMarks = new ArrayList<>();
+ for (final Function<ProfileRequestContext,Map<String,String>> strategy : trustMarkLookupStrategies) {
+ if (strategy == null) {
+ continue;
+ }
+ Optional.ofNullable(strategy.apply(profileRequestContext))
+ .filter(trustMark -> trustMark != null && !trustMark.isEmpty())
+ .ifPresent(trustMark -> trustMarks.add(trustMark));
+
+ }
+ return CollectionSupport.copyToList(trustMarks);
+ }
+
+}
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/navigate/DefaultTrustAnchorHintsLookupStrategy.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/navigate/DefaultTrustAnchorHintsLookupStrategy.java
new file mode 100644
index 0000000..a72bd62
--- /dev/null
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/navigate/DefaultTrustAnchorHintsLookupStrategy.java
@@ -0,0 +1,89 @@
+/*
+ * 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.Map;
+import java.util.Objects;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import net.shibboleth.oidc.metadata.cache.MetadataCache;
+import net.shibboleth.oidc.metadata.cache.MetadataCacheException;
+import net.shibboleth.oidfed.metadata.cache.local.LocalKeyContainer;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.resolver.CriteriaSet;
+
+/**
+ * Default strategy to fetch value for the trust_amchor_hints -claim. The value is fetched from the configurable
+ * {@link MetadataCache} containing trusted trust anchors.
+ */
+public class DefaultTrustAnchorHintsLookupStrategy extends AbstractIdentifiableInitializableComponent
+ implements Function<ProfileRequestContext,List<String>> {
+
+ /** Class logger. */
+ @Nonnull private Logger log = LoggerFactory.getLogger(DefaultTrustAnchorHintsLookupStrategy.class);
+
+ /** Cache containing trusted trust anchors. */
+ @NonnullAfterInit private MetadataCache<Map<String, LocalKeyContainer>> trustAnchorsCache;
+
+ /**
+ * Set the cache containing trusted trust anchors.
+ *
+ * @param cache trust anchors cache
+ */
+ public void setTrustAnchorsCache(
+ @Nonnull final MetadataCache<Map<String, LocalKeyContainer>> cache) {
+ checkSetterPreconditions();
+ trustAnchorsCache = Constraint.isNotNull(cache, "Trust Anchors cache cannot be null");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+
+ if (trustAnchorsCache == null) {
+ throw new ComponentInitializationException("Trust Anchors cache cannot be null");
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override @Nullable
+ public List<String> apply(@Nullable final ProfileRequestContext profileRequestContext) {
+ checkComponentActive();
+ final List<Map<String, LocalKeyContainer>> keyContainers;
+ try {
+ keyContainers = trustAnchorsCache.get(new CriteriaSet());
+ } catch (final MetadataCacheException e) {
+ log.warn("Could not resolve any trust anchors", e);
+ return null;
+ }
+ if (keyContainers.isEmpty()) {
+ log.debug("No keycontainers returned from the trust anchor cache");
+ return null;
+ }
+ return keyContainers.get(0).keySet().stream().filter(Objects::nonNull).toList();
+ }
+}
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list