[java-idp-plugin-oidc-op-oidfed] branch main updated: Initial implementation of attribute filter policy rule for entity trust marks

Henri Mikkonen henri.mikkonen at iki.fi
Fri Oct 10 16:58:20 UTC 2025


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

hjmikkon pushed a commit to branch main
in repository java-idp-plugin-oidc-op-oidfed.

View the commit online:
http://git.shibboleth.net/view/?p=java-idp-plugin-oidc-op-oidfed.git;a=commit;h=9bbb2e57ff4a4430a7d4fc1d07cbfe63a97a1487

The following commit(s) were added to refs/heads/main by this push:
     new 9bbb2e5  Initial implementation of attribute filter policy rule for entity trust marks
9bbb2e5 is described below

commit 9bbb2e57ff4a4430a7d4fc1d07cbfe63a97a1487
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Fri Oct 10 19:58:04 2025 +0300

    Initial implementation of attribute filter policy rule for entity trust marks
    
    - Schema 'urn:mace:shibboleth:2.0:afp:oidfed'
    - Flow test case to issue email-claim only if entity has 'https://example.org/email-allowing-trust-mark' trust mark
    - Also fixed and improved trust mark parsing according to the current spec
---
 idp-oidfed-op-impl/pom.xml                         |  10 +-
 .../policyrule/impl/EntityTrustMarkPolicyRule.java | 139 +++++++++++++++++++++
 .../impl/AttributeFilterNamespaceHandler.java      |  35 ++++++
 .../impl/EntityTrustMarkPolicyRuleParser.java      |  40 ++++++
 ...DefaultPayloadJOSEObjectCredentialResolver.java |   9 +-
 ...DefaultTrustChainTrustMarksParsingStrategy.java |  80 ++++++++++--
 .../op/oidfed/profile/impl/ResolveTrustMarks.java  |  21 +++-
 .../oidfed/metadata-lookup-ext-oidfed-beans.xml    |   4 +
 .../idp/flows/oidfed/register/register-beans.xml   |   4 +
 .../oidfed/resolve-entity/resolve-entity-beans.xml |   4 +
 .../src/main/resources/META-INF/spring.handlers    |   1 +
 .../src/main/resources/META-INF/spring.schemas     |   2 +
 .../resources/schema/shibboleth-afp-oidfed.xsd     |  24 ++++
 .../flow/oidfed/AbstractFederationFlowTest.java    |  86 +++++++++----
 .../UserInfoFlowAutomaticRegistrationTest.java     |  86 +++++++++++++
 .../idp/module/conf/attribute-filter.xml           |   8 +-
 16 files changed, 507 insertions(+), 46 deletions(-)

diff --git a/idp-oidfed-op-impl/pom.xml b/idp-oidfed-op-impl/pom.xml
index e71326d..6557347 100644
--- a/idp-oidfed-op-impl/pom.xml
+++ b/idp-oidfed-op-impl/pom.xml
@@ -265,6 +265,11 @@
             <artifactId>shib-attribute-filter-api</artifactId>
             <scope>provided</scope>
         </dependency>
+        <dependency>
+            <groupId>${shib-attribute.groupId}</groupId>
+            <artifactId>shib-attribute-filter-spring</artifactId>
+            <scope>provided</scope>
+        </dependency>
         <dependency>
             <groupId>net.minidev</groupId>
             <artifactId>json-smart</artifactId>     
@@ -362,11 +367,6 @@
             <artifactId>shib-attribute-filter-impl</artifactId>
             <scope>test</scope>
         </dependency>    
-        <dependency>
-            <groupId>${shib-shared.groupId}</groupId>
-            <artifactId>shib-attribute-filter-spring</artifactId>
-            <scope>test</scope>
-        </dependency>
         <dependency>
             <groupId>${opensaml.groupId}</groupId>
             <artifactId>opensaml-messaging-impl</artifactId>
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/attribute/filter/policyrule/impl/EntityTrustMarkPolicyRule.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/attribute/filter/policyrule/impl/EntityTrustMarkPolicyRule.java
new file mode 100644
index 0000000..c7dcc0d
--- /dev/null
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/attribute/filter/policyrule/impl/EntityTrustMarkPolicyRule.java
@@ -0,0 +1,139 @@
+/*
+ * 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.attribute.filter.policyrule.impl;
+
+import java.util.Optional;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.messaging.context.navigate.RecursiveTypedParentContextLookup;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.attribute.filter.PolicyRequirementRule;
+import net.shibboleth.idp.attribute.filter.context.AttributeFilterContext;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.RelyingPartyTrustChainContext;
+import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * Compare the verified trust mark IDs for the entity with the provided value.
+ */
+public class EntityTrustMarkPolicyRule extends AbstractIdentifiableInitializableComponent
+    implements PolicyRequirementRule {
+
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(EntityTrustMarkPolicyRule.class);
+
+    /** Lookup strategy to locate trust chain context. */
+    @Nonnull private Function<ProfileRequestContext, RelyingPartyTrustChainContext> trustChainContextLookupStrategy;
+
+    /** String to match for a positive evaluation. */
+    @Nullable private String matchString;
+
+    /** The String used to prefix log message. */
+    @Nullable private String logPrefix;
+
+    /**
+     * Constructor.
+     */
+    public EntityTrustMarkPolicyRule() {
+        final Function<ProfileRequestContext, RelyingPartyTrustChainContext> tcls =
+                new ChildContextLookup<>(RelyingPartyTrustChainContext.class, false).compose(
+                        new InboundMessageContextLookup());
+        assert tcls != null;
+        trustChainContextLookupStrategy = tcls;
+        logPrefix = null;
+    }
+
+    /**
+     * Set the lookup strategy to locate trust chain context.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setTrustChainContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext, RelyingPartyTrustChainContext> strategy) {
+        checkSetterPreconditions();
+        trustChainContextLookupStrategy =
+                Constraint.isNotNull(strategy, "TrustChainContextLookupStrategy cannot be null");
+    }
+
+    /**
+     * Sets the string to match for a positive evaluation.
+     * 
+     * @param match string to match for a positive evaluation
+     */
+    public void setMatchString(@Nullable final String match) {
+        checkSetterPreconditions();
+        matchString = match;
+    }
+
+    /**
+     * Return a string which is to be prepended to all log messages.
+     * 
+     * @return "Attribute Filter '<filterID>' :"
+     */
+    @Nonnull protected String getLogPrefix() {
+        // local cache of cached entry to allow unsynchronised clearing.
+        String prefix = logPrefix;
+        if (null == prefix) {
+            final StringBuilder builder = new StringBuilder("Attribute Filter '").append(getId()).append("':");
+            prefix = builder.toString();
+            if (null == logPrefix) {
+                logPrefix = prefix;
+            }
+        }
+        
+        assert prefix != null;
+        return prefix;
+    }
+
+    /**
+     * Compare the authentication request scopes with the provided string.
+     * 
+     * @param filterContext the context
+     * @return whether it matches
+     */
+    @Override
+    public Tristate matches(@Nonnull final AttributeFilterContext filterContext) {
+        checkComponentActive();
+        final RelyingPartyTrustChainContext trustChainContext = trustChainContextLookupStrategy.apply(
+                new RecursiveTypedParentContextLookup<>(ProfileRequestContext.class).apply(filterContext));
+        if (trustChainContext == null) {
+            log.debug("{} No RelyingPartyTrustChainContext resolved", getLogPrefix());
+            return Tristate.FALSE;
+        }
+        final String entityId = Optional.ofNullable(trustChainContext.getSelectedTrustChain())
+                .map(pair -> pair.getFirst())
+                .map(chain -> chain.get(0))
+                .map(statement -> statement.getEntityID().getValue())
+                .orElse(null);
+        if (entityId == null) {
+            log.debug("{} Could not resolve entity ID from selected trust chain", getLogPrefix());
+            return Tristate.FALSE;
+        }
+        final Boolean result = Optional.ofNullable(trustChainContext.getVerifiedTrustMarkIds())
+                .map(map -> map.get(entityId))
+                .map(ids -> ids.contains(matchString))
+                .orElse(Boolean.FALSE);
+        log.debug("{} Returning {}", getLogPrefix(), result);
+        return result.booleanValue() ? Tristate.TRUE : Tristate.FALSE;
+    }
+}
\ No newline at end of file
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/attribute/filter/spring/impl/AttributeFilterNamespaceHandler.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/attribute/filter/spring/impl/AttributeFilterNamespaceHandler.java
new file mode 100644
index 0000000..9036ab8
--- /dev/null
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/attribute/filter/spring/impl/AttributeFilterNamespaceHandler.java
@@ -0,0 +1,35 @@
+/*
+ * 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.attribute.filter.spring.impl;
+
+import net.shibboleth.idp.plugin.oidc.op.oidfed.attribute.filter.spring.policyrule.impl.EntityTrustMarkPolicyRuleParser;
+import net.shibboleth.shared.spring.custom.BaseSpringNamespaceHandler;
+
+/**
+ * Namespace handler for the OpenID federation specific attribute filter engine functions.
+ */
+public class AttributeFilterNamespaceHandler extends BaseSpringNamespaceHandler {
+
+    /** OIDFed namespace. */
+    public static final String NAMESPACE = "urn:mace:shibboleth:2.0:afp:oidfed";
+
+    /** {@inheritDoc} */
+    @Override
+    public void init() {
+        // Policy rules
+        registerBeanDefinitionParser(EntityTrustMarkPolicyRuleParser.SCHEMA_TYPE_AFP,
+                new EntityTrustMarkPolicyRuleParser());
+    }
+}
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/attribute/filter/spring/policyrule/impl/EntityTrustMarkPolicyRuleParser.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/attribute/filter/spring/policyrule/impl/EntityTrustMarkPolicyRuleParser.java
new file mode 100644
index 0000000..0891184
--- /dev/null
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/attribute/filter/spring/policyrule/impl/EntityTrustMarkPolicyRuleParser.java
@@ -0,0 +1,40 @@
+/*
+ * 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.attribute.filter.spring.policyrule.impl;
+
+import javax.annotation.Nonnull;
+import javax.xml.namespace.QName;
+
+import net.shibboleth.idp.plugin.oidc.op.oidfed.attribute.filter.policyrule.impl.EntityTrustMarkPolicyRule;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.attribute.filter.spring.impl.AttributeFilterNamespaceHandler;
+import net.shibboleth.idp.attribute.filter.spring.policyrule.impl.AbstractStringPolicyRuleParser;
+
+/**
+ * Bean definition parser for {@link EntityTrustMarkPolicyRule}.
+ */
+public class EntityTrustMarkPolicyRuleParser extends AbstractStringPolicyRuleParser {
+
+    /** Schema type. */
+    @Nonnull public static final QName SCHEMA_TYPE_AFP =
+            new QName(AttributeFilterNamespaceHandler.NAMESPACE, "OIDFEDEntityTrustMark");
+
+    /** {@inheritDoc} */
+    @Override
+    @Nonnull
+    protected Class<EntityTrustMarkPolicyRule> getNativeBeanClass() {
+        return EntityTrustMarkPolicyRule.class;
+    }
+
+}
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultPayloadJOSEObjectCredentialResolver.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultPayloadJOSEObjectCredentialResolver.java
index 99fc68b..895728f 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultPayloadJOSEObjectCredentialResolver.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultPayloadJOSEObjectCredentialResolver.java
@@ -15,6 +15,7 @@
 package net.shibboleth.idp.plugin.oidc.op.oidfed.metadata;
 
 import java.text.ParseException;
+import java.util.Map;
 import java.util.Objects;
 
 import javax.annotation.Nonnull;
@@ -29,6 +30,7 @@ import com.nimbusds.jwt.SignedJWT;
 
 import net.shibboleth.oidc.security.credential.impl.BasicJOSEObjectCredentialResolver;
 import net.shibboleth.oidc.security.jose.criterion.JOSEObjectCriterion;
+import net.shibboleth.shared.collection.CollectionSupport;
 import net.shibboleth.shared.primitive.LoggerFactory;
 import net.shibboleth.shared.resolver.CriteriaSet;
 import net.shibboleth.shared.resolver.ResolverException;
@@ -56,7 +58,12 @@ public class DefaultPayloadJOSEObjectCredentialResolver extends BasicJOSEObjectC
         }
         try {
             final SignedJWT jwt = SignedJWT.parse(joseObject.serialize());
-            final JWKSet jwks = JWKSet.parse(jwt.getJWTClaimsSet().getJSONObjectClaim("jwks"));
+            final Map<String, Object> rawJwks = jwt.getJWTClaimsSet().getJSONObjectClaim("jwks");
+            if (rawJwks == null || rawJwks.isEmpty()) {
+                log.debug("No jwks found from the payload");
+                return CollectionSupport.emptyList();
+            }
+            final JWKSet jwks = JWKSet.parse(rawJwks);
             return jwks.getKeys().stream()
                     .filter(Objects::nonNull)
                     .map(jwk -> buildJWKCredential(jwk, null))
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultTrustChainTrustMarksParsingStrategy.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultTrustChainTrustMarksParsingStrategy.java
index 5f98e06..623ef0b 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultTrustChainTrustMarksParsingStrategy.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultTrustChainTrustMarksParsingStrategy.java
@@ -23,14 +23,22 @@ import java.util.function.Function;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
-import javax.annotation.concurrent.ThreadSafe;
 
 import org.slf4j.Logger;
 
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JavaType;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.type.MapType;
 import com.nimbusds.jwt.JWTClaimsSet;
 import com.nimbusds.jwt.SignedJWT;
 import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
 
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.ThreadSafeAfterInit;
+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.primitive.StringSupport;
 
@@ -40,16 +48,38 @@ import net.shibboleth.shared.primitive.StringSupport;
  * 
  * TODO: iat / subject validation (switch into using claims validators)
  */
- at ThreadSafe
-public class DefaultTrustChainTrustMarksParsingStrategy
+ at ThreadSafeAfterInit
+public class DefaultTrustChainTrustMarksParsingStrategy extends AbstractIdentifiableInitializableComponent
         implements Function<List<EntityStatement>,Map<String,List<SignedJWT>>> {
 
     /** Class logger. */
     @Nonnull private Logger log = LoggerFactory.getLogger(DefaultTrustChainTrustMarksParsingStrategy.class);
 
+    /** JSON object mapper used for decoding JSON into Map. */
+    @NonnullAfterInit private ObjectMapper objectMapper;
+
+    /**
+     * Set the JSON {@link ObjectMapper} used for decoding JSON into Map.
+     * 
+     * @param mapper object mapper
+     */
+    public void setObjectMapper(@Nonnull final ObjectMapper mapper) {
+        checkSetterPreconditions();
+
+        objectMapper = Constraint.isNotNull(mapper, "Object mapper cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        if (objectMapper == null) {
+            throw new ComponentInitializationException("Object mapper cannot be null");
+        }
+    }
     /** {@inheritDoc} */
     @Override @Nullable
     public Map<String, List<SignedJWT>> apply(@Nullable final List<EntityStatement> trustChain) {
+        checkComponentActive();
         if (trustChain == null || trustChain.size() < 3) {
             log.error("Unexpected length in the trust chain: {}", trustChain == null ? "null" : trustChain.size());
             return null;
@@ -59,18 +89,18 @@ public class DefaultTrustChainTrustMarksParsingStrategy
         for (final EntityStatement statement : trustChain) {
             assert statement != null;
             final SignedJWT statementJwt = statement.getSignedStatement();
-            log.trace("Inspecting entity statement {} with trust marks {}",
-                    statementJwt.serialize(), statement.getClaimsSet().getTrustMarks());
-
             final List<Object> rawTrustMarks = statement.getClaimsSet().getJSONArrayClaim("trust_marks");
+            log.trace("Inspecting entity statement {} with trust marks {}", statementJwt.serialize(), rawTrustMarks);
+
             if (rawTrustMarks != null && !rawTrustMarks.isEmpty()) {
+                log.trace("Transforming the trust mark into a JWT");
                 final List<SignedJWT> trustMarks = rawTrustMarks
                     .stream()
-                    .filter(Map.class::isInstance)
-                    .map(Map.class::cast)
-                    .map(map -> parseTrustMark(map.get("trust_mark"), map.get("trust_mark_type")))
+                    .map(entry -> parseTrustMark(entry))
                     .filter(Objects::nonNull)
                     .toList();
+                log.debug("Returning {} trust marks for entity {}", trustMarks.size(),
+                        statement.getEntityID().getValue());
                 result.put(statement.getEntityID().getValue(), trustMarks);
             }
         }
@@ -78,15 +108,41 @@ public class DefaultTrustChainTrustMarksParsingStrategy
         return result;
     }
 
+    /**
+     * Parses and validates JWT from the trust mark entry.
+     * 
+     * @param trustMarkEntry trust mark entry as Strign to be parsed into a JWT
+     * @return trust mark JWT if valid, null otherwise
+     */
+    @Nullable private SignedJWT parseTrustMark(@Nullable final Object trustMarkEntry) {
+        if (trustMarkEntry == null) {
+            return null;
+        }
+        final JavaType stringType = objectMapper.constructType(String.class);
+        final MapType stringMapType =
+                objectMapper.getTypeFactory().constructMapType(Map.class, stringType, stringType);
+        try {
+            final Map<String,String> map = objectMapper.readValue(trustMarkEntry.toString(), stringMapType);
+            if (map != null) {
+                log.debug("Parsed trust_mark map {}", map);
+                return verifyTrustMark(map.get("trust_mark"), map.get("trust_mark_type"));
+            }
+        } catch (final JsonProcessingException e) {
+            log.warn("Could not parse trust mark issuers from the trust chain", e);
+        }
+        return null;
+    }
+
     /**
      * Verifies the trust mark id and issuer claims.
      * 
-     * @param trustMark trust mark to be verified, expected to be parseable from string
+     * @param trustMark trust mark to be verified
      * @param id the id to be verified from the JWT claims set
      * @return trust mark JWT if valid, null otherwise
      */
-    @Nullable private SignedJWT parseTrustMark(@Nullable final Object trustMark, @Nullable final Object id) {
-        if (trustMark == null || !(trustMark instanceof String) || id == null || !(id instanceof String)) {
+    @Nullable private SignedJWT verifyTrustMark(@Nullable final String trustMark, @Nullable final String id) {
+        if (trustMark == null || id == null) {
+            log.trace("Could not parse trust mark {} with trust_mark_type {}", trustMark, id);
             return null;
         }
         try {
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ResolveTrustMarks.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ResolveTrustMarks.java
index 5c7500a..28cbbbe 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ResolveTrustMarks.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ResolveTrustMarks.java
@@ -40,7 +40,6 @@ import com.nimbusds.jwt.JWTClaimsSet;
 import com.nimbusds.jwt.SignedJWT;
 import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
 
-import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultTrustChainTrustMarksParsingStrategy;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.SubjectEntityIDCriterion;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.SubjectEntityStatementCriterion;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.TrustMarkOwnersCriterion;
@@ -78,7 +77,8 @@ public class ResolveTrustMarks extends AbstractProfileAction {
     @Nonnull private Function<ProfileRequestContext, RelyingPartyTrustChainContext> trustChainContextLookupStrategy;
 
     /** Strategy used to parse trust marks from the selected trust chain. */
-    @Nonnull private Function<List<EntityStatement>,Map<String,List<SignedJWT>>> trustChainTrustMarksParsingStrategy;
+    @NonnullAfterInit
+    private Function<List<EntityStatement>,Map<String,List<SignedJWT>>> trustChainTrustMarksParsingStrategy;
 
     /** Strategy used to lookup trusted trust mark issuers for the trust chain. */
     @NonnullAfterInit
@@ -128,7 +128,6 @@ public class ResolveTrustMarks extends AbstractProfileAction {
                         new InboundMessageContextLookup());
         assert tcls != null;
         trustChainContextLookupStrategy = tcls;
-        trustChainTrustMarksParsingStrategy = new DefaultTrustChainTrustMarksParsingStrategy();
         trustedTrustMarkIssuersOnlyCondition = PredicateSupport.alwaysTrue();
     }
 
@@ -143,6 +142,18 @@ public class ResolveTrustMarks extends AbstractProfileAction {
         trustChainContextLookupStrategy =
                 Constraint.isNotNull(strategy, "TrustChainContextLookupStrategy cannot be null");
     }
+    
+    /**
+     * Set the strategy used to parse trust marks from the selected trust chain.
+     * 
+     * @param strategy parsing strategy
+     */
+    public void setTrustChainTrustMarksParsingStrategy(
+            @Nonnull final Function<List<EntityStatement>,Map<String,List<SignedJWT>>> strategy) {
+        checkSetterPreconditions();
+        trustChainTrustMarksParsingStrategy =
+                Constraint.isNotNull(strategy, "TrustChainTrustMarksParsingStrategy cannot be null");
+    }
 
     /**
      * Set the strategy used to lookup trusted trust mark issuers for the trust chain.
@@ -247,6 +258,9 @@ public class ResolveTrustMarks extends AbstractProfileAction {
         if (delegationTrustEngine == null) {
             throw new ComponentInitializationException("Delegation Trust Engine cannot be null");
         }
+        if (trustChainTrustMarksParsingStrategy == null) {
+            throw new ComponentInitializationException("Trust marks parsing strategy cannot be null");
+        }
         if (trustedTrustMarkIssuersLookupStrategy == null) {
             throw new ComponentInitializationException("Trusted trust mark issuers lookup strategy cannot be null");
         }
@@ -476,6 +490,7 @@ public class ResolveTrustMarks extends AbstractProfileAction {
         final EntityStatement trustMarkIssuer = trustMarkChain.get(0);
         assert trustMarkIssuer != null;
         final CriteriaSet criteria = new CriteriaSet(new SubjectEntityStatementCriterion(trustMarkIssuer));
+        log.trace("{} Validating entity statement {}", getLogPrefix(), trustMarkIssuer.getSignedStatement().serialize());
         try {
             if (trustEngine.validate(jwt, criteria)) {
                 final String id = getTrustMarkId(jwt);
diff --git a/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup-ext/oidfed/metadata-lookup-ext-oidfed-beans.xml b/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup-ext/oidfed/metadata-lookup-ext-oidfed-beans.xml
index 3c502ef..d8c2f11 100644
--- a/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup-ext/oidfed/metadata-lookup-ext-oidfed-beans.xml
+++ b/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup-ext/oidfed/metadata-lookup-ext-oidfed-beans.xml
@@ -110,6 +110,10 @@
         p:delegatedTrustMarkClaimsValidationLookupStrategy="#{getObject('shibboleth.oidfed.DelegatedTrustMarkClaimsValidationLookupStrategy') ?: getObject('DefaultTrustMarkClaimsValidationLookupStrategy')}"
         p:trustEngine-ref="shibboleth.oidfed.DefaultEntityConfigurationTrustEngine"
         p:delegationTrustEngine-ref="shibboleth.oidfed.DefaultDelegatedTrustMarkTrustEngine">
+        <property name="trustChainTrustMarksParsingStrategy">
+            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultTrustChainTrustMarksParsingStrategy"
+                p:objectMapper-ref="shibboleth.oidc.JSONObjectMapper" />
+        </property>
         <property name="trustedTrustMarkIssuersLookupStrategy">
             <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultTrustChainTrustedTrustMarkIssuersLookupStrategy"
                 p:objectMapper-ref="shibboleth.oidc.JSONObjectMapper" />
diff --git a/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/register/register-beans.xml b/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/register/register-beans.xml
index bd090cc..33283ad 100644
--- a/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/register/register-beans.xml
+++ b/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/register/register-beans.xml
@@ -188,6 +188,10 @@
         p:delegatedTrustMarkClaimsValidationLookupStrategy="#{getObject('shibboleth.oidfed.DelegatedTrustMarkClaimsValidationLookupStrategy') ?: getObject('DefaultTrustMarkClaimsValidationLookupStrategy')}"
         p:trustEngine-ref="shibboleth.oidfed.DefaultEntityConfigurationTrustEngine"
         p:delegationTrustEngine-ref="shibboleth.oidfed.DefaultDelegatedTrustMarkTrustEngine">
+        <property name="trustChainTrustMarksParsingStrategy">
+            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultTrustChainTrustMarksParsingStrategy"
+                p:objectMapper-ref="shibboleth.oidc.JSONObjectMapper" />
+        </property>
         <property name="trustedTrustMarkIssuersLookupStrategy">
             <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultTrustChainTrustedTrustMarkIssuersLookupStrategy"
                 p:objectMapper-ref="shibboleth.oidc.JSONObjectMapper" />
diff --git a/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/resolve-entity/resolve-entity-beans.xml b/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/resolve-entity/resolve-entity-beans.xml
index ee74c7e..cf45574 100644
--- a/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/resolve-entity/resolve-entity-beans.xml
+++ b/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/resolve-entity/resolve-entity-beans.xml
@@ -131,6 +131,10 @@
         p:delegatedTrustMarkClaimsValidationLookupStrategy="#{getObject('shibboleth.oidfed.DelegatedTrustMarkClaimsValidationLookupStrategy') ?: getObject('DefaultTrustMarkClaimsValidationLookupStrategy')}"
         p:trustEngine-ref="shibboleth.oidfed.DefaultEntityConfigurationTrustEngine"
         p:delegationTrustEngine-ref="shibboleth.oidfed.DefaultDelegatedTrustMarkTrustEngine">
+        <property name="trustChainTrustMarksParsingStrategy">
+            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultTrustChainTrustMarksParsingStrategy"
+                p:objectMapper-ref="shibboleth.oidc.JSONObjectMapper" />
+        </property>
         <property name="trustedTrustMarkIssuersLookupStrategy">
             <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultTrustChainTrustedTrustMarkIssuersLookupStrategy"
                 p:objectMapper-ref="shibboleth.oidc.JSONObjectMapper" />
diff --git a/idp-oidfed-op-impl/src/main/resources/META-INF/spring.handlers b/idp-oidfed-op-impl/src/main/resources/META-INF/spring.handlers
new file mode 100644
index 0000000..7e9c56c
--- /dev/null
+++ b/idp-oidfed-op-impl/src/main/resources/META-INF/spring.handlers
@@ -0,0 +1 @@
+urn\:mace\:shibboleth\:2.0\:afp\:oidfed = net.shibboleth.idp.plugin.oidc.op.oidfed.attribute.filter.spring.impl.AttributeFilterNamespaceHandler
\ No newline at end of file
diff --git a/idp-oidfed-op-impl/src/main/resources/META-INF/spring.schemas b/idp-oidfed-op-impl/src/main/resources/META-INF/spring.schemas
new file mode 100644
index 0000000..85b0d7c
--- /dev/null
+++ b/idp-oidfed-op-impl/src/main/resources/META-INF/spring.schemas
@@ -0,0 +1,2 @@
+classpath\:/schema/shibboleth-afp-oidfed.xsd = schema/shibboleth-afp-oidfed.xsd
+http\://shibboleth.net/schema/oidfed/shibboleth-afp-oidfed.xsd = schema/shibboleth-afp-oidfed.xsd
\ No newline at end of file
diff --git a/idp-oidfed-op-impl/src/main/resources/schema/shibboleth-afp-oidfed.xsd b/idp-oidfed-op-impl/src/main/resources/schema/shibboleth-afp-oidfed.xsd
new file mode 100644
index 0000000..5f57c35
--- /dev/null
+++ b/idp-oidfed-op-impl/src/main/resources/schema/shibboleth-afp-oidfed.xsd
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<schema xmlns="http://www.w3.org/2001/XMLSchema" xmlns:afp="urn:mace:shibboleth:2.0:afp"
+    targetNamespace="urn:mace:shibboleth:2.0:afp:oidfed"
+    elementFormDefault="qualified">
+
+    <import namespace="urn:mace:shibboleth:2.0:afp"
+        schemaLocation="http://shibboleth.net/schema/idp/shibboleth-afp.xsd" />
+
+    <annotation>
+        <documentation>Schema for the OIDFed extension attribute filter policies.</documentation>
+    </annotation>
+
+    <complexType name="OIDFEDEntityTrustMark">
+        <annotation>
+            <documentation>
+                A match function that matches the verified trust mark IDs of the entity against the specified value.
+            </documentation>
+        </annotation>
+        <complexContent>
+            <extension base="afp:StringMatchType" />
+        </complexContent>
+    </complexType>
+
+</schema>
\ No newline at end of file
diff --git a/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/AbstractFederationFlowTest.java b/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/AbstractFederationFlowTest.java
index fe61a3d..eaa0934 100644
--- a/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/AbstractFederationFlowTest.java
+++ b/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/AbstractFederationFlowTest.java
@@ -79,6 +79,7 @@ import net.minidev.json.JSONObject;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.TrustChainTestUtil;
 import net.shibboleth.idp.plugin.oidc.op.profile.flow.AbstractOidcFlowTest;
 import net.shibboleth.oidc.security.credential.BasicJWKCredential;
+import net.shibboleth.shared.collection.CollectionSupport;
 import net.shibboleth.shared.logic.Constraint;
 
 /**
@@ -97,6 +98,7 @@ public class AbstractFederationFlowTest extends AbstractOidcFlowTest {
     final String anchorId = "https://trust-anchor.federation.local";
     final String anchorFetchEndpoint = anchorId + "/fetch";
     final String anchorResolveEndpoint = anchorId + "/resolve";
+    final String trustMarkIssuerId = "https://trust-mark-issuer.federation.local";
     String issuer = "https://op.example.org";
 
     JWK rpKey;
@@ -104,6 +106,7 @@ public class AbstractFederationFlowTest extends AbstractOidcFlowTest {
     JWK anchorKey;
     JWK trustedAnchorKey;
     JWK intermediateKey;
+    JWK trustMarkIssuerKey;
 
     String subject = "jdoe";
 
@@ -126,6 +129,7 @@ public class AbstractFederationFlowTest extends AbstractOidcFlowTest {
                 .keyID("locallyTrustedAnchorKey")
                 .build();
         intermediateKey = initializeNewJwk("RSA", 2048, "mockIntermediateKey");
+        trustMarkIssuerKey = initializeNewJwk("RSA", 2048, "mockTrustMarkIssuerKey");
     }
 
     @BeforeMethod
@@ -226,16 +230,23 @@ public class AbstractFederationFlowTest extends AbstractOidcFlowTest {
 
     protected String rpEntityConfiguration(final String clientId, final OIDCClientMetadata metadata,
             final String... authorityHints) throws URISyntaxException {
-        final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder().issuer(clientId).subject(clientId)
+        return rpEntityConfiguration(clientId, metadata, null, authorityHints);
+    }
+
+    protected String rpEntityConfiguration(final String clientId, final OIDCClientMetadata metadata,
+            final List<Map<String, String>> trustMarks, 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)
-                .build();
+                        new String[] { anchorId } : authorityHints);
+        if (trustMarks != null) {
+            builder.claim("trust_marks", trustMarks);
+        }
         final EntityStatement rpConfiguration =
-                TrustChainTestUtil.entityStatement(JWSAlgorithm.RS256, leafKey, claimsSet);
+                TrustChainTestUtil.entityStatement(JWSAlgorithm.RS256, leafKey, builder.build());
         return rpConfiguration.getSignedStatement().serialize();
     }
 
@@ -254,6 +265,20 @@ public class AbstractFederationFlowTest extends AbstractOidcFlowTest {
         return rpConfiguration.getSignedStatement().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", CollectionSupport.emptyMap()))
+                .build();
+        final EntityStatement rpConfiguration =
+                TrustChainTestUtil.entityStatement(JWSAlgorithm.RS256, trustMarkIssuerKey, claimsSet);
+        return rpConfiguration.getSignedStatement().serialize();
+    }
+    
     protected String opEntityConfiguration(final String issuer, final String... authorityHints)
             throws URISyntaxException {
         return opEntityConfiguration(issuer, emptyOpMetadata(issuer), authorityHints);
@@ -290,31 +315,32 @@ public class AbstractFederationFlowTest extends AbstractOidcFlowTest {
     }
 
     protected String trustedAnchorConfiguration() {
-        final String anchorId = "https://trust-anchor.federation.local";
-        final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder().issuer(anchorId).subject(anchorId)
-                .issueTime(Date.from(Instant.now()))
-                .expirationTime(Date.from(Instant.now().plusSeconds(300)))
-                .claim("jwks", new JWKSet(trustedAnchorKey).toJSONObject(true))
-                .claim("metadata", Map.of("federation_entity", Map.of("federation_fetch_endpoint",
-                        anchorFetchEndpoint, "federation_resolve_endpoint", anchorResolveEndpoint)))
-                .build();
-        final EntityStatement anchorConfiguration =
-                TrustChainTestUtil.entityStatement(JWSAlgorithm.RS256, trustedAnchorKey, claimsSet);
-        return anchorConfiguration.getSignedStatement().serialize();
+        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) {
+
         final String anchorId = "https://trust-anchor.federation.local";
-        final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder().issuer(anchorId).subject(anchorId)
+        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", new JWKSet(trustedAnchorKey).toJSONObject(true))
                 .claim("metadata", Map.of("federation_entity", Map.of("federation_fetch_endpoint",
-                        anchorFetchEndpoint)))
-                .claim("constraints", constraints)
-                .build();
+                        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, trustedAnchorKey, claimsSet);
+                TrustChainTestUtil.entityStatement(JWSAlgorithm.RS256, trustedAnchorKey, builder.build());
         return anchorConfiguration.getSignedStatement().serialize();
     }
 
@@ -378,6 +404,11 @@ public class AbstractFederationFlowTest extends AbstractOidcFlowTest {
     }
 
     protected String rpResolveEntityResponse(final String clientId, final Map<String, Object> metadata) {
+        return rpResolveEntityResponse(clientId, metadata, null);
+    }
+
+    protected String rpResolveEntityResponse(final String clientId, final Map<String, Object> metadata,
+            final List<Map<String, String>> trustMarks) {
         final List<String> trustChain;
         try {
             trustChain = List.of(rpEntityConfiguration(clientId),
@@ -393,6 +424,7 @@ public class AbstractFederationFlowTest extends AbstractOidcFlowTest {
                 .expirationTime(Date.from(Instant.now().plusSeconds(300)))
                 .claim("trust_chain", trustChain)
                 .claim("metadata", metadata)
+                .claim("trust_marks", trustMarks)
                 .build();
         return TrustChainTestUtil.signedJwt(JWSAlgorithm.RS256, trustedAnchorKey, "application/resolve-response+jwt",
                 claimsSet).serialize();
@@ -441,9 +473,14 @@ public class AbstractFederationFlowTest extends AbstractOidcFlowTest {
     }
 
     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(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration));
             mapResponse(subordinateStatementUrl(anchorFetchEndpoint, clientId),
                     mockResponse(subordinateStatement(clientId, Map.of("openid_relying_party",
                             new OIDCClientMetadata().toJSONObject()))));
@@ -473,11 +510,16 @@ public class AbstractFederationFlowTest extends AbstractOidcFlowTest {
     }
 
     protected void rpResolveEntityConfigureMockHttpClient(final String clientId, final OIDCClientMetadata metadata) {
+        rpResolveEntityConfigureMockHttpClient(clientId, metadata, null);
+    }
+
+    protected void rpResolveEntityConfigureMockHttpClient(final String clientId, final OIDCClientMetadata metadata,
+            final List<Map<String, String>> trustMarks) {
         try {
             mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
             mapResponse(resolveEntityUrl(anchorResolveEndpoint, clientId, anchorId),
                     mockResponse(rpResolveEntityResponse(clientId, Map.of("openid_relying_party",
-                            metadata.toJSONObject()))));
+                            metadata.toJSONObject()), trustMarks)));
         } catch (UnsupportedOperationException | IOException e) {
             Assert.fail("Could not initialize mock HTTP client", e);
         }
diff --git a/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/UserInfoFlowAutomaticRegistrationTest.java b/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/UserInfoFlowAutomaticRegistrationTest.java
index c692231..33b4d69 100644
--- a/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/UserInfoFlowAutomaticRegistrationTest.java
+++ b/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/UserInfoFlowAutomaticRegistrationTest.java
@@ -20,6 +20,7 @@ import java.net.URISyntaxException;
 import java.security.NoSuchAlgorithmException;
 import java.time.Instant;
 import java.util.List;
+import java.util.Map;
 
 import org.opensaml.storage.StorageService;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -29,6 +30,8 @@ import org.testng.Assert;
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.jwk.JWKSet;
 import com.nimbusds.oauth2.sdk.OAuth2Error;
 import com.nimbusds.oauth2.sdk.Scope;
 import com.nimbusds.oauth2.sdk.id.ClientID;
@@ -36,11 +39,14 @@ import com.nimbusds.oauth2.sdk.token.BearerAccessToken;
 import com.nimbusds.oauth2.sdk.token.BearerTokenError;
 import com.nimbusds.openid.connect.sdk.UserInfoSuccessResponse;
 import com.nimbusds.openid.connect.sdk.claims.UserInfo;
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientMetadata;
 
+import net.shibboleth.idp.plugin.oidc.op.oidfed.TrustChainTestUtil;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.support.ClaimsSetExtensionSupport;
 import net.shibboleth.idp.plugin.oidc.op.profile.flow.UserInfoTest;
 import net.shibboleth.idp.plugin.oidc.op.token.support.AccessTokenClaimsSet;
 import net.shibboleth.idp.plugin.oidc.op.token.support.TokenClaimsSet;
+import net.shibboleth.shared.collection.CollectionSupport;
 import net.shibboleth.shared.component.ComponentInitializationException;
 import net.shibboleth.shared.security.DataSealerException;
 
@@ -77,6 +83,42 @@ public class UserInfoFlowAutomaticRegistrationTest extends AbstractFederationFlo
         Assert.assertNull(response.getUserInfoJWT());
     }
 
+    @Test
+    public void testSuccess_extraClaimViaTrustMark() throws URISyntaxException, NoSuchAlgorithmException,
+        DataSealerException, ComponentInitializationException, IOException {
+        final String clientId = uniqueClientId();
+        final String trustMark = TrustChainTestUtil.trustMark(JWSAlgorithm.RS256, trustMarkIssuerKey, trustMarkIssuerId,
+                clientId, "https://example.org/email-allowing-trust-mark", Instant.now().plusSeconds(300)).serialize();
+        final OIDCClientMetadata metadata = new OIDCClientMetadata();
+        metadata.setRedirectionURI(new URI(redirectUri));
+        metadata.setJWKSet(new JWKSet(rpKey.toPublicJWK()));
+        final String rpEntityConfiguration = rpEntityConfiguration(clientId, metadata, List.of(Map.of(
+                "trust_mark_type", "https://example.org/email-allowing-trust-mark",
+                "trust_mark", trustMark)));
+        final String trustAnchorConfiguration = trustedAnchorConfiguration();
+        rpConfigureMockHttpClient(clientId, rpEntityConfiguration, trustAnchorConfiguration);
+        try {
+            mapResponse(entityConfigurationUrl(trustMarkIssuerId),
+                    mockResponse(trustMarkIssuerConfiguration(trustMarkIssuerId)));
+            mapResponse(subordinateStatementUrl(anchorFetchEndpoint, trustMarkIssuerId),
+                    mockResponse(subordinateStatement(trustMarkIssuerId,
+                            Map.of("federation_entity", CollectionSupport.emptyMap()))));
+        } catch (UnsupportedOperationException | IOException e) {
+            Assert.fail("Could not initialize mock HTTP client", e);
+        }
+
+        final BearerAccessToken token = buildToken(clientId, List.of(clientId, anchorId));
+        request.addHeader("Authorization", token.toAuthorizationHeader());
+        final FlowExecutionResult result = flowExecutor.launchExecution(UserInfoTest.FLOW_ID, null, externalContext);
+        final UserInfoSuccessResponse response = parseSuccessResponse(result, UserInfoSuccessResponse.class);
+        Assert.assertEquals(response.getUserInfo().getSubject().getValue(), subject);
+        final UserInfo userInfo = response.getUserInfo();
+        Assert.assertNotNull(userInfo);
+        Assert.assertEquals(userInfo.getEmailAddress(), "jdoe at example.org");
+        Assert.assertNull(userInfo.getNickname());
+        Assert.assertNull(response.getUserInfoJWT());
+    }
+
     @Test
     public void testSuccess_resolveApi() throws Exception {
         final String clientId = uniqueClientId();
@@ -94,6 +136,50 @@ public class UserInfoFlowAutomaticRegistrationTest extends AbstractFederationFlo
         Assert.assertNull(response.getUserInfoJWT());
     }
 
+    @Test
+    public void testSuccess_resolveApi_extraClaimViaTrustMark() throws Exception {
+        final String clientId = uniqueClientId();
+        final OIDCClientMetadata metadata = new OIDCClientMetadata();
+        metadata.setRedirectionURI(URI.create(redirectUri));
+        metadata.setJWKSet(new JWKSet(rpKey.toPublicJWK()));
+        final String trustMark = TrustChainTestUtil.trustMark(JWSAlgorithm.RS256, anchorKey, anchorId,
+                clientId, "https://example.org/email-allowing-trust-mark", Instant.now().plusSeconds(300)).serialize();
+        rpResolveEntityConfigureMockHttpClient(clientId, metadata, List.of(Map.of(
+                "trust_mark_type", "https://example.org/email-allowing-trust-mark",
+                "trust_mark", trustMark)));
+        request.addHeader(USE_CUSTOM_RESOLVER_API_CONDITION, "true");
+        final BearerAccessToken token = buildToken(clientId, List.of(clientId, anchorId));
+        request.addHeader("Authorization", token.toAuthorizationHeader());
+        final FlowExecutionResult result = flowExecutor.launchExecution(UserInfoTest.FLOW_ID, null, externalContext);
+        final UserInfoSuccessResponse response = parseSuccessResponse(result, UserInfoSuccessResponse.class);
+        Assert.assertEquals(response.getUserInfo().getSubject().getValue(), subject);
+        final UserInfo userInfo = response.getUserInfo();
+        Assert.assertNotNull(userInfo);
+        Assert.assertEquals(userInfo.getEmailAddress(), "jdoe at example.org");
+    }
+
+    @Test
+    public void testSuccess_resolveApi_noExtraClaimViaAuthorityTrustMark() throws Exception {
+        final String clientId = uniqueClientId();
+        final OIDCClientMetadata metadata = new OIDCClientMetadata();
+        metadata.setRedirectionURI(URI.create(redirectUri));
+        metadata.setJWKSet(new JWKSet(rpKey.toPublicJWK()));
+        final String trustMark = TrustChainTestUtil.trustMark(JWSAlgorithm.RS256, anchorKey, anchorId,
+                anchorId, "https://example.org/email-allowing-trust-mark", Instant.now().plusSeconds(300)).serialize();
+        rpResolveEntityConfigureMockHttpClient(clientId, metadata, List.of(Map.of(
+                "trust_mark_type", "https://example.org/email-allowing-trust-mark",
+                "trust_mark", trustMark)));
+        request.addHeader(USE_CUSTOM_RESOLVER_API_CONDITION, "true");
+        final BearerAccessToken token = buildToken(clientId, List.of(clientId, anchorId));
+        request.addHeader("Authorization", token.toAuthorizationHeader());
+        final FlowExecutionResult result = flowExecutor.launchExecution(UserInfoTest.FLOW_ID, null, externalContext);
+        final UserInfoSuccessResponse response = parseSuccessResponse(result, UserInfoSuccessResponse.class);
+        Assert.assertEquals(response.getUserInfo().getSubject().getValue(), subject);
+        final UserInfo userInfo = response.getUserInfo();
+        Assert.assertNotNull(userInfo);
+        Assert.assertNull(userInfo.getEmailAddress());
+    }
+
     @Test
     public void testFails_resolveApiFails_noFallback() throws Exception {
         final String clientId = uniqueClientId();
diff --git a/idp-oidfed-op-impl/src/test/resources/net/shibboleth/idp/module/conf/attribute-filter.xml b/idp-oidfed-op-impl/src/test/resources/net/shibboleth/idp/module/conf/attribute-filter.xml
index a8e9921..606841a 100644
--- a/idp-oidfed-op-impl/src/test/resources/net/shibboleth/idp/module/conf/attribute-filter.xml
+++ b/idp-oidfed-op-impl/src/test/resources/net/shibboleth/idp/module/conf/attribute-filter.xml
@@ -13,8 +13,10 @@
         xmlns="urn:mace:shibboleth:2.0:afp"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns:oidc="urn:mace:shibboleth:2.0:afp:oidc"
+        xmlns:oidfed="urn:mace:shibboleth:2.0:afp:oidfed"
         xsi:schemaLocation="urn:mace:shibboleth:2.0:afp http://shibboleth.net/schema/idp/shibboleth-afp.xsd
-                            urn:mace:shibboleth:2.0:afp:oidc http://shibboleth.net/schema/oidc/shibboleth-afp-oidc.xsd">
+                            urn:mace:shibboleth:2.0:afp:oidc http://shibboleth.net/schema/oidc/shibboleth-afp-oidc.xsd
+                            urn:mace:shibboleth:2.0:afp:oidfed http://shibboleth.net/schema/oidfed/shibboleth-afp-oidfed.xsd">
 
 
     <!--
@@ -129,8 +131,8 @@
         </AttributeRule>
     </AttributeFilterPolicy>
 
-    <AttributeFilterPolicy id="OPENID_SCOPE_EMAIL">
-        <PolicyRequirementRule xsi:type="oidc:OIDCScope" value="email" />
+    <AttributeFilterPolicy id="OIDFED_EMAIL">
+        <PolicyRequirementRule xsi:type="oidfed:OIDFEDEntityTrustMark" value="https://example.org/email-allowing-trust-mark" />
         <AttributeRule attributeID="mail">
             <PermitValueRule xsi:type="ANY" />
         </AttributeRule>

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.


More information about the commits mailing list