[java-idp-oidc] 07/31: JOIDC-222 - Support for OpenID Federation

Henri Mikkonen henri.mikkonen at iki.fi
Tue Jun 24 08:52:43 UTC 2025


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

hjmikkon pushed a commit to branch dev/JOIDC-222
in repository java-idp-oidc.

View the commit online:
http://git.shibboleth.net/view/?p=java-idp-oidc.git;a=commit;h=40178d7c81e0e1991006635d18bcd0114834d925

commit 40178d7c81e0e1991006635d18bcd0114834d925
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Wed Jan 29 18:57:48 2025 +0200

    JOIDC-222 - Support for OpenID Federation
    
    https://shibboleth.atlassian.net/browse/JOIDC-222
    
    Initial support for automatic registration via metadata caches in the API endpoints
    - Conditional trust chain resolution was moved from authorize-flow to metadata-lookup
    - Authorization endpoint stores selected trust chain to the token claims set
      - Stored as a list of entity IDs (as Strings)
    - The value is consumed from grant (code or refresh token) or access token to perform automatic registration
      - Trust chain fetching strategy exploits the pre-selection, ignoring statements that are not in the list of entity IDs
    Token flow was refactored to do grant unwrapping before metadata lookup
    - ValidateGrant was split into UnwrapGrant and ValidateGrant actions
      - JWT refresh token deserializer claims validators were refactored to base on inbound message instead of relying party context
        - Relying party configuration is not selected at unwrapping phase yet
---
 .../oidc/op/token/support/TokenClaimsSet.java      |  46 +++++
 .../op/oauth2/profile/impl/BuildAccessToken.java   |   1 +
 .../SetAuthorizationCodeToResponseContext.java     |  20 ++
 .../DefaultTrustChainFetchingStrategy.java         |  15 +-
 .../metadata/PreSelectedTrustChainCriterion.java   |  80 ++++++++
 .../op/oidfed/profile/impl/ResolveTrustChains.java |  28 ++-
 ...aultPreSelectedTrustChainIDsLookupStrategy.java |  47 +++++
 ...DefaultSelectedTrustChainIDsLookupStrategy.java |  93 +++++++++
 .../DefaultTrustChainIDsLookupStrategy.java        |  43 +++++
 .../impl/SetRefreshTokenToResponseContext.java     |   1 +
 .../plugin/oidc/op/profile/impl/UnwrapGrant.java   | 215 +++++++++++++++++++++
 .../plugin/oidc/op/profile/impl/ValidateGrant.java | 153 ++++-----------
 .../oauth2/introspection/introspection-beans.xml   |   2 +
 .../pushed-authorization-beans.xml                 |   2 +
 .../flows/oauth2/revocation/revocation-beans.xml   |   2 +
 .../oidc/abstract-api/oidc-abstract-api-beans.xml  |  24 ++-
 .../flows/oidc/abstract/oidc-abstract-beans.xml    |   6 +
 .../idp/flows/oidc/abstract/oidc-abstract-flow.xml |  15 +-
 .../idp/flows/oidc/authorize/authorize-flow.xml    |  42 +---
 .../oidc/metadata-lookup/metadata-lookup-beans.xml |  82 ++++++++
 .../oidc/metadata-lookup/metadata-lookup-flow.xml  |  35 +++-
 .../idp/flows/oidc/token/token-beans.xml           |   8 +-
 .../shibboleth/idp/flows/oidc/token/token-flow.xml |  12 +-
 .../idp/flows/oidc/userinfo/userinfo-beans.xml     |   2 +
 ...ValidateGrantTest.java => UnwrapGrantTest.java} | 183 +-----------------
 .../oidc/op/profile/impl/ValidateGrantTest.java    | 158 ++++-----------
 26 files changed, 854 insertions(+), 461 deletions(-)

diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/token/support/TokenClaimsSet.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/token/support/TokenClaimsSet.java
index b5e5ed1d..d6fc06d1 100644
--- a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/token/support/TokenClaimsSet.java
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/token/support/TokenClaimsSet.java
@@ -143,6 +143,9 @@ public class TokenClaimsSet {
     /** Identifier for the DPoP Proof JWK thumbprint under confirmation claim. */
     @Nonnull @NotEmpty public static final String KEY_DPOP_PROOF_JWK_THUMBPRINT = "jkt";
 
+    /** Identifier for trust chain used when client was registered via OID federation. */
+    @Nonnull @NotEmpty public static final String KEY_AUTO_REGISTERED_TRUST_CHAIN = "oidfed_artc";
+
     /** Claims set for the claim. */
     @Nullable private JWTClaimsSet tokenClaimsSet;
 
@@ -244,6 +247,9 @@ public class TokenClaimsSet {
                 }
             }
         }
+        if (tokenClaimsSet.getClaims().containsKey(KEY_AUTO_REGISTERED_TRUST_CHAIN)) {
+            tokenClaimsSet.getStringArrayClaim(KEY_AUTO_REGISTERED_TRUST_CHAIN);
+        }
     }
 // Checkstyle: CyclomaticComplexity ON
 
@@ -683,6 +689,26 @@ public class TokenClaimsSet {
         return null;
     }
 
+    /**
+     * Get the trust chain used when client was automatically registered via OpenID federation.
+     * 
+     * @return the trust chain.
+     * 
+     * @since 4.3.0
+     */
+    @Nullable public List<String> getAutomaticallyRegisteredTrustChain() {
+        final JWTClaimsSet tokenClaimsSet = assertedClaimsSet();
+        try {
+            final String[] array = tokenClaimsSet.getStringArrayClaim(KEY_AUTO_REGISTERED_TRUST_CHAIN);
+            if (array != null) {
+                return CollectionSupport.listOf(array);
+            }
+        } catch (final ParseException e) {
+            log.error("Error parsing trust chain claims {}", tokenClaimsSet.getClaim(KEY_AUTO_REGISTERED_TRUST_CHAIN));
+        }
+        return null;
+    }
+
     /**
      * Abstract builder to extend builders from that are instantiating claims sets extending TokenClaimsSet.
      * 
@@ -767,6 +793,9 @@ public class TokenClaimsSet {
         /** DPoP Proof JWK thumbprint. */
         @Nullable protected String dpopProofJwkThumbprint;
 
+        /** Automatic registered trust chain */
+        @Nullable protected List<String> automaticallyRegisteredTrustChain;
+
         /** Default constructor. */
         protected Builder() {
             audience = CollectionSupport.emptyList();
@@ -830,6 +859,9 @@ public class TokenClaimsSet {
             if (reqScope != null) {
                 builder.claim(KEY_SCOPE, reqScope.toString());
             }
+            if (automaticallyRegisteredTrustChain != null) {
+                builder.claim(KEY_AUTO_REGISTERED_TRUST_CHAIN, automaticallyRegisteredTrustChain);
+            }
             customClaims.forEach((n,v) -> {
                 if (n != null) {
                     builder.claim(n, v);
@@ -1231,6 +1263,20 @@ public class TokenClaimsSet {
             return this;
         }
 
+        /**
+         * Set automatically registered trust chain.
+         * 
+         * @param chain trust chain
+         * 
+         * @return the builder
+         * 
+         * @since 4.3.0
+         */
+        public Builder<T> setAutomaticallyRegisteredTrustChain(@Nullable final List<String> chain) {
+            automaticallyRegisteredTrustChain = chain == null ? null : CollectionSupport.copyToList(chain);
+            return this;
+        }
+
         /**
          * Builds claims set.
          * 
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/BuildAccessToken.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/BuildAccessToken.java
index cf981cf3..96e08061 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/BuildAccessToken.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/BuildAccessToken.java
@@ -539,6 +539,7 @@ public class BuildAccessToken extends AbstractOIDCResponseAction {
             if (StringSupport.trimOrNull(nonNullClaimsSet.getRootTokenIdentifier()) == null) {
                 builder.setRootTokenIdentifier(nonNullClaimsSet.getID());
             }
+            builder.setAutomaticallyRegisteredTrustChain(nonNullClaimsSet.getAutomaticallyRegisteredTrustChain());
         } else {
             final OIDCAuthenticationResponseConsentContext consentCtx =
                     consentContextLookupStrategy.apply(profileRequestContext);
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/SetAuthorizationCodeToResponseContext.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/SetAuthorizationCodeToResponseContext.java
index d166c6c6..23c92c40 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/SetAuthorizationCodeToResponseContext.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/SetAuthorizationCodeToResponseContext.java
@@ -18,6 +18,7 @@ import java.net.URI;
 import java.text.ParseException;
 import java.time.Duration;
 import java.time.Instant;
+import java.util.List;
 import java.util.Map;
 import java.util.function.BiFunction;
 import java.util.function.Function;
@@ -40,6 +41,7 @@ import net.shibboleth.idp.authn.context.SubjectContext;
 import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseConsentContext;
 import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext;
 import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseTokenClaimsContext;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.navigate.DefaultSelectedTrustChainIDsLookupStrategy;
 import net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.OIDCAuthenticationResponseContextLookupFunction;
 import net.shibboleth.idp.plugin.oidc.op.token.support.AuthorizeCodeClaimsSet;
 import net.shibboleth.idp.profile.IdPEventIds;
@@ -116,6 +118,9 @@ public class SetAuthorizationCodeToResponseContext extends AbstractOAuthAuthoriz
     /** The strategy used for manipulating the token claims set. */
     @Nullable private BiFunction<ProfileRequestContext,Map<String,Object>,Map<String,Object>> manipulationStrategy;
 
+    /** The strategy used to locate IDs for the selected trust chain. */
+    @Nonnull private Function<ProfileRequestContext,List<String>> selectedTrustChainIDsLookupStrategy;
+
     /** Subject context. */
     @Nullable private SubjectContext subjectCtx;
 
@@ -151,6 +156,7 @@ public class SetAuthorizationCodeToResponseContext extends AbstractOAuthAuthoriz
         tokenClaimsSetManipulationStrategyLookupStrategy =
                 new AuthorizationCodeClaimsSetManipulationStrategyLookupFunction();
         xmlSafeIdentifier = true;
+        selectedTrustChainIDsLookupStrategy = new DefaultSelectedTrustChainIDsLookupStrategy();
     }
 
     /**
@@ -283,6 +289,19 @@ public class SetAuthorizationCodeToResponseContext extends AbstractOAuthAuthoriz
         xmlSafeIdentifier = flag;
     }
 
+    /**
+     * Set the lookup strategy used to locate IDs for the selected trust chain.
+     * 
+     * @param strategy What to set
+     */
+    public void setSelectedTrustChainIDsLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext, List<String>> strategy) {
+        checkSetterPreconditions();
+
+        selectedTrustChainIDsLookupStrategy  = Constraint.isNotNull(strategy,
+                "SelectedTrustChainIDsLookupStrategy lookup strategy cannot be null");
+    }
+
     /**
      * Set the strategy used to locate the nonce of authentication request.
      * 
@@ -427,6 +446,7 @@ public class SetAuthorizationCodeToResponseContext extends AbstractOAuthAuthoriz
                 .setConsentEnabled(consentEnabledPredicate.test(profileRequestContext))
                 .setSessionIdentifier(responseCtx.getSessionId())
                 .setDpopProofJwkThumbprint(responseCtx.getDpopProofJwkThumbprint())
+                .setAutomaticallyRegisteredTrustChain(selectedTrustChainIDsLookupStrategy.apply(profileRequestContext))
                 .build();
 
         if (manipulationStrategy != null) {
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultTrustChainFetchingStrategy.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultTrustChainFetchingStrategy.java
index f20d3497..4bfc9cb9 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultTrustChainFetchingStrategy.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultTrustChainFetchingStrategy.java
@@ -130,6 +130,9 @@ public class DefaultTrustChainFetchingStrategy extends AbstractIdentifiableIniti
         if (criteria == null) {
             return null;
         }
+        final PreSelectedTrustChainCriterion preSelectedCriterion = criteria.get(PreSelectedTrustChainCriterion.class);
+        final List<String> preSelectedChain =
+                preSelectedCriterion == null ? CollectionSupport.emptyList() : preSelectedCriterion.getValue();
         try {
             final List<EntityStatement> entityConfigurations = entityConfigurationCache.get(criteria);
             if (entityConfigurations.isEmpty()) {
@@ -138,7 +141,7 @@ public class DefaultTrustChainFetchingStrategy extends AbstractIdentifiableIniti
             final EntityStatement entityConfiguration = entityConfigurationCache.get(criteria).get(0);
             assert entityConfiguration != null;
             final List<List<EntityStatement>> rawChains = populateChain(
-                    CollectionSupport.listOf(CollectionSupport.listOf(entityConfiguration)));
+                    CollectionSupport.listOf(CollectionSupport.listOf(entityConfiguration)), preSelectedChain);
             final List<List<EntityStatement>> trustChains =
                     stripIntermediateConfigurations(entityConfiguration, rawChains);
             return Stream.concat(trustChains.stream(), resolveLocallyTrustedTrustChains(trustChains).stream()).toList();
@@ -251,10 +254,12 @@ public class DefaultTrustChainFetchingStrategy extends AbstractIdentifiableIniti
      * contain any authority hints.
      * 
      * @param entities the list of trust chains to be populated.
+     * @param preSelectedChain the pre-selected trust chain (containing entity IDs as String).
      * @return
      */
     @Nonnull @NonnullElements private List<List<EntityStatement>> populateChain(
-            @Nonnull @NonnullElements final List<List<EntityStatement>> entities) {
+            @Nonnull @NonnullElements final List<List<EntityStatement>> entities,
+            @Nonnull final List<String> preSelectedChain) {
         final List<List<EntityStatement>> result = new ArrayList<>();
         boolean hints = false;
         for (final List<EntityStatement> chain : entities) {
@@ -263,13 +268,13 @@ public class DefaultTrustChainFetchingStrategy extends AbstractIdentifiableIniti
             if (authorityHints == null || authorityHints.isEmpty()) {
                 result.add(chain);
             } else {
-                // final entity statement in the chain contains authority hints that needs to be resolved
-                hints = true;
                 final List<Pair<EntityStatement, EntityStatement>> authorities = authorityHints.stream()
                         .map(EntityID::getValue)
+                        .filter(id -> preSelectedChain.isEmpty() || preSelectedChain.contains(id))
                         .map(id -> fetchAuthority(entityStatement, id))
                         .filter(pair -> pair != null && pair.getFirst() != null && pair.getSecond() != null)
                         .toList();
+                hints = !authorities.isEmpty();
                 authorities.stream().forEach(authority -> {
                     final ArrayList<EntityStatement> newChain = new ArrayList<>(chain);
                     newChain.add(authority.getSecond());
@@ -279,7 +284,7 @@ public class DefaultTrustChainFetchingStrategy extends AbstractIdentifiableIniti
             }
         }
         if (hints) {
-            return populateChain(result);
+            return populateChain(result, preSelectedChain);
         }
         return result;
     }
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/PreSelectedTrustChainCriterion.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/PreSelectedTrustChainCriterion.java
new file mode 100644
index 00000000..95cbb8d1
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/PreSelectedTrustChainCriterion.java
@@ -0,0 +1,80 @@
+/*
+ * 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;
+
+import java.util.List;
+import java.util.Objects;
+
+import javax.annotation.Nonnull;
+
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.resolver.Criterion;
+
+/**
+ * A {@link Criterion} representing entity IDs for a pre-selected trust chain.
+ */
+public class PreSelectedTrustChainCriterion implements Criterion {
+
+    /** The entity ID values. */
+    @Nonnull @NotEmpty private final List<String> value;
+
+    /**
+     * Constructor.
+     *
+     * @param entityIds the entity ID values, must not be null
+     */
+    public PreSelectedTrustChainCriterion(@Nonnull final List<String> entityIds) {
+        value = Constraint.isNotNull(entityIds, "Entity ID cannot be null");
+    }
+
+    /**
+     * Get the entity ID values.
+     * 
+     * @return the entity ID values
+     */
+    @Nonnull public List<String> getValue() {
+        return value;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String toString() {
+        return "PreSelectedTrustChainCriterion [value=" + value + "]";
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public int hashCode() {
+        return Objects.hash(value);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean equals(final Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final PreSelectedTrustChainCriterion other = (PreSelectedTrustChainCriterion) obj;
+        return value.containsAll(other.value) && other.value.containsAll(value);
+    }
+
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ResolveTrustChains.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ResolveTrustChains.java
index 2f5d5d34..a0463397 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ResolveTrustChains.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ResolveTrustChains.java
@@ -39,7 +39,9 @@ import com.nimbusds.openid.connect.sdk.rp.OIDCClientMetadata;
 
 import net.minidev.json.JSONObject;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultClientMetadataFromTrustChainLookupStrategy;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.PreSelectedTrustChainCriterion;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.SubjectEntityIDCriterion;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.navigate.DefaultPreSelectedTrustChainIDsLookupStrategy;
 import net.shibboleth.idp.profile.AbstractProfileAction;
 import net.shibboleth.oidc.metadata.cache.MetadataCache;
 import net.shibboleth.oidc.metadata.cache.MetadataCacheException;
@@ -89,6 +91,9 @@ public class ResolveTrustChains extends AbstractProfileAction {
     /** Enforcer function for applying metadata policy for an item. */
     @NonnullAfterInit private BiFunction<Object, MetadataPolicy, Pair<Object, Boolean>> metadataPolicyEnforcer;
 
+    /** Strategy used to fetch the pre-selected trust chain entity IDs. */
+    @Nonnull private Function<ProfileRequestContext, List<String>> preSelectedTrustChainIdsLookupStrategy;
+
     /** OAuth2 client id. */
     @NonnullBeforeExec private String clientId;
 
@@ -102,6 +107,7 @@ public class ResolveTrustChains extends AbstractProfileAction {
         assert tccs != null;
         trustChainContextCreationStrategy = tccs;
         metadataLookupStrategy = new DefaultClientMetadataFromTrustChainLookupStrategy();
+        preSelectedTrustChainIdsLookupStrategy = new DefaultPreSelectedTrustChainIDsLookupStrategy();
     }
 
     /**
@@ -159,6 +165,18 @@ public class ResolveTrustChains extends AbstractProfileAction {
         metadataPolicyEnforcer = Constraint.isNotNull(enforcer, "Metadata policy enforcer cannot be null");
     }
 
+    /**
+     * Set the strategy used to fetch the pre-selected trust chain entity IDs.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setPreSelectedTrustChainIdsLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext, List<String>> strategy) {
+        checkSetterPreconditions();
+        preSelectedTrustChainIdsLookupStrategy = Constraint.isNotNull(strategy,
+                "PreSelectedTrustChainIdsLookupStrategy cannot be null");
+    }
+
     /** {@inheritDoc} */
     @Override
     protected void doInitialize() throws ComponentInitializationException {
@@ -202,10 +220,16 @@ public class ResolveTrustChains extends AbstractProfileAction {
     @Override
     protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
         log.debug("{} Resolving trust chain for {}", getLogPrefix(), clientId);
+        assert clientId != null;
+        final CriteriaSet criteriaSet = new CriteriaSet(new SubjectEntityIDCriterion(clientId));
+        final List<String> preSelectedChain = preSelectedTrustChainIdsLookupStrategy.apply(profileRequestContext);
+        if (preSelectedChain != null && !preSelectedChain.isEmpty()) {
+            log.debug("{} Trust chain has been pre-selected: {}", getLogPrefix(), preSelectedChain);
+            criteriaSet.add(new PreSelectedTrustChainCriterion(preSelectedChain));
+        }
         final List<List<List<EntityStatement>>> cacheResult;
         try {
-            assert clientId != null;
-            cacheResult = trustChainCache.get(new CriteriaSet(new SubjectEntityIDCriterion(clientId)));
+            cacheResult = trustChainCache.get(criteriaSet);
         } catch (final MetadataCacheException e) {
             log.warn("{} Could fetch trust chains for {}", getLogPrefix(), clientId);
             return;
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/DefaultPreSelectedTrustChainIDsLookupStrategy.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/DefaultPreSelectedTrustChainIDsLookupStrategy.java
new file mode 100644
index 00000000..bce8d860
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/DefaultPreSelectedTrustChainIDsLookupStrategy.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.oidfed.profile.navigate;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.function.Function;
+
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext;
+
+/**
+ * Default strategy for looking up the entity IDs of the previously selected trust chain. The selected trust chain is
+ * fetched via {@link OIDCAuthenticationResponseContext#getAuthorizationGrantClaimsSet()}.
+ */
+public class DefaultPreSelectedTrustChainIDsLookupStrategy implements Function<ProfileRequestContext,List<String>> {
+
+    /** {@inheritDoc} */
+    @Override @Nullable
+    public List<String> apply(@Nullable final ProfileRequestContext input) {
+        return Optional.ofNullable(input)
+                .filter(prc -> prc.getOutboundMessageContext() != null)
+                .map(prc -> prc.ensureOutboundMessageContext())
+                .filter(msgCtx -> msgCtx.containsSubcontext(OIDCAuthenticationResponseContext.class))
+                .map(msgCtx -> msgCtx.ensureSubcontext(OIDCAuthenticationResponseContext.class))
+                .map(oidcResponseCtx -> oidcResponseCtx.getAuthorizationGrantClaimsSet())
+                .filter(Objects::nonNull)
+                .map(claimsSet -> claimsSet.getAutomaticallyRegisteredTrustChain())
+                .orElse(null);
+    }
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/DefaultSelectedTrustChainIDsLookupStrategy.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/DefaultSelectedTrustChainIDsLookupStrategy.java
new file mode 100644
index 00000000..75b87dd8
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/DefaultSelectedTrustChainIDsLookupStrategy.java
@@ -0,0 +1,93 @@
+/*
+ * 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.profile.navigate;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
+import org.slf4j.Logger;
+
+import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
+
+import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.RelyingPartyTrustChainContext;
+import net.shibboleth.shared.annotation.ParameterName;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * Default strategy for looking up the entity IDs of the selected trust chain. The selected trust chain is fetched via
+ * {@link RelyingPartyTrustChainContext#getSelectedTrustChain()}.
+ */
+public class DefaultSelectedTrustChainIDsLookupStrategy implements Function<ProfileRequestContext,List<String>> {
+
+    /** Class logger. */
+    @Nonnull private Logger log = LoggerFactory.getLogger(DefaultSelectedTrustChainIDsLookupStrategy.class);
+
+    /** Strategy used to locate the trust chain context. */
+    @Nonnull private Function<ProfileRequestContext, RelyingPartyTrustChainContext> trustChainContextLookupStrategy;
+
+    /** Strategy used to get entity IDs from a trust chain. */
+    @Nonnull private Function<List<EntityStatement>, List<String>> trustChainIDsLookupStrategy;
+
+    /**
+     * Constructor.
+     */
+    public DefaultSelectedTrustChainIDsLookupStrategy() {
+        final Function<ProfileRequestContext, RelyingPartyTrustChainContext> tcls =
+                new ChildContextLookup<>(RelyingPartyTrustChainContext.class, true).compose(
+                        new InboundMessageContextLookup());
+        assert tcls != null;
+        trustChainContextLookupStrategy = tcls;
+        trustChainIDsLookupStrategy = new DefaultTrustChainIDsLookupStrategy();
+    }
+
+    /**
+     * Constructor.
+     *
+     * @param trustChainContextStrategy strategy used to locate the trust chain context
+     * @param trustChainIDsStrategy strategy used to get entity IDs from a trust chain
+     */
+    public DefaultSelectedTrustChainIDsLookupStrategy(
+            @Nonnull @ParameterName(name = "trustChainContextLookupStrategy")
+            final Function<ProfileRequestContext, RelyingPartyTrustChainContext> trustChainContextStrategy,
+            @Nonnull @ParameterName(name = "trustChainIDsLookupStrategy")
+            final Function<List<EntityStatement>, List<String>> trustChainIDsStrategy) {
+        trustChainContextLookupStrategy =
+                Constraint.isNotNull(trustChainContextStrategy, "TrustChainContextLookupStrategy cannot be null");
+        trustChainIDsLookupStrategy =
+                Constraint.isNotNull(trustChainIDsStrategy, "TrustChainIDsLookupStrategy cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    @Override @Nullable
+    public List<String> apply(@Nullable final ProfileRequestContext input) {
+        final RelyingPartyTrustChainContext trustChainContext = trustChainContextLookupStrategy.apply(input);
+        if (trustChainContext == null || trustChainContext.getSelectedTrustChain() == null) {
+            log.debug("No selected trust chain located");
+            return null;
+        }
+        return Optional.ofNullable(trustChainContext.getSelectedTrustChain())
+                .map(pair -> pair.getFirst())
+                .map(chain -> trustChainIDsLookupStrategy.apply(chain))
+                .orElse(null);
+    }
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/DefaultTrustChainIDsLookupStrategy.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/DefaultTrustChainIDsLookupStrategy.java
new file mode 100644
index 00000000..7af8e2b7
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/DefaultTrustChainIDsLookupStrategy.java
@@ -0,0 +1,43 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.oidfed.profile.navigate;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.stream.IntStream;
+
+import javax.annotation.Nullable;
+
+import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
+
+/**
+ * Default strategy for looking up the entity IDs of a trust chain.
+ */
+public class DefaultTrustChainIDsLookupStrategy implements Function<List<EntityStatement>,List<String>> {
+
+    /** {@inheritDoc} */
+    @Override @Nullable
+    public List<String> apply(@Nullable final List<EntityStatement> input) {
+        return Optional.ofNullable(input)
+                .filter(chain -> chain != null && !chain.isEmpty())
+                .map(chain -> IntStream.range(0, chain.size())
+                        .mapToObj(i -> chain.get(i))
+                        .map(statement -> statement.getEntityID().getValue())
+                        .distinct()
+                        .toList())
+                .orElse(null);
+    }
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetRefreshTokenToResponseContext.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetRefreshTokenToResponseContext.java
index 1e4729d6..f2deb92e 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetRefreshTokenToResponseContext.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetRefreshTokenToResponseContext.java
@@ -346,6 +346,7 @@ public class SetRefreshTokenToResponseContext extends AbstractOIDCResponseAction
         builder.setJWTID(idGenerator, xmlSafeIdentifier);
         builder.setRootTokenIdentifier(rootTokenId);
         builder.setDpopProofJwkThumbprint(oidcResponseContext.getDpopProofJwkThumbprint());
+        builder.setAutomaticallyRegisteredTrustChain(nonNullClaimsSet.getAutomaticallyRegisteredTrustChain());
         final RefreshTokenClaimsSet claimsSet = builder.build();
         
         if (manipulationStrategy != null) {
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/UnwrapGrant.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/UnwrapGrant.java
new file mode 100644
index 00000000..ffe1e0e2
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/UnwrapGrant.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.idp.plugin.oidc.op.profile.impl;
+
+import java.text.ParseException;
+import java.time.Instant;
+import java.util.List;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import com.nimbusds.oauth2.sdk.AuthorizationCodeGrant;
+import com.nimbusds.oauth2.sdk.AuthorizationGrant;
+import com.nimbusds.oauth2.sdk.GrantType;
+import com.nimbusds.oauth2.sdk.RefreshTokenGrant;
+import com.nimbusds.oauth2.sdk.id.ClientID;
+
+import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext;
+import net.shibboleth.idp.plugin.oidc.op.messaging.context.navigate.TokenRequestClientIDLookupFunction;
+import net.shibboleth.idp.plugin.oidc.op.token.support.AuthorizeCodeClaimsSet;
+import net.shibboleth.idp.plugin.oidc.op.token.support.RefreshTokenClaimsSet;
+import net.shibboleth.idp.plugin.oidc.op.token.support.TokenClaimsSet;
+import net.shibboleth.oidc.profile.core.OidcEventIds;
+import net.shibboleth.shared.annotation.ParameterName;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.security.DataSealer;
+import net.shibboleth.shared.security.DataSealerException;
+
+/**
+ * Action that unwraps an authorization grant or refresh token grant.
+ * 
+ * <p>Operation is valid if it is successfully unwrapped, parsed as a code or refresh token, is unexpired and was issued
+ * to the expected client.</p>
+ * 
+ * <p> The claims set from the grant is stored to response context via
+ * {@link OIDCAuthenticationResponseContext#setAuthorizationGrantClaimsSet(TokenClaimsSet)}.</p>
+ * 
+ * <p>Note that the addition of support for the "client_credentials" grant type means that there may not in fact be a
+ * grant, or resulting claims set.</p>
+ */
+public class UnwrapGrant extends AbstractOIDCTokenResponseAction {
+
+    /** Class logger. */
+    @Nonnull private Logger log = LoggerFactory.getLogger(UnwrapGrant.class);
+
+    /** Data sealer for unwrapping authorization code. */
+    @Nonnull private final DataSealer dataSealer;
+
+    /** Strategy used to obtain the client id value from token request. */
+    @Nonnull private Function<MessageContext, ClientID> clientIDLookupStrategy;
+
+    /** List of deserializer bi-functions for refresh tokens to be used in addition to unsealing opaque value. */
+    @Nonnull private List<BiFunction<ProfileRequestContext,String,RefreshTokenClaimsSet>> refreshTokenDeserializers;
+
+    /**
+     * Constructor.
+     * 
+     * @param sealer sealer to decrypt/hmac authorize code.
+     */
+    public UnwrapGrant(@Nonnull @ParameterName(name = "sealer") final DataSealer sealer) {
+        dataSealer = Constraint.isNotNull(sealer, "DataSealer cannot be null");
+        refreshTokenDeserializers = CollectionSupport.emptyList();
+        clientIDLookupStrategy = new TokenRequestClientIDLookupFunction();
+    }
+
+    /**
+     * Set the list of deserializer bi-functions for refresh tokens to be used in addition to unsealing opaque value.
+     * 
+     * @param deserializers list of deserializers
+     */
+    public void setRefreshTokenDeserializers(
+            @Nonnull final List<BiFunction<ProfileRequestContext,String,RefreshTokenClaimsSet>> deserializers) {
+        checkSetterPreconditions();
+        refreshTokenDeserializers =
+                Constraint.isNotNull(deserializers, "List of refresh token deserializers cannot be null");
+    }
+
+    /**
+     * Set the strategy used to locate the client id of the request.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setClientIDLookupStrategy(@Nonnull final Function<MessageContext, ClientID> strategy) {
+        ifInitializedThrowUnmodifiabledComponentException();
+        clientIDLookupStrategy =
+                Constraint.isNotNull(strategy, "ClientIDLookupStrategy lookup strategy cannot be null");
+    }
+
+// Checkstyle: CyclomaticComplexity|MethodLength|ReturnCount OFF
+    /** {@inheritDoc} */
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        final AuthorizationGrant grant = getTokenRequest().getAuthorizationGrant();
+        
+        log.debug("{} Unwrapping grant type: {}", getLogPrefix(),grant.getType());
+
+        TokenClaimsSet tokenClaimsSet = null;
+        if (GrantType.AUTHORIZATION_CODE.equals(grant.getType())) {
+            final AuthorizationCodeGrant codeGrant = (AuthorizationCodeGrant) grant;
+            if (codeGrant.getAuthorizationCode() != null && codeGrant.getAuthorizationCode().getValue() != null) {
+                try {
+                    final String codeValue = codeGrant.getAuthorizationCode().getValue();
+                    assert codeValue != null;
+                    final AuthorizeCodeClaimsSet authzCodeClaimsSet =
+                            AuthorizeCodeClaimsSet.parse(codeValue, dataSealer);
+                    assert authzCodeClaimsSet != null;
+                    final String jti = authzCodeClaimsSet.getID();
+                    if (jti == null) {
+                        log.warn("{} Invalid contents in the authz code grant: no JTI", getLogPrefix());
+                        ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_GRANT);
+                        return;
+                    }
+                    log.debug("{} Authz code unwrapped {}", getLogPrefix(), authzCodeClaimsSet.serialize());
+                    tokenClaimsSet = authzCodeClaimsSet;
+                } catch (final DataSealerException | ParseException e) {
+                    log.warn("{} Unwrapping authz code failed: {}", getLogPrefix(), e.getMessage());
+                    ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_GRANT);
+                    return;
+                }
+            }
+        } else if (GrantType.REFRESH_TOKEN.equals(grant.getType())) {
+            final RefreshTokenGrant refreshTokentokenGrant = (RefreshTokenGrant) grant;
+            if (refreshTokentokenGrant.getRefreshToken() != null
+                    && refreshTokentokenGrant.getRefreshToken().getValue() != null) {
+                final String tokenValue = refreshTokentokenGrant.getRefreshToken().getValue();
+                assert tokenValue != null;
+                final RefreshTokenClaimsSet refreshTokenClaimsSet = deserializeRefreshToken(profileRequestContext,
+                        tokenValue);
+                if (refreshTokenClaimsSet == null) {
+                    log.warn("{} Unwrapping refresh token failed", getLogPrefix());
+                    ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_GRANT);
+                    return;
+                }
+                final Instant chainExp = refreshTokenClaimsSet.getChainExp();
+                if (chainExp != null && chainExp.isBefore(Instant.now())) {
+                    log.warn("{} Refresh token chain has expired on {}", getLogPrefix(),
+                            refreshTokenClaimsSet.getChainExp());
+                    ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_GRANT);
+                    return;
+                }
+                tokenClaimsSet = refreshTokenClaimsSet;
+            }
+        } else if (GrantType.CLIENT_CREDENTIALS.equals(grant.getType())) {
+            return;
+        }
+        
+        if (tokenClaimsSet == null) {
+            log.warn("{} Grant type not supported", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_GRANT);
+            return;
+        }
+        if (!tokenClaimsSet.isTimeValid()) {
+            log.warn("{} Token is expired or not net valid", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_GRANT);
+            return;
+        }
+        final ClientID clientId = tokenClaimsSet.getClientID();
+        assert clientId != null;
+        final ClientID requestClientId = clientIDLookupStrategy.apply(profileRequestContext.ensureInboundMessageContext());
+        if (!clientId.equals(requestClientId)) {
+            log.warn("{} Token issued to client {}, invalid for {}", getLogPrefix(),
+                    clientId.getValue(), requestClientId);
+            ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_GRANT);
+            return;
+        }
+        final OIDCAuthenticationResponseContext oidcResponseContext = getOidcResponseContext();
+        assert oidcResponseContext != null;
+        oidcResponseContext.setAuthorizationGrantClaimsSet(tokenClaimsSet);
+    }
+// Checkstyle: CyclomaticComplexity|MethodLength|ReturnCount ON
+
+    /**
+     * Attempt to deseriaalize a (serialized) refresh token value via configured deserializers.
+     * 
+     * @param profileRequestContext The profile request context given to the deserializers
+     * @param refreshToken The serialized refresh token value
+     * @return refresh token claims set, or null if it couldn't be parsed
+     */
+    protected RefreshTokenClaimsSet deserializeRefreshToken(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final String refreshToken) {
+        try {
+            return RefreshTokenClaimsSet.parse(refreshToken, dataSealer);
+        } catch (ParseException | DataSealerException e) {
+        }
+        for (final BiFunction<ProfileRequestContext, String, RefreshTokenClaimsSet> deserializer : 
+            refreshTokenDeserializers) {
+            final RefreshTokenClaimsSet deserializedSet = deserializer.apply(profileRequestContext, refreshToken);
+            if (deserializedSet != null) {
+                return deserializedSet;
+            }
+        }
+        return null;
+    }
+
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateGrant.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateGrant.java
index c884911d..d77ad9ba 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateGrant.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateGrant.java
@@ -14,7 +14,6 @@
 
 package net.shibboleth.idp.plugin.oidc.op.profile.impl;
 
-import java.text.ParseException;
 import java.time.Duration;
 import java.time.Instant;
 import java.util.List;
@@ -37,7 +36,6 @@ import com.nimbusds.jwt.JWTClaimsSet;
 import com.nimbusds.oauth2.sdk.AuthorizationCodeGrant;
 import com.nimbusds.oauth2.sdk.AuthorizationGrant;
 import com.nimbusds.oauth2.sdk.GrantType;
-import com.nimbusds.oauth2.sdk.RefreshTokenGrant;
 import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod;
 import com.nimbusds.oauth2.sdk.id.ClientID;
 import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformation;
@@ -56,15 +54,11 @@ import net.shibboleth.oidc.profile.config.navigate.RefreshTokenChainLifetimeLook
 import net.shibboleth.oidc.profile.config.navigate.RevocationLifetimeLookupFunction;
 import net.shibboleth.oidc.profile.core.OidcEventIds;
 import net.shibboleth.profile.context.RelyingPartyContext;
-import net.shibboleth.shared.annotation.ParameterName;
 import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
-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.StringSupport;
-import net.shibboleth.shared.security.DataSealer;
-import net.shibboleth.shared.security.DataSealerException;
 
 /**
  * Action that validates an authorization grant.
@@ -84,9 +78,6 @@ public class ValidateGrant extends AbstractOIDCTokenResponseAction {
     /** Class logger. */
     @Nonnull private Logger log = LoggerFactory.getLogger(ValidateGrant.class);
 
-    /** Data sealer for unwrapping authorization code. */
-    @Nonnull private final DataSealer dataSealer;
-
     /** Message replay cache instance to use. */
     @NonnullAfterInit private ReplayCache replayCache;
 
@@ -107,9 +98,6 @@ public class ValidateGrant extends AbstractOIDCTokenResponseAction {
     /** Strategy used to obtain the refresh token lifetime. */
     @Nonnull private Function<ProfileRequestContext,Duration> refreshTokenChainLifetimeLookupStrategy;
 
-    /** List of deserializer bi-functions for refresh tokens to be used in addition to unsealing opaque value. */
-    @Nonnull private List<BiFunction<ProfileRequestContext,String,RefreshTokenClaimsSet>> refreshTokenDeserializers;
-
     /** Strategy used to locate thumbprint of validated DPoP Proof JWT. */
     @Nonnull private Function<ProfileRequestContext, String> dpopProofThumbprintLookupStrategy;
 
@@ -124,17 +112,13 @@ public class ValidateGrant extends AbstractOIDCTokenResponseAction {
 
     /**
      * Constructor.
-     * 
-     * @param sealer sealer to decrypt/hmac authorize code.
      */
-    public ValidateGrant(@Nonnull @ParameterName(name = "sealer") final DataSealer sealer) {
-        dataSealer = Constraint.isNotNull(sealer, "DataSealer cannot be null");
+    public ValidateGrant() {
         relyingPartyContextLookupStrategy = new ChildContextLookup<>(RelyingPartyContext.class);
         refreshTokensEnabledPredicate = new RefreshTokensEnabledPredicate();
         chainRevocationLifetimeLookupStrategy = new DefaultChainRevocationLifetimeLookupStrategy();
         ((RevocationLifetimeLookupFunction) chainRevocationLifetimeLookupStrategy).setUseActiveProfileOnly(false);
         refreshTokenChainLifetimeLookupStrategy = new RefreshTokenChainLifetimeLookupFunction();
-        refreshTokenDeserializers = CollectionSupport.emptyList();
         dpopProofThumbprintLookupStrategy = new DefaultDPoPProofThumbprintLookupFunction();
     }
 
@@ -207,18 +191,6 @@ public class ValidateGrant extends AbstractOIDCTokenResponseAction {
                 Constraint.isNotNull(strategy, "Refresh token chain lifetime lookup strategy cannot be null");
     }
 
-    /**
-     * Set the list of deserializer bi-functions for refresh tokens to be used in addition to unsealing opaque value.
-     * 
-     * @param deserializers list of deserializers
-     */
-    public void setRefreshTokenDeserializers(
-            @Nonnull final List<BiFunction<ProfileRequestContext,String,RefreshTokenClaimsSet>> deserializers) {
-        checkSetterPreconditions();
-        refreshTokenDeserializers =
-                Constraint.isNotNull(deserializers, "List of refresh token deserializers cannot be null");
-    }
-
     /**
      * Set the strategy used to locate the thumbprint of validated DPoP Proof JWT.
      * 
@@ -286,51 +258,45 @@ public class ValidateGrant extends AbstractOIDCTokenResponseAction {
         
         log.debug("{} Validating grant type: {}", getLogPrefix(),grant.getType());
 
-        TokenClaimsSet tokenClaimsSet = null;
+        final OIDCAuthenticationResponseContext oidcResponseContext = getOidcResponseContext();
+        assert oidcResponseContext != null;
+        final TokenClaimsSet tokenClaimsSet = oidcResponseContext.getAuthorizationGrantClaimsSet();
         if (GrantType.AUTHORIZATION_CODE.equals(grant.getType())) {
-            final AuthorizationCodeGrant codeGrant = (AuthorizationCodeGrant) grant;
-            if (codeGrant.getAuthorizationCode() != null && codeGrant.getAuthorizationCode().getValue() != null) {
-                try {
-                    final String codeValue = codeGrant.getAuthorizationCode().getValue();
-                    assert codeValue != null;
-                    final AuthorizeCodeClaimsSet authzCodeClaimsSet =
-                            AuthorizeCodeClaimsSet.parse(codeValue, dataSealer);
-                    assert authzCodeClaimsSet != null;
-                    final String jti = authzCodeClaimsSet.getID();
-                    if (jti == null) {
-                        log.warn("{} Invalid contents in the authz code grant: no JTI", getLogPrefix());
-                        ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_GRANT);
-                        return;
-                    }
-                    log.debug("{} Authz code unwrapped {}", getLogPrefix(), authzCodeClaimsSet.serialize());
-                    final String cacheContext = getClass().getName();
-                    assert cacheContext != null;
-                    if (!replayCache.check(cacheContext, jti, authzCodeClaimsSet.getExp())) {
-                        log.error("{} Replay detected of authz code {}", getLogPrefix(), jti);
-                        if (!revokeChain(jti,
-                                chainRevocationLifetimeLookupStrategy.apply(profileRequestContext))) {
-                            log.warn("{} Fatal error, unable to save replayed code to revocation cache",
-                                    getLogPrefix());
-                        }
-                        ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_GRANT);
-                        return;
-                    }
-                    final String dpopJkt = authzCodeClaimsSet.getDpopProofJwkThumbprint();
-                    if (dpopJkt != null) {
-                        final String proofThumbprint = dpopProofThumbprintLookupStrategy.apply(profileRequestContext);
-                        if (!dpopJkt.equals(proofThumbprint)) {
-                            log.warn("{} The DPoP jkt in claims set '{}' did not match with the DPoP proof JWT '{}'",
-                                    getLogPrefix(), dpopJkt, proofThumbprint);
-                            ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_DPOP_PROOF);
-                            return;
-                        }
+            if (tokenClaimsSet instanceof AuthorizeCodeClaimsSet authzCodeClaimsSet) {
+                final String jti = authzCodeClaimsSet.getID();
+                if (jti == null) {
+                    log.warn("{} Invalid contents in the authz code grant: no JTI", getLogPrefix());
+                    ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_GRANT);
+                    return;
+                }
+                log.debug("{} Authz code unwrapped {}", getLogPrefix(), authzCodeClaimsSet.serialize());
+                final String cacheContext = getClass().getName();
+                assert cacheContext != null;
+                if (!replayCache.check(cacheContext, jti, authzCodeClaimsSet.getExp())) {
+                    log.error("{} Replay detected of authz code {}", getLogPrefix(), jti);
+                    if (!revokeChain(jti,
+                            chainRevocationLifetimeLookupStrategy.apply(profileRequestContext))) {
+                        log.warn("{} Fatal error, unable to save replayed code to revocation cache",
+                                getLogPrefix());
                     }
-                    tokenClaimsSet = authzCodeClaimsSet;
-                } catch (final DataSealerException | ParseException e) {
-                    log.warn("{} Unwrapping authz code failed: {}", getLogPrefix(), e.getMessage());
                     ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_GRANT);
                     return;
                 }
+                final String dpopJkt = authzCodeClaimsSet.getDpopProofJwkThumbprint();
+                if (dpopJkt != null) {
+                    final String proofThumbprint = dpopProofThumbprintLookupStrategy.apply(profileRequestContext);
+                    if (!dpopJkt.equals(proofThumbprint)) {
+                        log.warn("{} The DPoP jkt in claims set '{}' did not match with the DPoP proof JWT '{}'",
+                                getLogPrefix(), dpopJkt, proofThumbprint);
+                        ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_DPOP_PROOF);
+                        return;
+                    }
+                }
+            } else {
+                log.error("{} Unexpected instance of token claims set for authorization code: {}",
+                        getLogPrefix(), tokenClaimsSet);
+                ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_GRANT);
+                return;
             }
         } else if (GrantType.REFRESH_TOKEN.equals(grant.getType())) {
             if (!refreshTokensEnabledPredicate.test(profileRequestContext)) {
@@ -338,18 +304,7 @@ public class ValidateGrant extends AbstractOIDCTokenResponseAction {
                 ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_GRANT);
                 return;
             }
-            final RefreshTokenGrant refreshTokentokenGrant = (RefreshTokenGrant) grant;
-            if (refreshTokentokenGrant.getRefreshToken() != null
-                    && refreshTokentokenGrant.getRefreshToken().getValue() != null) {
-                final String tokenValue = refreshTokentokenGrant.getRefreshToken().getValue();
-                assert tokenValue != null;
-                final RefreshTokenClaimsSet refreshTokenClaimsSet = deserializeRefreshToken(profileRequestContext,
-                        tokenValue);
-                if (refreshTokenClaimsSet == null) {
-                    log.warn("{} Unwrapping refresh token failed", getLogPrefix());
-                    ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_GRANT);
-                    return;
-                }
+            if (tokenClaimsSet instanceof RefreshTokenClaimsSet refreshTokenClaimsSet) {
                 final String rootJti = refreshTokenClaimsSet.getRootTokenIdentifier();
                 final String rootJtiToUse;
                 if (StringSupport.trimOrNull(rootJti) == null) {
@@ -388,7 +343,6 @@ public class ValidateGrant extends AbstractOIDCTokenResponseAction {
                     ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_GRANT);
                     return;
                 }
-                tokenClaimsSet = refreshTokenClaimsSet;
                 final Instant authnTime = tokenClaimsSet.getAuthenticationTime();
                 assert authnTime != null;
                 if (Instant.now().isAfter(authnTime.plus(refreshTokenChainLifetime))) {
@@ -397,13 +351,16 @@ public class ValidateGrant extends AbstractOIDCTokenResponseAction {
                     ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_GRANT);
                     return;
                 }
+            } else {
+                log.error("{} Unexpected instance of token claims set for refresh token: {}",
+                        getLogPrefix(), tokenClaimsSet);
+                ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_GRANT);
+                return;
             }
 
         } else if (GrantType.CLIENT_CREDENTIALS.equals(grant.getType())) {
             final String proofThumbprint = dpopProofThumbprintLookupStrategy.apply(profileRequestContext);
             if (proofThumbprint != null) {
-                final OIDCAuthenticationResponseContext oidcResponseContext = getOidcResponseContext();
-                assert oidcResponseContext != null;
                 oidcResponseContext.setDpopProofJwkThumbprint(proofThumbprint);
             }
             return;
@@ -435,9 +392,6 @@ public class ValidateGrant extends AbstractOIDCTokenResponseAction {
             ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_GRANT);
             return;
         }
-        final OIDCAuthenticationResponseContext oidcResponseContext = getOidcResponseContext();
-        assert oidcResponseContext != null;
-        oidcResponseContext.setAuthorizationGrantClaimsSet(tokenClaimsSet);
         final String claimsSetThumbprint = GrantType.REFRESH_TOKEN.equals(grant.getType()) && !isPublicClient() ?
                 null : tokenClaimsSet.getDpopProofJwkThumbprint();
         final String proofThumbprint = dpopProofThumbprintLookupStrategy.apply(profileRequestContext);
@@ -481,29 +435,6 @@ public class ValidateGrant extends AbstractOIDCTokenResponseAction {
         return false;
     }
 
-    /**
-     * Attempt to deseriaalize a (serialized) refresh token value via configured deserializers.
-     * 
-     * @param profileRequestContext The profile request context given to the deserializers
-     * @param refreshToken The serialized refresh token value
-     * @return refresh token claims set, or null if it couldn't be parsed
-     */
-    protected RefreshTokenClaimsSet deserializeRefreshToken(@Nonnull final ProfileRequestContext profileRequestContext,
-            @Nonnull final String refreshToken) {
-        try {
-            return RefreshTokenClaimsSet.parse(refreshToken, dataSealer);
-        } catch (ParseException | DataSealerException e) {
-        }
-        for (final BiFunction<ProfileRequestContext, String, RefreshTokenClaimsSet> deserializer : 
-            refreshTokenDeserializers) {
-            final RefreshTokenClaimsSet deserializedSet = deserializer.apply(profileRequestContext, refreshToken);
-            if (deserializedSet != null) {
-                return deserializedSet;
-            }
-        }
-        return null;
-    }
-
     /**
      * Revokes the token chain with the given id, optionally with a given lifetime. If the given lifetime is null,
      * the default lifetime set to the {@link RevocationCache} is used.
@@ -521,4 +452,4 @@ public class ValidateGrant extends AbstractOIDCTokenResponseAction {
         return revocationCache.revoke(RevocationCacheContexts.AUTHORIZATION_CODE, id, lifetime);
     }
     
-}
\ No newline at end of file
+}
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/introspection/introspection-beans.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/introspection/introspection-beans.xml
index a6e3d194..e32a39e2 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/introspection/introspection-beans.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/introspection/introspection-beans.xml
@@ -61,4 +61,6 @@
         </property>
     </bean>
 
+    <alias alias="AutomaticRegistrationCondition" name="%{idp.oidfed.introspection.automaticRegistrationCondition:DefaultApiAutomaticRegistrationCondition}" />
+
 </beans>
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/pushed-authorization/pushed-authorization-beans.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/pushed-authorization/pushed-authorization-beans.xml
index 91721d52..0f69d394 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/pushed-authorization/pushed-authorization-beans.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/pushed-authorization/pushed-authorization-beans.xml
@@ -378,4 +378,6 @@
         </property>
     </bean>
 
+    <alias alias="AutomaticRegistrationCondition" name="%{idp.oidfed.par.automaticRegistrationCondition:shibboleth.Conditions.FALSE}" />
+
 </beans>
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/revocation/revocation-beans.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/revocation/revocation-beans.xml
index 4655bc34..057ca92e 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/revocation/revocation-beans.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/revocation/revocation-beans.xml
@@ -73,4 +73,6 @@
         </property>
     </bean>
 
+    <alias alias="AutomaticRegistrationCondition" name="%{idp.oidfed.revocation.automaticRegistrationCondition:DefaultApiAutomaticRegistrationCondition}" />
+
 </beans>
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/abstract-api/oidc-abstract-api-beans.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/abstract-api/oidc-abstract-api-beans.xml
index 1aa04370..83a13584 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/abstract-api/oidc-abstract-api-beans.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/abstract-api/oidc-abstract-api-beans.xml
@@ -42,17 +42,24 @@
                         <bean class="net.shibboleth.oidc.security.jwt.claims.impl.ExactMatchClaimsValidator"
                             p:claimName="iss">
                             <property name="valueToMatchLookupStrategy">
-                                <bean class="net.shibboleth.shared.logic.BiFunctionSupport"
-                                    factory-method="forFunctionOfFirstArg"
-                                    c:_0-ref="shibboleth.ResponderIdLookup.Simple" />
+                                <bean parent="shibboleth.BiFunctions.Constant" c:target-ref="shibboleth.oidc.issuer"/>
                             </property>
                         </bean>
                         <bean class="net.shibboleth.oidc.security.jwt.claims.impl.ExactMatchClaimsValidator"
                             p:claimName="client_id">
                             <property name="valueToMatchLookupStrategy">
                                 <bean class="net.shibboleth.shared.logic.BiFunctionSupport"
-                                    factory-method="forFunctionOfFirstArg"
-                                    c:_0-ref="shibboleth.RelyingPartyIdLookup.Simple" />
+                                    factory-method="forFunctionOfFirstArg">
+                                    <constructor-arg>
+                                        <bean parent="shibboleth.Functions.Expression" c:expression="#custom.apply(#input).getValue()">
+                                            <property name="customObject">
+                                                <bean parent="shibboleth.Functions.Compose"
+                                                    c:g="#{getObject('shibboleth.ClientIDLookupStrategy') ?: getObject('shibboleth.RelyingPartyIdLookup.Simple')}"
+                                                    c:f-ref="shibboleth.MessageContextLookup.Inbound" />
+                                            </property>
+                                        </bean>
+                                    </constructor-arg>
+                                </bean>
                             </property>
                         </bean>
                         <bean class="net.shibboleth.oidc.security.jwt.claims.impl.AudienceClaimsValidator">
@@ -68,4 +75,11 @@
         </property>
     </bean>
 
+    <bean id="DefaultApiAutomaticRegistrationCondition" parent="shibboleth.Conditions.Expression"
+        c:expression="#custom.apply(#input) != null">
+        <property name="customObject">
+            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.navigate.DefaultPreSelectedTrustChainIDsLookupStrategy" />
+        </property>
+    </bean>
+
 </beans>
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/abstract/oidc-abstract-beans.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/abstract/oidc-abstract-beans.xml
index af5f5bf7..79b9ba1c 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/abstract/oidc-abstract-beans.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/abstract/oidc-abstract-beans.xml
@@ -140,4 +140,10 @@
         c:type="#{ T(net.shibboleth.oidc.security.jose.context.SecurityParametersContext) }"
         c:createContext="true" />
 
+    <bean id="StoreAutomaticRegistration" class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.StoreAutomaticRegistration"
+        scope="prototype"
+        p:clientInformationManager-ref="#{'%{idp.oidc.dynreg.clientInformationManager:shibboleth.oidc.ClientInformationManager}'.trim()}"
+        p:activationCondition-ref="shibboleth.Conditions.FALSE">
+    </bean>
+
 </beans>
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/abstract/oidc-abstract-flow.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/abstract/oidc-abstract-flow.xml
index c453f19a..4b91bbdc 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/abstract/oidc-abstract-flow.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/abstract/oidc-abstract-flow.xml
@@ -14,7 +14,20 @@
     </action-state>
 
     <!--  Actions common to most OIDC profile flows. -->
-    <action-state id="BuildResponseMessage">
+    <decision-state id="BuildResponseMessage">
+        <if test="conversationScope.automaticallyRegistered != null and conversationScope.automaticallyRegistered  and opensamlProfileRequestContext.ensureInboundMessageContext().containsSubcontext(T(net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.RelyingPartyTrustChainContext)) and opensamlProfileRequestContext.ensureInboundMessageContext().getSubcontext(T(net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.RelyingPartyTrustChainContext)).getSelectedTrustChain() != null"
+            then="StoreAutomaticRegistration"
+            else="FormResponseMessage" />
+    </decision-state>
+
+    <action-state id="StoreAutomaticRegistration">
+        <evaluate expression="StoreAutomaticRegistration" />
+        <evaluate expression="'proceed'" />
+        <transition on="proceed" to="FormResponseMessage" />
+        <transition to="HandleError" />
+    </action-state>
+
+    <action-state id="FormResponseMessage">
         <evaluate expression="FormOutboundMessage" />
         <evaluate expression="'proceed'" />
         <transition on="proceed" to="PopulateOutboundInterceptContext" />
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/authorize/authorize-flow.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/authorize/authorize-flow.xml
index 5fe04f32..4ffc9741 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/authorize/authorize-flow.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/authorize/authorize-flow.xml
@@ -36,30 +36,7 @@
         <transition on="proceed" to="DoMetadataLookup" />
     </action-state>
 
-    <decision-state id="SelectConfiguration">
-        <on-entry>
-            <set name="flowScope.automaticallyRegistered" value="'false'" />
-        </on-entry>
-        <if test="AutomaticRegistrationCondition.test(opensamlProfileRequestContext) and !opensamlProfileRequestContext.ensureInboundMessageContext().containsSubcontext(T(net.shibboleth.oidc.metadata.context.OIDCMetadataContext))"
-            then="DoAutomaticRegistration" else="DoSelectConfiguration" />
-    </decision-state>
-
-    <action-state id="DoAutomaticRegistration">
-        <evaluate expression="ResolveTrustChains" />
-        <evaluate expression="SelectTrustChain" />
-        <evaluate expression="ResolveTrustMarks" />
-        <evaluate expression="SelectAutomaticRegistrationRelyingPartyConfiguration" />
-        <evaluate expression="SelectAutomaticRegistrationProfileConfiguration" />
-        <evaluate expression="ValidateAutomaticRegistrationProfileConfiguration" />
-        <evaluate expression="InitializeRelyingPartyContext" />
-        <evaluate expression="'proceed'" />
-        <transition on="ReselectTrustChain" to="DoAutomaticRegistration" />
-        <transition on="proceed" to="DoSelectConfiguration">
-            <set name="flowScope.automaticallyRegistered" value="opensamlProfileRequestContext.ensureInboundMessageContext().containsSubcontext(T(net.shibboleth.oidc.metadata.context.OIDCMetadataContext))" />
-        </transition>
-    </action-state>
-
-    <action-state id="DoSelectConfiguration">
+    <action-state id="SelectConfiguration">
         <evaluate expression="SelectRelyingPartyConfiguration" />
         <evaluate expression="SelectProfileConfiguration" />
         <evaluate expression="PostLookupPopulateAuditContext" />
@@ -322,29 +299,16 @@
         <evaluate expression="PopulateClientStorageSaveContext" />
         <evaluate expression="'proceed'" />
         <transition on="proceed" to="ClientStorageSave" />
-        <transition on="NoSaveNeeded" to="CheckIfAutomaticRegistration" />
+        <transition on="NoSaveNeeded" to="BuildResponseMessage" />
         <transition to="HandleError" />
     </action-state>
 
     <subflow-state id="ClientStorageSave" subflow="client-storage/write">
         <input name="calledAsSubflow" value="true" />
-        <transition on="proceed" to="CheckIfAutomaticRegistration"/>
+        <transition on="proceed" to="BuildResponseMessage"/>
         <transition to="HandleError" />
     </subflow-state>
 
-    <decision-state id="CheckIfAutomaticRegistration">
-        <if test="flowScope.automaticallyRegistered and opensamlProfileRequestContext.ensureInboundMessageContext().containsSubcontext(T(net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.RelyingPartyTrustChainContext)) and opensamlProfileRequestContext.ensureInboundMessageContext().getSubcontext(T(net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.RelyingPartyTrustChainContext)).getSelectedTrustChain() != null"
-            then="StoreAutomaticRegistration"
-            else="BuildResponseMessage" />
-    </decision-state>
-
-    <action-state id="StoreAutomaticRegistration">
-        <evaluate expression="StoreAutomaticRegistration" />
-        <evaluate expression="'proceed'" />
-        <transition on="proceed" to="BuildResponseMessage" />
-        <transition to="HandleError" />
-    </action-state>
-
     <!-- Error views, handling and end states -->
 
     <!-- Passthrough state if an exception is thrown. -->
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup/metadata-lookup-beans.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup/metadata-lookup-beans.xml
index b898c7b0..2a27ed5b 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup/metadata-lookup-beans.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup/metadata-lookup-beans.xml
@@ -82,4 +82,86 @@
         </constructor-arg>
     </bean>
 
+    <bean id="ResolveTrustChains" class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.ResolveTrustChains"
+        scope="prototype"
+        p:trustChainCache-ref="#{'%{idp.oidfed.authorize.TrustChainMetadataCache:shibboleth.oidfed.TrustChainMetadataCache}'.trim()}"
+        p:clientIDLookupStrategy-ref="shibboleth.ClientIDLookupStrategy"
+        p:metadataPolicyMergingStrategy-ref="#{'%{idp.oidfed.authorize.TrustChainMetadataPolicyMergingStrategy:DefaultTrustChainMetadataPolicyMergingStrategy}'.trim()}"
+        p:metadataPolicyEnforcer-ref="#{'%{idp.oidfed.authorize.MetadataPolicyEnforcer:DefaultMetadataPolicyEnforcer}'.trim()}"/>
+
+    <bean id="DefaultMetadataPolicyEnforcer"
+        class="net.shibboleth.oidc.metadata.policy.impl.DefaultMetadataPolicyEnforcer" />
+
+    <bean id="DefaultTrustChainMetadataPolicyMergingStrategy" 
+        class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.navigate.DefaultTrustChainMetadataPolicyMergingStrategy"
+        p:localMetadataPolicyStrategy-ref="#{'%{idp.oidfed.authorize.LocalMetadataPolicyStrategy:DefaultLocalMetadataPolicyStrategy}'.trim()}"/>
+
+    <bean id="DefaultLocalMetadataPolicyStrategy"
+        parent="shibboleth.Functions.Constant">
+        <constructor-arg name="target">
+            <util:map>
+                <entry key="scope">
+                    <bean class="net.shibboleth.oidc.metadata.policy.MetadataPolicy" p:defaultValue="openid" />
+                </entry>
+                <entry key="token_endpoint_auth_method">
+                    <bean class="net.shibboleth.oidc.metadata.policy.MetadataPolicy" p:defaultValue="private_key_jwt" />
+                </entry>
+            </util:map>
+        </constructor-arg>
+    </bean>
+
+    <bean id="AutomaticRegistrationRelyingPartyCreationStrategy" parent="shibboleth.Functions.Compose"
+        c:g-ref="shibboleth.ChildLookupOrCreate.RelyingPartyContext"
+        c:f-ref="RelyingPartyTrustChainContextLookupStrategy" />
+
+    <bean id="RelyingPartyTrustChainContextLookupStrategy" parent="shibboleth.Functions.Expression"
+        c:expression="#input.ensureInboundMessageContext().getSubcontext(T(net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.RelyingPartyTrustChainContext))" />
+
+    <bean id="SelectAutomaticRegistrationRelyingPartyConfiguration"
+        class="net.shibboleth.idp.profile.impl.SelectRelyingPartyConfiguration" scope="prototype"
+        p:relyingPartyContextLookupStrategy-ref="AutomaticRegistrationRelyingPartyCreationStrategy"
+        p:relyingPartyConfigurationResolver-ref="shibboleth.RelyingPartyResolverService" />
+
+    <bean id="SelectAutomaticRegistrationProfileConfiguration"
+        class="net.shibboleth.idp.profile.impl.SelectProfileConfiguration" scope="prototype"
+        p:relyingPartyContextLookupStrategy-ref="AutomaticRegistrationRelyingPartyCreationStrategy"
+        p:profileId="#{T(net.shibboleth.idp.plugin.oidc.op.oidfed.config.OIDFederationAutomaticRegistrationConfiguration).PROFILE_ID}" />
+
+    <bean id="SelectTrustChain" class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.SelectTrustChain"
+        scope="prototype">
+        <property name="activationCondition">
+            <bean parent="shibboleth.Conditions.Expression"
+                c:expression="#input.ensureInboundMessageContext().containsSubcontext(T(net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.RelyingPartyTrustChainContext))" />
+        </property>
+    </bean>
+
+    <bean id="ResolveTrustMarks" class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.ResolveTrustMarks"
+        scope="prototype"
+        p:trustChainCache-ref="#{'%{idp.oidfed.authorize.TrustChainMetadataCache:shibboleth.oidfed.TrustChainMetadataCache}'.trim()}">
+        <property name="trustEngine">
+            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.TokenPayloadAsymmetricKeyTrustEngine">
+                <constructor-arg>
+                    <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultEntityStatementCredentialResolver" />
+                 </constructor-arg>
+            </bean>
+        </property>
+        <property name="trustedTrustMarkIssuersLookupStrategy">
+            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultTrustChainTrustedTrustMarkIssuersLookupStrategy"
+                p:objectMapper-ref="shibboleth.oidc.JSONObjectMapper" />
+        </property>
+    </bean>
+
+    <bean id="ValidateAutomaticRegistrationProfileConfiguration"
+        class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.ValidateAutomaticRegistrationProfileConfiguration"
+        scope="prototype">
+        <property name="localMetadataPolicyMergingStrategy">
+            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.navigate.DefaultLocalMetadataPolicyMergingStrategy"
+                p:metadataPolicyEnforcer-ref="#{'%{idp.oidfed.authorize.MetadataPolicyEnforcer:DefaultMetadataPolicyEnforcer}'.trim()}"/>
+        </property>
+        <property name="mandatoryTrustMarksLookupStrategy">
+            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.navigate.MandatoryTrustMarksLookupFunction"
+                p:relyingPartyContextLookupStrategy-ref="AutomaticRegistrationRelyingPartyCreationStrategy"/>
+        </property>
+    </bean>
+
 </beans>
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup/metadata-lookup-flow.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup/metadata-lookup-flow.xml
index 53f58ddf..a904530e 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup/metadata-lookup-flow.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup/metadata-lookup-flow.xml
@@ -30,7 +30,7 @@
 
     <decision-state id="CheckIfFoundFromSAMLMetadata">
         <if test="opensamlProfileRequestContext.ensureInboundMessageContext().containsSubcontext(T(org.opensaml.saml.common.messaging.context.SAMLPeerEntityContext)) and opensamlProfileRequestContext.ensureInboundMessageContext().ensureSubcontext(T(org.opensaml.saml.common.messaging.context.SAMLPeerEntityContext)).containsSubcontext(T(org.opensaml.saml.common.messaging.context.SAMLMetadataContext))"
-            then="PopulateOIDCMetadataContextFromSAML" else="SelectConfiguration" />
+            then="PopulateOIDCMetadataContextFromSAML" else="CheckIfResolveTrustChains" />
     </decision-state>
     
     <action-state id="PopulateOIDCMetadataContextFromSAML">
@@ -40,6 +40,39 @@
         <transition on="proceed" to="SelectConfiguration" />
     </action-state>
 
+    <decision-state id="CheckIfResolveTrustChains">
+        <on-entry>
+            <set name="conversationScope.automaticallyRegistered" value="false" />
+        </on-entry>
+        <if test="AutomaticRegistrationCondition.test(opensamlProfileRequestContext) and !opensamlProfileRequestContext.ensureInboundMessageContext().containsSubcontext(T(net.shibboleth.oidc.metadata.context.OIDCMetadataContext))"
+            then="ResolveTrustChains" else="SelectConfiguration" />
+    </decision-state>
+
+    <action-state id="ResolveTrustChains">
+        <evaluate expression="ResolveTrustChains" />
+        <evaluate expression="'proceed'" />
+        <transition on="proceed" to="CheckIfDoAutomaticRegistration" />
+    </action-state>
+
+    <decision-state id="CheckIfDoAutomaticRegistration">
+        <if test="opensamlProfileRequestContext.ensureInboundMessageContext().containsSubcontext(T(net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.RelyingPartyTrustChainContext)) and opensamlProfileRequestContext.ensureInboundMessageContext().ensureSubcontext(T(net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.RelyingPartyTrustChainContext)).getPolicyCompliantTrustChains() != null and opensamlProfileRequestContext.ensureInboundMessageContext().ensureSubcontext(T(net.shibboleth. [...]
+            then="DoAutomaticRegistration" else="SelectConfiguration" />
+    </decision-state>
+
+    <action-state id="DoAutomaticRegistration">
+        <evaluate expression="SelectTrustChain" />
+        <evaluate expression="ResolveTrustMarks" />
+        <evaluate expression="SelectAutomaticRegistrationRelyingPartyConfiguration" />
+        <evaluate expression="SelectAutomaticRegistrationProfileConfiguration" />
+        <evaluate expression="ValidateAutomaticRegistrationProfileConfiguration" />
+        <evaluate expression="InitializeRelyingPartyContext" />
+        <evaluate expression="'proceed'" />
+        <transition on="ReselectTrustChain" to="DoAutomaticRegistration" />
+        <transition on="proceed" to="SelectConfiguration">
+            <set name="conversationScope.automaticallyRegistered" value="opensamlProfileRequestContext.ensureInboundMessageContext().containsSubcontext(T(net.shibboleth.oidc.metadata.context.OIDCMetadataContext))" />
+        </transition>
+    </action-state>
+
     <bean-import resource="classpath:/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup/metadata-lookup-beans.xml" />
 
 </flow>
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/token/token-beans.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/token/token-beans.xml
index 602f119c..f27a9e75 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/token/token-beans.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/token/token-beans.xml
@@ -33,6 +33,10 @@
     <bean id="shibboleth.ClientIDLookupStrategy"
         class="net.shibboleth.idp.plugin.oidc.op.messaging.context.navigate.TokenRequestClientIDLookupFunction" />
 
+    <bean id="UnwrapGrant" class="net.shibboleth.idp.plugin.oidc.op.profile.impl.UnwrapGrant" scope="prototype"
+        c:sealer-ref="#{'%{idp.oidc.tokenSealer:shibboleth.oidc.TokenSealer}'.trim()}"
+        p:refreshTokenDeserializers-ref="#{'%{idp.oauth2.refreshToken.deserializers:shibboleth.oidc.DefaultRefreshTokenDeserializers}'.trim()}"/>
+
     <bean id="ResolveAttributesForClientPredicate"
         class="net.shibboleth.profile.config.logic.ResolveAttributesPredicate" />
 
@@ -74,10 +78,8 @@
     <!-- Traditional third-party grant handling. -->
 
     <bean id="ValidateGrant" class="net.shibboleth.idp.plugin.oidc.op.profile.impl.ValidateGrant" scope="prototype"
-        c:sealer-ref="#{'%{idp.oidc.tokenSealer:shibboleth.oidc.TokenSealer}'.trim()}"
         p:replayCache-ref="shibboleth.ReplayCache"
         p:revocationCache-ref="shibboleth.oidc.RevocationCache"
-        p:refreshTokenDeserializers-ref="#{'%{idp.oauth2.refreshToken.deserializers:shibboleth.oidc.DefaultRefreshTokenDeserializers}'.trim()}"
         p:tokenRevocationCondition="#{getObject('%{idp.oauth2.revocationCondition:shibboleth.BiConditions.FALSE}')}">
         <property name="chainRevocationLifetimeLookupStrategy">
             <bean class="net.shibboleth.idp.plugin.oidc.op.profile.logic.DefaultChainRevocationLifetimeLookupStrategy"
@@ -819,4 +821,6 @@
     <bean id="PostResponsePopulateAuditContext" parent="shibboleth.AbstractPopulateAuditContext"
         p:fieldExtractors="#{getObject('shibboleth.oidc.TokenPostResponseAuditExtractors') ?: getObject('shibboleth.oidc.DefaultTokenPostResponseAuditExtractors')}" />
 
+    <alias alias="AutomaticRegistrationCondition" name="%{idp.oidfed.token.automaticRegistrationCondition:DefaultApiAutomaticRegistrationCondition}" />
+
 </beans>
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/token/token-flow.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/token/token-flow.xml
index 98e1d836..9eb45d3f 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/token/token-flow.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/token/token-flow.xml
@@ -9,7 +9,17 @@
         <evaluate expression="InitializeOutboundMessageContext" />
         <evaluate expression="'proceed'" />
         
-        <transition on="proceed" to="DecodeMessage"/>
+        <transition on="proceed" to="DecodeMessage">
+            <set name="flowScope.transitionAfterDecode" value="'PostDecodeMessage'" />
+         </transition>
+    </action-state>
+
+    <action-state id="PostDecodeMessage">
+        <evaluate expression="UnwrapGrant" />
+        <evaluate expression="'proceed'" />
+        
+        <!-- DoMetadataLookup is expected to proceed to SelectConfiguration -->
+        <transition on="proceed" to="DoMetadataLookup" />
     </action-state>
 
     <action-state id="SelectConfiguration">
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/userinfo/userinfo-beans.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/userinfo/userinfo-beans.xml
index 61113570..71a9627e 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/userinfo/userinfo-beans.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/userinfo/userinfo-beans.xml
@@ -237,4 +237,6 @@
         </property>
     </bean>
 
+    <alias alias="AutomaticRegistrationCondition" name="%{idp.oidfed.userinfo.automaticRegistrationCondition:DefaultApiAutomaticRegistrationCondition}" />
+
 </beans>
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateGrantTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/UnwrapGrantTest.java
similarity index 60%
copy from idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateGrantTest.java
copy to idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/UnwrapGrantTest.java
index e3083841..21d47723 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateGrantTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/UnwrapGrantTest.java
@@ -16,36 +16,25 @@ package net.shibboleth.idp.plugin.oidc.op.profile.impl;
 
 import net.minidev.json.JSONObject;
 import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext;
-import net.shibboleth.idp.plugin.oidc.op.storage.RevocationCacheContexts;
 import net.shibboleth.idp.plugin.oidc.op.token.support.AuthorizeCodeClaimsSet;
 import net.shibboleth.idp.plugin.oidc.op.token.support.RefreshTokenClaimsSet;
 import net.shibboleth.idp.plugin.oidc.op.token.support.TokenClaimsSet;
-import net.shibboleth.idp.profile.IdPEventIds;
 import net.shibboleth.idp.profile.testing.ActionTestingSupport;
 import net.shibboleth.oidc.profile.core.OidcEventIds;
 import net.shibboleth.shared.component.ComponentInitializationException;
-import net.shibboleth.shared.logic.PredicateSupport;
 import net.shibboleth.shared.security.DataSealerException;
 import net.shibboleth.shared.security.impl.SecureRandomIdentifierGenerationStrategy;
 
 import java.net.URI;
 import java.security.NoSuchAlgorithmException;
 import java.text.ParseException;
-import java.time.Duration;
 import java.time.Instant;
 import java.util.Collection;
 import java.util.List;
 import java.util.function.BiFunction;
-import java.util.function.Function;
 
-import org.mockito.Mockito;
 import org.opensaml.profile.context.ProfileRequestContext;
-import org.opensaml.storage.RevocationCache;
-import org.opensaml.storage.impl.MemoryStorageService;
-import org.opensaml.storage.impl.StorageServiceReplayCache;
-import org.opensaml.storage.impl.StorageServiceRevocationCache;
 import org.testng.Assert;
-import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
 import com.nimbusds.oauth2.sdk.AuthorizationCode;
@@ -61,10 +50,10 @@ import com.nimbusds.oauth2.sdk.id.ClientID;
 import com.nimbusds.oauth2.sdk.token.RefreshToken;
 import com.nimbusds.openid.connect.sdk.claims.ClaimsSet;
 
-/** {@link ValidateGrant} unit test. */
-public class ValidateGrantTest extends BaseOIDCResponseActionTest {
+/** {@link UnwrapGrant} unit test. */
+public class UnwrapGrantTest extends BaseOIDCResponseActionTest {
 
-    private ValidateGrant action;
+    private UnwrapGrant action;
 
     TokenClaimsSet acClaims;
 
@@ -76,38 +65,14 @@ public class ValidateGrantTest extends BaseOIDCResponseActionTest {
 
     URI callback;
     
-    MemoryStorageService storageService;
-    
     String rootTokenId;
 
-    @BeforeMethod
-    protected void setupStorage() throws ComponentInitializationException {
-        storageService = new MemoryStorageService();
-        storageService.setId("id");
-        storageService.initialize();
-    }
-    
     private void init() throws Exception {
-        init(true);
-    }
-
-    private void init(boolean refreshTokensEnabled) throws Exception {
-        init(refreshTokensEnabled, new MockRevocationCache(false, true), null);
-    }
-
-    private void init(final Instant authenticationTime) throws Exception {
-        init(true, new MockRevocationCache(false, true), null, authenticationTime, null);
-    }
-
-    private void init(boolean refreshTokensEnabled, final RevocationCache revocationCache,
-            final Function<ProfileRequestContext, Duration> revocationLifetimeLookup) throws Exception {
-        init(refreshTokensEnabled, revocationCache, revocationLifetimeLookup, Instant.now(), null);
+        init(Instant.now(), null);
     }
 
     @SuppressWarnings("null")
-    private void init(boolean refreshTokensEnabled, final RevocationCache revocationCache,
-            final Function<ProfileRequestContext, Duration> revocationLifetimeLookup,
-            final Instant authenticationTime,
+    private void init(final Instant authenticationTime,
             final List<BiFunction<ProfileRequestContext,String,RefreshTokenClaimsSet>> deserializers)
                     throws Exception {
         final Instant now = Instant.now();
@@ -136,22 +101,10 @@ public class ValidateGrantTest extends BaseOIDCResponseActionTest {
         // by default we create authz code request
         final TokenRequest req = new TokenRequest(callback, new ClientID(clientId), codeGrant);
         profileRequestCtx.ensureInboundMessageContext().setMessage(req);
-        action = new ValidateGrant(getDataSealer());
-        if (revocationLifetimeLookup != null) {
-            action.setChainRevocationLifetimeLookupStrategy(revocationLifetimeLookup);
-        }
-        action.setRevocationCache(revocationCache);
+        action = new UnwrapGrant(getDataSealer());
         if (deserializers != null) {
             action.setRefreshTokenDeserializers(deserializers);
         }
-        final StorageServiceReplayCache replayCache = new StorageServiceReplayCache();
-        replayCache.setStorage(storageService);
-        action.setReplayCache(replayCache);
-        if (refreshTokensEnabled) {
-            action.setRefreshTokensEnabledPredicate(PredicateSupport.alwaysTrue());
-        } else {
-            action.setRefreshTokensEnabledPredicate(PredicateSupport.alwaysFalse());
-        }
         action.initialize();
     }
 
@@ -188,7 +141,7 @@ public class ValidateGrantTest extends BaseOIDCResponseActionTest {
                     throws Exception {
         final TokenClaimsSet acClaims = buildTokenClaimsSet(clientId, issuer, userPrincipal, sub, callbackUrl,
                 codeChallenge, deliveryClaims, deliveryClaimsIDToken, deliveryClaimsUserInfo, scope, aud);
-        return new AuthorizationCode(acClaims.serialize(new ValidateGrantTest().getDataSealer()));
+        return new AuthorizationCode(acClaims.serialize(new UnwrapGrantTest().getDataSealer()));
     }
 
     @SuppressWarnings("null")
@@ -235,13 +188,6 @@ public class ValidateGrantTest extends BaseOIDCResponseActionTest {
         Assert.assertNotNull(arc.getAuthorizationGrantClaimsSet());
     }
 
-    @Test
-    public void testAuthorizeCodeReplayed() throws Exception {
-        init();
-        ActionTestingSupport.assertProceedEvent(action.execute(requestCtx));
-        ActionTestingSupport.assertEvent(action.execute(requestCtx), OidcEventIds.INVALID_GRANT);
-    }
-
     @Test
     public void testRefreshTokenSuccess() throws Exception {
         init();
@@ -281,7 +227,7 @@ public class ValidateGrantTest extends BaseOIDCResponseActionTest {
                     }
             
         });
-        init(true, new MockRevocationCache(false, true), null, Instant.now(), deserializers);
+        init(Instant.now(), deserializers);
         final RefreshToken customToken = new RefreshToken("customPrefix" + rfGrant.getRefreshToken().getValue());
         final TokenRequest req = new TokenRequest(callback, new ClientID(clientId), new RefreshTokenGrant(customToken));
         profileRequestCtx.ensureInboundMessageContext().setMessage(req);
@@ -291,100 +237,6 @@ public class ValidateGrantTest extends BaseOIDCResponseActionTest {
         Assert.assertNotNull(arc.getAuthorizationGrantClaimsSet());
     }
 
-    @Test
-    public void testRefreshTokenChainExpired() throws Exception {
-        init(Instant.now().minus(Duration.ofHours(2)));
-        final TokenRequest req = new TokenRequest(callback, new ClientID(clientId), rfGrant);
-        profileRequestCtx.ensureInboundMessageContext().setMessage(req);
-        ActionTestingSupport.assertEvent(action.execute(requestCtx), OidcEventIds.INVALID_GRANT);
-        final OIDCAuthenticationResponseContext arc =
-                profileRequestCtx.ensureOutboundMessageContext().ensureSubcontext(OIDCAuthenticationResponseContext.class);
-        Assert.assertNull(arc.getAuthorizationGrantClaimsSet());
-    }
-
-    @Test
-    public void testRefreshTokenNotEnabled() throws Exception {
-        init(false);
-        final TokenRequest req = new TokenRequest(callback, new ClientID(clientId), rfGrant);
-        profileRequestCtx.ensureInboundMessageContext().setMessage(req);
-        ActionTestingSupport.assertEvent(action.execute(requestCtx), OidcEventIds.INVALID_GRANT);
-        final OIDCAuthenticationResponseContext arc =
-                profileRequestCtx.ensureOutboundMessageContext().ensureSubcontext(OIDCAuthenticationResponseContext.class);
-        Assert.assertNull(arc.getAuthorizationGrantClaimsSet());
-    }
-
-    @Test
-    public void testRefreshTokenReplayed() throws Exception {
-        init();
-        final TokenRequest req = new TokenRequest(callback, new ClientID(clientId), rfGrant);
-        profileRequestCtx.ensureInboundMessageContext().setMessage(req);
-        ActionTestingSupport.assertProceedEvent(action.execute(requestCtx));
-        ActionTestingSupport.assertProceedEvent(action.execute(requestCtx));
-        final OIDCAuthenticationResponseContext arc =
-                profileRequestCtx.ensureOutboundMessageContext().ensureSubcontext(OIDCAuthenticationResponseContext.class);
-        Assert.assertNotNull(arc.getAuthorizationGrantClaimsSet());
-    }
-
-    @Test
-    public void testRefreshTokenAuthorizationGrantRevoked() throws Exception {
-        final RevocationCache revocationCache = buildRevocationCache();
-        init(true, revocationCache, null);
-        final String rootJti = rfClaims.getRootTokenIdentifier();
-        assert rootJti != null;
-        Assert.assertTrue(revocationCache.revoke(RevocationCacheContexts.AUTHORIZATION_CODE, rootJti));
-        final TokenRequest req = new TokenRequest(callback, new ClientID(clientId), rfGrant);
-        profileRequestCtx.ensureInboundMessageContext().setMessage(req);
-        ActionTestingSupport.assertEvent(action.execute(requestCtx), OidcEventIds.INVALID_GRANT);
-        final OIDCAuthenticationResponseContext arc =
-                profileRequestCtx.ensureOutboundMessageContext().ensureSubcontext(OIDCAuthenticationResponseContext.class);
-        Assert.assertNull(arc.getAuthorizationGrantClaimsSet());
-    }
-
-    @Test
-    public void testRefreshTokenRevokedShouldRevokeAuthorizationCode() throws Exception {
-        final RevocationCache revocationCache = buildRevocationCache();
-        init(true, revocationCache, null);
-        final String jti = rfClaims.getID();
-        assert jti != null;
-        Assert.assertTrue(revocationCache.revoke(RevocationCacheContexts.SINGLE_ACCESS_OR_REFRESH_TOKENS, jti));
-        final TokenRequest req = new TokenRequest(callback, new ClientID(clientId), rfGrant);
-        profileRequestCtx.ensureInboundMessageContext().setMessage(req);
-        ActionTestingSupport.assertEvent(action.execute(requestCtx), OidcEventIds.INVALID_GRANT);
-        final OIDCAuthenticationResponseContext arc =
-                profileRequestCtx.ensureOutboundMessageContext().ensureSubcontext(OIDCAuthenticationResponseContext.class);
-        Assert.assertNull(arc.getAuthorizationGrantClaimsSet());
-        final String rootJti = rfClaims.getRootTokenIdentifier();
-        assert rootJti != null;
-        Assert.assertTrue(revocationCache.isRevoked(RevocationCacheContexts.AUTHORIZATION_CODE, rootJti));
-    }
-
-    protected RevocationCache buildRevocationCache() throws ComponentInitializationException {
-        final StorageServiceRevocationCache revocationCache = new StorageServiceRevocationCache();
-        revocationCache.setId("mockCache");
-        assert storageService != null;
-        revocationCache.setStorage(storageService);
-        revocationCache.initialize();
-        return revocationCache;
-    }
-
-    @SuppressWarnings("null")
-    @Test
-    public void testTokenRevocationViaRevokedTokenFailsReturnsInvalidProfileConfig() throws Exception {
-        RevocationCache revocationCache = Mockito.mock(RevocationCache.class);
-        Mockito.when(revocationCache.isRevoked(Mockito.matches(RevocationCacheContexts.AUTHORIZATION_CODE),
-                Mockito.anyString())).thenReturn(false);
-        Mockito.when(revocationCache.isRevoked(Mockito.matches(RevocationCacheContexts.SINGLE_ACCESS_OR_REFRESH_TOKENS),
-                Mockito.anyString())).thenReturn(true);
-        Mockito.when(revocationCache.revoke(Mockito.anyString(), Mockito.anyString())).thenReturn(false);
-        init(true, revocationCache, prc -> null);
-        final TokenRequest req = new TokenRequest(callback, new ClientID(clientId), rfGrant);
-        profileRequestCtx.ensureInboundMessageContext().setMessage(req);
-        ActionTestingSupport.assertEvent(action.execute(requestCtx), IdPEventIds.INVALID_PROFILE_CONFIG);
-        final OIDCAuthenticationResponseContext arc =
-                profileRequestCtx.ensureOutboundMessageContext().ensureSubcontext(OIDCAuthenticationResponseContext.class);
-        Assert.assertNull(arc.getAuthorizationGrantClaimsSet());
-    }
-
     @Test
     public void testMixGrant() throws Exception {
         init();
@@ -439,25 +291,6 @@ public class ValidateGrantTest extends BaseOIDCResponseActionTest {
         Assert.assertNull(arc.getAuthorizationGrantClaimsSet());
     }
     
-    @Test(expectedExceptions = ComponentInitializationException.class)
-    public void testNoRevocationCache() throws ComponentInitializationException, NoSuchAlgorithmException {
-        action = new ValidateGrant(getDataSealer());
-        final StorageServiceReplayCache replayCache = new StorageServiceReplayCache();
-        final MemoryStorageService storageService = new MemoryStorageService();
-        storageService.setId("mockId");
-        storageService.initialize();
-        replayCache.setStorage(storageService);
-        action.setReplayCache(replayCache);
-        action.initialize();
-    }
-
-    @Test(expectedExceptions = ComponentInitializationException.class)
-    public void testNoReplayCache() throws ComponentInitializationException, NoSuchAlgorithmException {
-        action = new ValidateGrant(getDataSealer());
-        action.setRevocationCache(new MockRevocationCache(false, true));
-        action.initialize();
-    }
-
     private static class DeliveryClaimsSet extends ClaimsSet {
 
         public DeliveryClaimsSet(final JSONObject claims) {
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateGrantTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateGrantTest.java
index e3083841..e78c7587 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateGrantTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateGrantTest.java
@@ -25,17 +25,13 @@ import net.shibboleth.idp.profile.testing.ActionTestingSupport;
 import net.shibboleth.oidc.profile.core.OidcEventIds;
 import net.shibboleth.shared.component.ComponentInitializationException;
 import net.shibboleth.shared.logic.PredicateSupport;
-import net.shibboleth.shared.security.DataSealerException;
 import net.shibboleth.shared.security.impl.SecureRandomIdentifierGenerationStrategy;
 
 import java.net.URI;
 import java.security.NoSuchAlgorithmException;
-import java.text.ParseException;
 import java.time.Duration;
 import java.time.Instant;
 import java.util.Collection;
-import java.util.List;
-import java.util.function.BiFunction;
 import java.util.function.Function;
 
 import org.mockito.Mockito;
@@ -50,7 +46,6 @@ import org.testng.annotations.Test;
 
 import com.nimbusds.oauth2.sdk.AuthorizationCode;
 import com.nimbusds.oauth2.sdk.AuthorizationCodeGrant;
-import com.nimbusds.oauth2.sdk.AuthorizationGrant;
 import com.nimbusds.oauth2.sdk.ClientCredentialsGrant;
 import com.nimbusds.oauth2.sdk.RefreshTokenGrant;
 import com.nimbusds.oauth2.sdk.Scope;
@@ -70,10 +65,6 @@ public class ValidateGrantTest extends BaseOIDCResponseActionTest {
 
     TokenClaimsSet rfClaims;
 
-    AuthorizationGrant codeGrant;
-
-    RefreshTokenGrant rfGrant;
-
     URI callback;
     
     MemoryStorageService storageService;
@@ -92,23 +83,16 @@ public class ValidateGrantTest extends BaseOIDCResponseActionTest {
     }
 
     private void init(boolean refreshTokensEnabled) throws Exception {
-        init(refreshTokensEnabled, new MockRevocationCache(false, true), null);
+        init(refreshTokensEnabled, new MockRevocationCache(false, true), null, Instant.now());
     }
 
     private void init(final Instant authenticationTime) throws Exception {
-        init(true, new MockRevocationCache(false, true), null, authenticationTime, null);
-    }
-
-    private void init(boolean refreshTokensEnabled, final RevocationCache revocationCache,
-            final Function<ProfileRequestContext, Duration> revocationLifetimeLookup) throws Exception {
-        init(refreshTokensEnabled, revocationCache, revocationLifetimeLookup, Instant.now(), null);
+        init(true, new MockRevocationCache(false, true), null, authenticationTime);
     }
 
     @SuppressWarnings("null")
     private void init(boolean refreshTokensEnabled, final RevocationCache revocationCache,
-            final Function<ProfileRequestContext, Duration> revocationLifetimeLookup,
-            final Instant authenticationTime,
-            final List<BiFunction<ProfileRequestContext,String,RefreshTokenClaimsSet>> deserializers)
+            final Function<ProfileRequestContext, Duration> revocationLifetimeLookup, final Instant authenticationTime)
                     throws Exception {
         final Instant now = Instant.now();
         rootTokenId = "mockId" + now.toEpochMilli();
@@ -128,22 +112,12 @@ public class ValidateGrantTest extends BaseOIDCResponseActionTest {
         rfClaims = new RefreshTokenClaimsSet.Builder(acClaims, now, now.plusSeconds(100))
                 .setRootTokenIdentifier(rootTokenId)
                 .build();
-        final AuthorizationCode code = new AuthorizationCode(acClaims.serialize(getDataSealer()));
-        final RefreshToken rfToken = new RefreshToken(rfClaims.serialize(getDataSealer()));
         callback = new URI("https://client.com/callback");
-        codeGrant = new AuthorizationCodeGrant(code, callback);
-        rfGrant = new RefreshTokenGrant(rfToken);
-        // by default we create authz code request
-        final TokenRequest req = new TokenRequest(callback, new ClientID(clientId), codeGrant);
-        profileRequestCtx.ensureInboundMessageContext().setMessage(req);
-        action = new ValidateGrant(getDataSealer());
+        action = new ValidateGrant();
         if (revocationLifetimeLookup != null) {
             action.setChainRevocationLifetimeLookupStrategy(revocationLifetimeLookup);
         }
         action.setRevocationCache(revocationCache);
-        if (deserializers != null) {
-            action.setRefreshTokenDeserializers(deserializers);
-        }
         final StorageServiceReplayCache replayCache = new StorageServiceReplayCache();
         replayCache.setStorage(storageService);
         action.setReplayCache(replayCache);
@@ -153,27 +127,30 @@ public class ValidateGrantTest extends BaseOIDCResponseActionTest {
             action.setRefreshTokensEnabledPredicate(PredicateSupport.alwaysFalse());
         }
         action.initialize();
+        final TokenRequest req = new TokenRequest(callback, new ClientID(clientId), new AuthorizationCodeGrant(new AuthorizationCode("obsolete"), callback));
+        profileRequestCtx.ensureInboundMessageContext().setMessage(req);
+        profileRequestCtx.ensureOutboundMessageContext().ensureSubcontext(OIDCAuthenticationResponseContext.class).setAuthorizationGrantClaimsSet(acClaims);;
     }
 
-    static public AuthorizationCode buildAuthorizationCode(final String clientId, final String issuer,
+    static public TokenClaimsSet buildAuthorizationCode(final String clientId, final String issuer,
             final String userPrincipal, final String sub, final String callbackUrl) throws Exception {
         return buildAuthorizationCode(clientId, issuer, userPrincipal, sub, callbackUrl, null);
     }
 
-    static public AuthorizationCode buildAuthorizationCode(final String clientId, final String issuer,
+    static public TokenClaimsSet buildAuthorizationCode(final String clientId, final String issuer,
             final String userPrincipal, final String sub, final String callbackUrl, final String scope)
                     throws Exception {
         return buildAuthorizationCode(clientId, issuer, userPrincipal, sub, callbackUrl, null, scope);
     }
 
-    static public AuthorizationCode buildAuthorizationCode(final String clientId, final String issuer,
+    static public TokenClaimsSet buildAuthorizationCode(final String clientId, final String issuer,
             final String userPrincipal, final String sub, final String callbackUrl, final String codeChallenge,
             final String scope) throws Exception {
         return buildAuthorizationCode(clientId, issuer, userPrincipal, sub, callbackUrl, codeChallenge, null, null,
                 null, scope);
     }
 
-    static public AuthorizationCode buildAuthorizationCode(final String clientId, final String issuer,
+    static public TokenClaimsSet buildAuthorizationCode(final String clientId, final String issuer,
             final String userPrincipal, final String sub, final String callbackUrl, final String codeChallenge,
             final JSONObject deliveryClaims, final JSONObject deliveryClaimsIDToken,
             final JSONObject deliveryClaimsUserInfo, final String scope) throws Exception {
@@ -181,14 +158,13 @@ public class ValidateGrantTest extends BaseOIDCResponseActionTest {
                 deliveryClaimsIDToken, deliveryClaimsUserInfo, scope, null);
     }
 
-    static public AuthorizationCode buildAuthorizationCode(final String clientId, final String issuer,
+    static public TokenClaimsSet buildAuthorizationCode(final String clientId, final String issuer,
             final String userPrincipal, final String sub, final String callbackUrl, final String codeChallenge,
             final JSONObject deliveryClaims, final JSONObject deliveryClaimsIDToken,
             final JSONObject deliveryClaimsUserInfo, final String scope, final Collection<String> aud)
                     throws Exception {
-        final TokenClaimsSet acClaims = buildTokenClaimsSet(clientId, issuer, userPrincipal, sub, callbackUrl,
+        return buildTokenClaimsSet(clientId, issuer, userPrincipal, sub, callbackUrl,
                 codeChallenge, deliveryClaims, deliveryClaimsIDToken, deliveryClaimsUserInfo, scope, aud);
-        return new AuthorizationCode(acClaims.serialize(new ValidateGrantTest().getDataSealer()));
     }
 
     @SuppressWarnings("null")
@@ -245,46 +221,9 @@ public class ValidateGrantTest extends BaseOIDCResponseActionTest {
     @Test
     public void testRefreshTokenSuccess() throws Exception {
         init();
-        final TokenRequest req = new TokenRequest(callback, new ClientID(clientId), rfGrant);
-        profileRequestCtx.ensureInboundMessageContext().setMessage(req);
-        ActionTestingSupport.assertProceedEvent(action.execute(requestCtx));
-        final OIDCAuthenticationResponseContext arc =
-                profileRequestCtx.ensureOutboundMessageContext().ensureSubcontext(OIDCAuthenticationResponseContext.class);
-        Assert.assertNotNull(arc.getAuthorizationGrantClaimsSet());
-    }
-
-    @Test
-    public void testCustomRefreshTokenNoDeserializers() throws Exception {
-        init();
-        final RefreshToken customToken = new RefreshToken("customPrefix" + rfGrant.getRefreshToken().getValue());
-        final TokenRequest req = new TokenRequest(callback, new ClientID(clientId), new RefreshTokenGrant(customToken));
-        profileRequestCtx.ensureInboundMessageContext().setMessage(req);
-        ActionTestingSupport.assertEvent(action.execute(requestCtx), OidcEventIds.INVALID_GRANT);
-    }
-
-    @Test
-    public void testCustomRefreshTokenSuccess() throws Exception {
-        final List<BiFunction<ProfileRequestContext,String,RefreshTokenClaimsSet>> deserializers =
-                List.of(new BiFunction<>() {
-
-                    @Override
-                    public RefreshTokenClaimsSet apply(final ProfileRequestContext prc, final String value ) {
-                        try {
-                            final String strippedValue = value.substring("customPrefix".length());
-                            assert strippedValue != null;
-                            return RefreshTokenClaimsSet.parse(strippedValue, getDataSealer());
-                        } catch (NoSuchAlgorithmException | ParseException | DataSealerException
-                                | ComponentInitializationException e) {
-                            Assert.fail("Could not decrypt the custom refresh token", e);
-                        }
-                        return null;
-                    }
-            
-        });
-        init(true, new MockRevocationCache(false, true), null, Instant.now(), deserializers);
-        final RefreshToken customToken = new RefreshToken("customPrefix" + rfGrant.getRefreshToken().getValue());
-        final TokenRequest req = new TokenRequest(callback, new ClientID(clientId), new RefreshTokenGrant(customToken));
+        final TokenRequest req = new TokenRequest(callback, new ClientID(clientId), new RefreshTokenGrant(new RefreshToken("obsolete")));
         profileRequestCtx.ensureInboundMessageContext().setMessage(req);
+        profileRequestCtx.ensureOutboundMessageContext().ensureSubcontext(OIDCAuthenticationResponseContext.class).setAuthorizationGrantClaimsSet(rfClaims);;
         ActionTestingSupport.assertProceedEvent(action.execute(requestCtx));
         final OIDCAuthenticationResponseContext arc =
                 profileRequestCtx.ensureOutboundMessageContext().ensureSubcontext(OIDCAuthenticationResponseContext.class);
@@ -294,65 +233,55 @@ public class ValidateGrantTest extends BaseOIDCResponseActionTest {
     @Test
     public void testRefreshTokenChainExpired() throws Exception {
         init(Instant.now().minus(Duration.ofHours(2)));
-        final TokenRequest req = new TokenRequest(callback, new ClientID(clientId), rfGrant);
+        final TokenRequest req = new TokenRequest(callback, new ClientID(clientId), new RefreshTokenGrant(new RefreshToken("obsolete")));
         profileRequestCtx.ensureInboundMessageContext().setMessage(req);
+        profileRequestCtx.ensureOutboundMessageContext().ensureSubcontext(OIDCAuthenticationResponseContext.class).setAuthorizationGrantClaimsSet(rfClaims);;
         ActionTestingSupport.assertEvent(action.execute(requestCtx), OidcEventIds.INVALID_GRANT);
-        final OIDCAuthenticationResponseContext arc =
-                profileRequestCtx.ensureOutboundMessageContext().ensureSubcontext(OIDCAuthenticationResponseContext.class);
-        Assert.assertNull(arc.getAuthorizationGrantClaimsSet());
     }
 
     @Test
     public void testRefreshTokenNotEnabled() throws Exception {
         init(false);
-        final TokenRequest req = new TokenRequest(callback, new ClientID(clientId), rfGrant);
+        final TokenRequest req = new TokenRequest(callback, new ClientID(clientId), new RefreshTokenGrant(new RefreshToken("obsolete")));
         profileRequestCtx.ensureInboundMessageContext().setMessage(req);
+        profileRequestCtx.ensureOutboundMessageContext().ensureSubcontext(OIDCAuthenticationResponseContext.class).setAuthorizationGrantClaimsSet(rfClaims);;
         ActionTestingSupport.assertEvent(action.execute(requestCtx), OidcEventIds.INVALID_GRANT);
-        final OIDCAuthenticationResponseContext arc =
-                profileRequestCtx.ensureOutboundMessageContext().ensureSubcontext(OIDCAuthenticationResponseContext.class);
-        Assert.assertNull(arc.getAuthorizationGrantClaimsSet());
     }
 
     @Test
     public void testRefreshTokenReplayed() throws Exception {
         init();
-        final TokenRequest req = new TokenRequest(callback, new ClientID(clientId), rfGrant);
+        final TokenRequest req = new TokenRequest(callback, new ClientID(clientId), new RefreshTokenGrant(new RefreshToken("obsolete")));
         profileRequestCtx.ensureInboundMessageContext().setMessage(req);
+        profileRequestCtx.ensureOutboundMessageContext().ensureSubcontext(OIDCAuthenticationResponseContext.class).setAuthorizationGrantClaimsSet(rfClaims);;
         ActionTestingSupport.assertProceedEvent(action.execute(requestCtx));
         ActionTestingSupport.assertProceedEvent(action.execute(requestCtx));
-        final OIDCAuthenticationResponseContext arc =
-                profileRequestCtx.ensureOutboundMessageContext().ensureSubcontext(OIDCAuthenticationResponseContext.class);
-        Assert.assertNotNull(arc.getAuthorizationGrantClaimsSet());
     }
 
     @Test
     public void testRefreshTokenAuthorizationGrantRevoked() throws Exception {
         final RevocationCache revocationCache = buildRevocationCache();
-        init(true, revocationCache, null);
+        init(true, revocationCache, null, Instant.now());
         final String rootJti = rfClaims.getRootTokenIdentifier();
         assert rootJti != null;
         Assert.assertTrue(revocationCache.revoke(RevocationCacheContexts.AUTHORIZATION_CODE, rootJti));
-        final TokenRequest req = new TokenRequest(callback, new ClientID(clientId), rfGrant);
+        final TokenRequest req = new TokenRequest(callback, new ClientID(clientId), new RefreshTokenGrant(new RefreshToken("obsolete")));
         profileRequestCtx.ensureInboundMessageContext().setMessage(req);
+        profileRequestCtx.ensureOutboundMessageContext().ensureSubcontext(OIDCAuthenticationResponseContext.class).setAuthorizationGrantClaimsSet(rfClaims);;
         ActionTestingSupport.assertEvent(action.execute(requestCtx), OidcEventIds.INVALID_GRANT);
-        final OIDCAuthenticationResponseContext arc =
-                profileRequestCtx.ensureOutboundMessageContext().ensureSubcontext(OIDCAuthenticationResponseContext.class);
-        Assert.assertNull(arc.getAuthorizationGrantClaimsSet());
     }
 
     @Test
     public void testRefreshTokenRevokedShouldRevokeAuthorizationCode() throws Exception {
         final RevocationCache revocationCache = buildRevocationCache();
-        init(true, revocationCache, null);
+        init(true, revocationCache, null, Instant.now());
         final String jti = rfClaims.getID();
         assert jti != null;
         Assert.assertTrue(revocationCache.revoke(RevocationCacheContexts.SINGLE_ACCESS_OR_REFRESH_TOKENS, jti));
-        final TokenRequest req = new TokenRequest(callback, new ClientID(clientId), rfGrant);
+        final TokenRequest req = new TokenRequest(callback, new ClientID(clientId), new RefreshTokenGrant(new RefreshToken("obsolete")));
         profileRequestCtx.ensureInboundMessageContext().setMessage(req);
+        profileRequestCtx.ensureOutboundMessageContext().ensureSubcontext(OIDCAuthenticationResponseContext.class).setAuthorizationGrantClaimsSet(rfClaims);;
         ActionTestingSupport.assertEvent(action.execute(requestCtx), OidcEventIds.INVALID_GRANT);
-        final OIDCAuthenticationResponseContext arc =
-                profileRequestCtx.ensureOutboundMessageContext().ensureSubcontext(OIDCAuthenticationResponseContext.class);
-        Assert.assertNull(arc.getAuthorizationGrantClaimsSet());
         final String rootJti = rfClaims.getRootTokenIdentifier();
         assert rootJti != null;
         Assert.assertTrue(revocationCache.isRevoked(RevocationCacheContexts.AUTHORIZATION_CODE, rootJti));
@@ -376,13 +305,13 @@ public class ValidateGrantTest extends BaseOIDCResponseActionTest {
         Mockito.when(revocationCache.isRevoked(Mockito.matches(RevocationCacheContexts.SINGLE_ACCESS_OR_REFRESH_TOKENS),
                 Mockito.anyString())).thenReturn(true);
         Mockito.when(revocationCache.revoke(Mockito.anyString(), Mockito.anyString())).thenReturn(false);
-        init(true, revocationCache, prc -> null);
-        final TokenRequest req = new TokenRequest(callback, new ClientID(clientId), rfGrant);
+        init(true, revocationCache, prc -> null, Instant.now());
+        final TokenRequest req = new TokenRequest(callback, new ClientID(clientId),
+                new RefreshTokenGrant(new RefreshToken("obsolete")));
         profileRequestCtx.ensureInboundMessageContext().setMessage(req);
+        profileRequestCtx.ensureOutboundMessageContext().ensureSubcontext(OIDCAuthenticationResponseContext.class)
+            .setAuthorizationGrantClaimsSet(rfClaims);
         ActionTestingSupport.assertEvent(action.execute(requestCtx), IdPEventIds.INVALID_PROFILE_CONFIG);
-        final OIDCAuthenticationResponseContext arc =
-                profileRequestCtx.ensureOutboundMessageContext().ensureSubcontext(OIDCAuthenticationResponseContext.class);
-        Assert.assertNull(arc.getAuthorizationGrantClaimsSet());
     }
 
     @Test
@@ -392,23 +321,15 @@ public class ValidateGrantTest extends BaseOIDCResponseActionTest {
                 new RefreshTokenGrant(new RefreshToken(acClaims.serialize(getDataSealer()))));
         profileRequestCtx.ensureInboundMessageContext().setMessage(req);
         ActionTestingSupport.assertEvent(action.execute(requestCtx), OidcEventIds.INVALID_GRANT);
-        final OIDCAuthenticationResponseContext arc =
-                profileRequestCtx.ensureOutboundMessageContext().ensureSubcontext(OIDCAuthenticationResponseContext.class);
-        Assert.assertNull(arc.getAuthorizationGrantClaimsSet());
     }
 
     @Test
     public void testWrongClient() throws Exception {
         init();
-        final AuthorizationCode code =
-                buildAuthorizationCode("clientIdWrong", "issuer", "userPrin", "subject", "http://example.com");
-        final TokenRequest req =
-                new TokenRequest(callback, new ClientID(clientId), new AuthorizationCodeGrant(code, callback));
-        profileRequestCtx.ensureInboundMessageContext().setMessage(req);
+        profileRequestCtx.ensureOutboundMessageContext().ensureSubcontext(OIDCAuthenticationResponseContext.class)
+            .setAuthorizationGrantClaimsSet(
+                    buildAuthorizationCode("clientIdWrong", "issuer", "userPrin", "subject", "http://example.com"));
         ActionTestingSupport.assertEvent(action.execute(requestCtx), OidcEventIds.INVALID_GRANT);
-        final OIDCAuthenticationResponseContext arc =
-                profileRequestCtx.ensureOutboundMessageContext().ensureSubcontext(OIDCAuthenticationResponseContext.class);
-        Assert.assertNull(arc.getAuthorizationGrantClaimsSet());
     }
 
     @SuppressWarnings("null")
@@ -421,9 +342,6 @@ public class ValidateGrantTest extends BaseOIDCResponseActionTest {
                 new RefreshTokenGrant(new RefreshToken(rfClaims.serialize(getDataSealer()))));
         profileRequestCtx.ensureInboundMessageContext().setMessage(req);
         ActionTestingSupport.assertEvent(action.execute(requestCtx), OidcEventIds.INVALID_GRANT);
-        final OIDCAuthenticationResponseContext arc =
-                profileRequestCtx.ensureOutboundMessageContext().ensureSubcontext(OIDCAuthenticationResponseContext.class);
-        Assert.assertNull(arc.getAuthorizationGrantClaimsSet());
     }
 
     @Test
@@ -433,6 +351,8 @@ public class ValidateGrantTest extends BaseOIDCResponseActionTest {
                 new ClientSecretBasic(new ClientID(clientId), new Secret("foo")),
                 new ClientCredentialsGrant());
         profileRequestCtx.ensureInboundMessageContext().setMessage(req);
+        profileRequestCtx.ensureOutboundMessageContext().ensureSubcontext(OIDCAuthenticationResponseContext.class)
+            .setAuthorizationGrantClaimsSet(null);
         ActionTestingSupport.assertProceedEvent(action.execute(requestCtx));
         final OIDCAuthenticationResponseContext arc =
                 profileRequestCtx.ensureOutboundMessageContext().ensureSubcontext(OIDCAuthenticationResponseContext.class);
@@ -441,7 +361,7 @@ public class ValidateGrantTest extends BaseOIDCResponseActionTest {
     
     @Test(expectedExceptions = ComponentInitializationException.class)
     public void testNoRevocationCache() throws ComponentInitializationException, NoSuchAlgorithmException {
-        action = new ValidateGrant(getDataSealer());
+        action = new ValidateGrant();
         final StorageServiceReplayCache replayCache = new StorageServiceReplayCache();
         final MemoryStorageService storageService = new MemoryStorageService();
         storageService.setId("mockId");
@@ -453,7 +373,7 @@ public class ValidateGrantTest extends BaseOIDCResponseActionTest {
 
     @Test(expectedExceptions = ComponentInitializationException.class)
     public void testNoReplayCache() throws ComponentInitializationException, NoSuchAlgorithmException {
-        action = new ValidateGrant(getDataSealer());
+        action = new ValidateGrant();
         action.setRevocationCache(new MockRevocationCache(false, true));
         action.initialize();
     }

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


More information about the commits mailing list