[java-idp-oidc] branch main updated: JOIDC-254 - New method for manipulating token claims sets

Henri Mikkonen henri.mikkonen at iki.fi
Tue Sep 9 10:21:13 UTC 2025


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

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

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

The following commit(s) were added to refs/heads/main by this push:
     new c1f2ca17 JOIDC-254 - New method for manipulating token claims sets
c1f2ca17 is described below

commit c1f2ca17b95ad968014d4bd6ffc57eb6ae2b132e
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Tue Sep 9 13:20:54 2025 +0300

    JOIDC-254 - New method for manipulating token claims sets
    
    https://shibboleth.atlassian.net/browse/JOIDC-254
    
    The following new interfaces can be used for manipulating the claims sets:
    
    - AuthorizationCodeClaimsSetDecorator for authorization code claims sets (SetAuthorizationCodeToResponseContext, authorize flow)
    - AccessTokenClaimsSetDecorator for access token claims sets (BuildAccessToken, authorize and token flows)
    - IdTokenClaimsSetDecorator for ID token claims sets (ManipulateClaimsForIDToken, authorize and token flows)
    - RefreshTokenClaimsSetDecorator for refresh token claims sets (SetRefreshTokenToResponseContext, token flow)
    - RequestUriClaimsSetDecorator for pushed authorization requests (FormOutbounPushedAuthorizationResponseMessage, par flow)
    
    The interfaces extend BiConsumer<Map<String, Object>, ProfileRequestContext> and IdentifiedComponent. The map is a modifiable HashMap.
---
 .../op/profile/AccessTokenClaimsSetDecorator.java  | 35 ++++++++++
 .../AuthorizationCodeClaimsSetDecorator.java       | 35 ++++++++++
 .../oidc/op/profile/IdTokenClaimsSetDecorator.java | 35 ++++++++++
 .../op/profile/RefreshTokenClaimsSetDecorator.java | 35 ++++++++++
 .../op/profile/RequestUriClaimsSetDecorator.java   | 35 ++++++++++
 .../op/oauth2/profile/impl/BuildAccessToken.java   | 35 +++++++++-
 ...mOutbounPushedAuthorizationResponseMessage.java | 25 +++++++-
 .../SetAuthorizationCodeToResponseContext.java     | 42 ++++++++++--
 .../profile/impl/ManipulateClaimsForIDToken.java   | 63 ++++++++++++++----
 .../impl/SetRefreshTokenToResponseContext.java     | 31 ++++++++-
 .../oauth2/profile/impl/BuildAccessTokenTest.java  |  2 +-
 .../SetAuthorizationCodeToResponseContextTest.java |  2 +-
 .../oidc/op/profile/flow/AuthorizeFlowTest.java    | 51 ++++++++++++++-
 .../op/profile/flow/PushedAuthorizeFlowTest.java   |  2 +
 .../flow/TestAccessTokenClaimsSetDecorator.java    | 48 ++++++++++++++
 .../TestAuthorizationCodeClaimsSetDecorator.java   | 48 ++++++++++++++
 .../flow/TestIdTokenClaimsSetDecorator.java        | 48 ++++++++++++++
 .../flow/TestRefreshTokenClaimsSetDecorator.java   | 48 ++++++++++++++
 .../flow/TestRequestUriClaimsSetDecorator.java     | 48 ++++++++++++++
 .../plugin/oidc/op/profile/flow/TokenFlowTest.java | 74 ++++++++++++++++++++++
 .../impl/ManipulateClaimsForIDTokenTest.java       |  2 +-
 .../impl/SetRefreshTokenToResponseContextTest.java |  2 +-
 .../net/shibboleth/idp/module/conf/global.xml      |  5 ++
 23 files changed, 721 insertions(+), 30 deletions(-)

diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/AccessTokenClaimsSetDecorator.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/AccessTokenClaimsSetDecorator.java
new file mode 100644
index 00000000..898d91c7
--- /dev/null
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/AccessTokenClaimsSetDecorator.java
@@ -0,0 +1,35 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.profile;
+
+import java.util.Map;
+import java.util.function.BiConsumer;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import net.shibboleth.shared.component.IdentifiedComponent;
+
+/**
+ * An interface for decorating claims sets related to access tokens.
+ * 
+ * The interface extends the {@code BiConsumer} by specifying the input types explicitly and is expected to operate via
+ * side-effects.
+ * 
+ * @since 4.4.0
+ */
+public interface AccessTokenClaimsSetDecorator
+    extends BiConsumer<Map<String, Object>, ProfileRequestContext>, IdentifiedComponent {
+
+}
diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/AuthorizationCodeClaimsSetDecorator.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/AuthorizationCodeClaimsSetDecorator.java
new file mode 100644
index 00000000..f94f53a8
--- /dev/null
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/AuthorizationCodeClaimsSetDecorator.java
@@ -0,0 +1,35 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.profile;
+
+import java.util.Map;
+import java.util.function.BiConsumer;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import net.shibboleth.shared.component.IdentifiedComponent;
+
+/**
+ * An interface for decorating claims sets related to authorization codes.
+ * 
+ * The interface extends the {@code BiConsumer} by specifying the input types explicitly and is expected to operate via
+ * side-effects.
+ * 
+ * @since 4.4.0
+ */
+public interface AuthorizationCodeClaimsSetDecorator
+    extends BiConsumer<Map<String, Object>, ProfileRequestContext>, IdentifiedComponent {
+
+}
diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/IdTokenClaimsSetDecorator.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/IdTokenClaimsSetDecorator.java
new file mode 100644
index 00000000..933a04f1
--- /dev/null
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/IdTokenClaimsSetDecorator.java
@@ -0,0 +1,35 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.profile;
+
+import java.util.Map;
+import java.util.function.BiConsumer;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import net.shibboleth.shared.component.IdentifiedComponent;
+
+/**
+ * An interface for decorating claims sets related to ID tokens.
+ * 
+ * The interface extends the {@code BiConsumer} by specifying the input types explicitly and is expected to operate via
+ * side-effects.
+ * 
+ * @since 4.4.0
+ */
+public interface IdTokenClaimsSetDecorator
+    extends BiConsumer<Map<String, Object>, ProfileRequestContext>, IdentifiedComponent {
+
+}
diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/RefreshTokenClaimsSetDecorator.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/RefreshTokenClaimsSetDecorator.java
new file mode 100644
index 00000000..cb452001
--- /dev/null
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/RefreshTokenClaimsSetDecorator.java
@@ -0,0 +1,35 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.profile;
+
+import java.util.Map;
+import java.util.function.BiConsumer;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import net.shibboleth.shared.component.IdentifiedComponent;
+
+/**
+ * An interface for decorating claims sets related to refresh tokens.
+ * 
+ * The interface extends the {@code BiConsumer} by specifying the input types explicitly and is expected to operate via
+ * side-effects.
+ * 
+ * @since 4.4.0
+ */
+public interface RefreshTokenClaimsSetDecorator
+    extends BiConsumer<Map<String, Object>, ProfileRequestContext>, IdentifiedComponent {
+
+}
diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/RequestUriClaimsSetDecorator.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/RequestUriClaimsSetDecorator.java
new file mode 100644
index 00000000..23b9f893
--- /dev/null
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/RequestUriClaimsSetDecorator.java
@@ -0,0 +1,35 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.profile;
+
+import java.util.Map;
+import java.util.function.BiConsumer;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import net.shibboleth.shared.component.IdentifiedComponent;
+
+/**
+ * An interface for decorating claims sets related to pushed authorization requests.
+ * 
+ * The interface extends the {@code BiConsumer} by specifying the input types explicitly and is expected to operate via
+ * side-effects.
+ * 
+ * @since 4.4.0
+ */
+public interface RequestUriClaimsSetDecorator
+    extends BiConsumer<Map<String, Object>, ProfileRequestContext>, IdentifiedComponent {
+
+}
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..e55c9a54 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
@@ -17,6 +17,7 @@ package net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl;
 import java.text.ParseException;
 import java.time.Duration;
 import java.time.Instant;
+import java.util.Collection;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.function.BiFunction;
@@ -31,6 +32,7 @@ import org.opensaml.profile.context.ProfileRequestContext;
 import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
 import org.opensaml.profile.context.navigate.OutboundMessageContextLookup;
 import org.slf4j.Logger;
+import org.springframework.beans.factory.annotation.Autowired;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.nimbusds.jwt.JWT;
@@ -50,6 +52,7 @@ import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationRes
 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.messaging.context.navigate.TokenRequestClientIDLookupFunction;
+import net.shibboleth.idp.plugin.oidc.op.profile.AccessTokenClaimsSetDecorator;
 import net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.OIDCAuthenticationResponseContextLookupFunction;
 import net.shibboleth.idp.plugin.oidc.op.profile.impl.AbstractOIDCResponseAction;
 import net.shibboleth.idp.plugin.oidc.op.token.support.AccessTokenClaimsSet;
@@ -65,6 +68,7 @@ import net.shibboleth.oidc.profile.config.navigate.AccessTokenLifetimeLookupFunc
 import net.shibboleth.oidc.profile.config.navigate.AccessTokenTypeLookupFunction;
 import net.shibboleth.profile.context.navigate.IssuerLookupFunction;
 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.logic.FunctionSupport;
@@ -152,6 +156,9 @@ public class BuildAccessToken extends AbstractOIDCResponseAction {
     /** The strategy used for manipulating the token claims set. */
     @Nullable private BiFunction<ProfileRequestContext,Map<String,Object>,Map<String,Object>> manipulationStrategy;
 
+    /** Autowired claims set decorators. */
+    @Nonnull private Collection<AccessTokenClaimsSetDecorator> tokenClaimsSetDecorators;
+
     /** Object mapper used for pretty-printing JWT contents. */
     @NonnullAfterInit private ObjectMapper objectMapper;
 
@@ -176,8 +183,13 @@ public class BuildAccessToken extends AbstractOIDCResponseAction {
     /** The xmlSafe-flag passed to the identifier generator. */
     private boolean xmlSafeIdentifier;
 
-    /** Constructor. */
-    public BuildAccessToken() {
+    /**
+     * Constructor.
+     * 
+     * @param freeDecorators free-standing decorators to add
+     */
+    @Autowired
+    public BuildAccessToken(@Nullable final Collection<AccessTokenClaimsSetDecorator> freeDecorators) {
         accessTokenTypeLookupStrategy = new AccessTokenTypeLookupFunction();
         accessTokenLifetimeLookupStrategy = new AccessTokenLifetimeLookupFunction();
         issuerLookupStrategy = new IssuerLookupFunction();
@@ -213,6 +225,7 @@ public class BuildAccessToken extends AbstractOIDCResponseAction {
                 new AccessTokenClaimsSetManipulationStrategyLookupFunction();
         xmlSafeIdentifier = true;
         alwaysIssueBearerAccessTokenCondition = new AlwaysIssueBearerAccessTokenPredicate();
+        tokenClaimsSetDecorators = freeDecorators == null ? CollectionSupport.emptyList() : freeDecorators;
     }
     
     /**
@@ -595,6 +608,24 @@ public class BuildAccessToken extends AbstractOIDCResponseAction {
         }
         
         final AccessTokenClaimsSet claimsSet = builder.build();
+        final JWTClaimsSet jwtClaimsSet = claimsSet.getClaimsSet();
+        assert jwtClaimsSet != null;
+        final Map<String, Object> claimsMap = new HashMap<>(jwtClaimsSet.toJSONObject());
+
+        for (final AccessTokenClaimsSetDecorator decorator : tokenClaimsSetDecorators) {
+            log.debug("{} Applying decorator {}", getLogPrefix(), decorator);
+            decorator.accept(claimsMap, profileRequestContext);
+            try {
+                final JWTClaimsSet parsedClaimsSet = JWTClaimsSet.parse(claimsMap);
+                assert parsedClaimsSet != null;
+                claimsSet.setClaimsSet(parsedClaimsSet);
+            } catch (final ParseException e) {
+                log.error("{} The resulted claims set after decorator {} could not be parsed",
+                        getLogPrefix(), decorator.getId(), e);
+                ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_PROFILE_CONFIG);
+                return;
+            }
+        }
 
         if (manipulationStrategy != null) {
             final JWTClaimsSet acClaimsSet = claimsSet.getClaimsSet();
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/FormOutbounPushedAuthorizationResponseMessage.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/FormOutbounPushedAuthorizationResponseMessage.java
index 141cbe11..c66614a9 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/FormOutbounPushedAuthorizationResponseMessage.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/FormOutbounPushedAuthorizationResponseMessage.java
@@ -17,6 +17,8 @@ package net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl;
 import java.net.URI;
 import java.text.ParseException;
 import java.time.Duration;
+import java.util.Collection;
+import java.util.HashMap;
 import java.util.Map;
 import java.util.function.BiFunction;
 import java.util.function.Function;
@@ -28,12 +30,14 @@ import org.opensaml.profile.action.ActionSupport;
 import org.opensaml.profile.action.EventIds;
 import org.opensaml.profile.context.ProfileRequestContext;
 import org.slf4j.Logger;
+import org.springframework.beans.factory.annotation.Autowired;
 
 import com.nimbusds.jwt.JWT;
 import com.nimbusds.oauth2.sdk.PushedAuthorizationRequest;
 import com.nimbusds.oauth2.sdk.PushedAuthorizationSuccessResponse;
 
 import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext;
+import net.shibboleth.idp.plugin.oidc.op.profile.RequestUriClaimsSetDecorator;
 import net.shibboleth.idp.profile.IdPEventIds;
 import net.shibboleth.oidc.profile.config.navigate.PushedAuthorizationRequestUriClaimsSetManipulationStrategyLookupFunction;
 import net.shibboleth.oidc.profile.config.navigate.PushedAuthorizationRequestUriLifetimeLookupFunction;
@@ -67,6 +71,9 @@ public class FormOutbounPushedAuthorizationResponseMessage extends AbstractOAuth
     @Nonnull private Map<String, BiFunction<ProfileRequestContext,Map<String,Object>,URI>>
         requestUriClaimsSetSerializationStrategies;
 
+    /** The auto-wired decorators for request URI claims set. */
+    @Nonnull private Collection<RequestUriClaimsSetDecorator> requestUriClaimsSetDecorators;
+
     /** The request URI type to use. */
     @Nullable private String requestUriType;
 
@@ -79,15 +86,23 @@ public class FormOutbounPushedAuthorizationResponseMessage extends AbstractOAuth
     /** The request message to operate on. */
     @Nullable private PushedAuthorizationRequest requestMessage;
 
+
     /**
+     * 
      * Constructor.
+     *
+     * @param decorators free-standing decorators to add
      */
-    public FormOutbounPushedAuthorizationResponseMessage() {
+    @Autowired
+    public FormOutbounPushedAuthorizationResponseMessage(
+            @Nullable final Collection<RequestUriClaimsSetDecorator> decorators) {
         requestUriTypeLookupStrategy = new PushedAuthorizationRequestUriTypeLookupFunction();
         requestUriLifetimeLookupStrategy = new PushedAuthorizationRequestUriLifetimeLookupFunction();
         requestUriClaimsSetManipulationStrategyLookupStrategy =
                 new PushedAuthorizationRequestUriClaimsSetManipulationStrategyLookupFunction();
         requestUriClaimsSetSerializationStrategies = CollectionSupport.emptyMap();
+        requestUriClaimsSetDecorators =
+                decorators != null ? CollectionSupport.copyToList(decorators) : CollectionSupport.emptyList();
     }
 
     /**
@@ -189,9 +204,13 @@ public class FormOutbounPushedAuthorizationResponseMessage extends AbstractOAuth
         final JWT requestObject = oidcContext.getRequestObject();
         if (requestObject == null) {
             assert requestMessage != null;
-            claimsSet = requestMessage.getAuthorizationRequest().toJWTClaimsSet().getClaims();
+            claimsSet = new HashMap<>(requestMessage.getAuthorizationRequest().toJWTClaimsSet().getClaims());
         } else {
-            claimsSet = requestObject.getJWTClaimsSet().getClaims();
+            claimsSet = new HashMap<>(requestObject.getJWTClaimsSet().getClaims());
+        }
+
+        for (final RequestUriClaimsSetDecorator decorator : requestUriClaimsSetDecorators) {
+            decorator.accept(claimsSet, profileRequestContext);
         }
 
         if (manipulationStrategy != null) {
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..55b41353 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,8 @@ import java.net.URI;
 import java.text.ParseException;
 import java.time.Duration;
 import java.time.Instant;
+import java.util.Collection;
+import java.util.HashMap;
 import java.util.Map;
 import java.util.function.BiFunction;
 import java.util.function.Function;
@@ -28,6 +30,7 @@ import javax.annotation.Nullable;
 import org.opensaml.profile.action.EventIds;
 import org.opensaml.profile.context.ProfileRequestContext;
 import org.slf4j.Logger;
+import org.springframework.beans.factory.annotation.Autowired;
 
 import com.nimbusds.jwt.JWTClaimsSet;
 import com.nimbusds.oauth2.sdk.AuthorizationRequest;
@@ -40,6 +43,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.profile.AuthorizationCodeClaimsSetDecorator;
 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;
@@ -48,6 +52,7 @@ import net.shibboleth.oidc.profile.config.navigate.AuthorizationCodeClaimsSetMan
 import net.shibboleth.oidc.profile.config.navigate.AuthzCodeLifetimeLookupFunction;
 import net.shibboleth.profile.context.navigate.IssuerLookupFunction;
 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.logic.FunctionSupport;
@@ -116,6 +121,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;
 
+    /** Autowired claims set decorators. */
+    @Nonnull private Collection<AuthorizationCodeClaimsSetDecorator> tokenClaimsSetDecorators;
+
     /** Subject context. */
     @Nullable private SubjectContext subjectCtx;
 
@@ -130,8 +138,12 @@ public class SetAuthorizationCodeToResponseContext extends AbstractOAuthAuthoriz
 
     /**
      * Constructor.
+     *
+     * @param freeDecorators free-standing decorators to add
      */
-    public SetAuthorizationCodeToResponseContext() {
+    @Autowired
+    public SetAuthorizationCodeToResponseContext(
+            @Nullable final Collection<AuthorizationCodeClaimsSetDecorator> freeDecorators) {
         final Function<ProfileRequestContext, OIDCAuthenticationResponseTokenClaimsContext> tccls =
                 new ChildContextLookup<>(OIDCAuthenticationResponseTokenClaimsContext.class).compose(
                         new OIDCAuthenticationResponseContextLookupFunction());
@@ -151,6 +163,7 @@ public class SetAuthorizationCodeToResponseContext extends AbstractOAuthAuthoriz
         tokenClaimsSetManipulationStrategyLookupStrategy =
                 new AuthorizationCodeClaimsSetManipulationStrategyLookupFunction();
         xmlSafeIdentifier = true;
+        tokenClaimsSetDecorators = freeDecorators == null ? CollectionSupport.emptyList() : freeDecorators;
     }
 
     /**
@@ -429,14 +442,29 @@ public class SetAuthorizationCodeToResponseContext extends AbstractOAuthAuthoriz
                 .setDpopProofJwkThumbprint(responseCtx.getDpopProofJwkThumbprint())
                 .build();
 
+        final JWTClaimsSet jwtClaimsSet = claimsSet.getClaimsSet();
+        assert jwtClaimsSet != null;
+        final Map<String, Object> claimsMap = new HashMap<>(jwtClaimsSet.toJSONObject());
+
+        for (final AuthorizationCodeClaimsSetDecorator decorator : tokenClaimsSetDecorators) {
+            log.debug("{} Applying decorator {}", getLogPrefix(), decorator);
+            decorator.accept(claimsMap, profileRequestContext);
+            try {
+                final JWTClaimsSet parsedClaimsSet = JWTClaimsSet.parse(claimsMap);
+                assert parsedClaimsSet != null;
+                claimsSet.setClaimsSet(parsedClaimsSet);
+            } catch (final ParseException e) {
+                log.error("{} The resulted claims set after decorator {} could not be parsed",
+                        getLogPrefix(), decorator.getId(), e);
+                ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_PROFILE_CONFIG);
+                return;
+            }
+        }
+
         if (manipulationStrategy != null) {
-            final JWTClaimsSet jwtClaimsSet = claimsSet.getClaimsSet();
-            assert jwtClaimsSet != null;
             log.debug("{} Manipulation strategy has been set, applying it to the claims set {}", getLogPrefix(),
                     claimsSet.serialize());
-            assert manipulationStrategy != null;
-            final Map<String, Object> result = manipulationStrategy.apply(profileRequestContext,
-                    jwtClaimsSet.toJSONObject());
+            final Map<String, Object> result = manipulationStrategy.apply(profileRequestContext, claimsMap);
             if (result == null) {
                 log.debug("{} Manipulation strategy returned null, leaving token claims set untouched.",
                         getLogPrefix());
@@ -447,7 +475,7 @@ public class SetAuthorizationCodeToResponseContext extends AbstractOAuthAuthoriz
                     assert parsedClaimsSet != null;
                     claimsSet.setClaimsSet(parsedClaimsSet);
                 } catch (final ParseException e) {
-                    log.error("{} The resulted claims set could not be transformed into ", getLogPrefix(), e);
+                    log.error("{} The resulted claims set after manipulation could not be parsed ", getLogPrefix(), e);
                     ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_PROFILE_CONFIG);
                     return;
                 }
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ManipulateClaimsForIDToken.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ManipulateClaimsForIDToken.java
index 29dd5cd1..e71d4422 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ManipulateClaimsForIDToken.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ManipulateClaimsForIDToken.java
@@ -15,17 +15,21 @@
 package net.shibboleth.idp.plugin.oidc.op.profile.impl;
 
 import java.text.ParseException;
+import java.util.Collection;
+import java.util.HashMap;
 import java.util.Map;
 import java.util.function.BiFunction;
 import java.util.function.Function;
 
 import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
 
 import org.opensaml.messaging.encoder.AbstractMessageEncoder;
 import org.opensaml.profile.action.ActionSupport;
 import org.opensaml.profile.action.EventIds;
 import org.opensaml.profile.context.ProfileRequestContext;
 import org.slf4j.Logger;
+import org.springframework.beans.factory.annotation.Autowired;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.nimbusds.jwt.JWTClaimsSet;
@@ -33,9 +37,12 @@ import com.nimbusds.openid.connect.sdk.claims.IDTokenClaimsSet;
 
 import net.shibboleth.idp.plugin.oidc.op.encoding.impl.ResponseUtil;
 import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext;
+import net.shibboleth.idp.plugin.oidc.op.profile.IdTokenClaimsSetDecorator;
 import net.shibboleth.idp.profile.IdPEventIds;
 import net.shibboleth.oidc.profile.config.navigate.IDTokenManipulationStrategyLookupFunction;
 import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.collection.CollectionSupport;
 import net.shibboleth.shared.component.ComponentInitializationException;
 import net.shibboleth.shared.logic.Constraint;
 import net.shibboleth.shared.primitive.LoggerFactory;
@@ -70,14 +77,21 @@ public class ManipulateClaimsForIDToken extends AbstractOIDCAuthenticationRespon
     /** Object mapper used for pretty-printing JWT contents. */
     @NonnullAfterInit private ObjectMapper objectMapper;
 
+    /** Autowired claims set decorators. */
+    @Nonnull private Collection<IdTokenClaimsSetDecorator> tokenClaimsSetDecorators;
+
     /** The id_token to operate on. */
-    private IDTokenClaimsSet idToken;
+    @NonnullBeforeExec private IDTokenClaimsSet idToken;
 
     /**
      * Constructor.
+     *
+     * @param freeDecorators free-standing decorators to add
      */
-    public ManipulateClaimsForIDToken() {
+    @Autowired
+    public ManipulateClaimsForIDToken(@Nullable final Collection<IdTokenClaimsSetDecorator> freeDecorators) {
         idTokenManipulationStrategyLookupStrategy = new IDTokenManipulationStrategyLookupFunction();
+        tokenClaimsSetDecorators = freeDecorators == null ? CollectionSupport.emptyList() : freeDecorators;
     }
 
     /**
@@ -85,6 +99,12 @@ public class ManipulateClaimsForIDToken extends AbstractOIDCAuthenticationRespon
      * 
      * @param strategy What to set
      */
+
+    /**
+     * Set the lookup function to supply strategy bi-function for manipulating id_token claims set.
+     * 
+     * @param strategy What to set
+     */
     public void setIDTokenManipulationStrategyLookupStrategy(@Nonnull final
             Function<ProfileRequestContext,BiFunction<ProfileRequestContext,Map<String,Object>,Map<String,Object>>>
             strategy) {
@@ -132,12 +152,6 @@ public class ManipulateClaimsForIDToken extends AbstractOIDCAuthenticationRespon
             return false;
         }
         manipulationStrategy = idTokenManipulationStrategyLookupStrategy.apply(profileRequestContext);
-        if (manipulationStrategy == null) {
-            log.debug("{} No manipulation strategy resolved, nothing to do.", getLogPrefix());
-            assert idToken != null;
-            doProtocolLog(profileRequestContext, idToken);
-            return false;
-        }
     
         return true;
     }
@@ -145,10 +159,37 @@ public class ManipulateClaimsForIDToken extends AbstractOIDCAuthenticationRespon
     /** {@inheritDoc} */
     @Override
     protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
-        final Map<String, Object> result = manipulationStrategy.apply(profileRequestContext, idToken.toJSONObject());
+        final Map<String, Object> claimsMap = new HashMap<>(idToken.toJSONObject());
+
+        if (!tokenClaimsSetDecorators.isEmpty()) {
+            for (final IdTokenClaimsSetDecorator decorator : tokenClaimsSetDecorators) {
+                log.debug("{} Applying decorator {}", getLogPrefix(), decorator);
+                decorator.accept(claimsMap, profileRequestContext);
+                try {
+                    final JWTClaimsSet parsedClaimsSet = JWTClaimsSet.parse(claimsMap);
+                    assert parsedClaimsSet != null;
+                    idToken = new IDTokenClaimsSet(JWTClaimsSet.parse(claimsMap));
+                } catch (final ParseException | com.nimbusds.oauth2.sdk.ParseException e) {
+                    log.error("{} The resulted claims set after decorator {} could not be parsed",
+                            getLogPrefix(), decorator.getId(), e);
+                    ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_PROFILE_CONFIG);
+                    return;
+                }
+            }
+
+            final OIDCAuthenticationResponseContext oidcResponseContext = getOidcResponseContext();
+            assert oidcResponseContext != null;
+            oidcResponseContext.setIDToken(idToken);
+        }
+        if (manipulationStrategy == null) {
+            log.debug("{} No manipulation strategy resolved, nothing to do.", getLogPrefix());
+            doProtocolLog(profileRequestContext, idToken);
+            return;
+        }
+
+        final Map<String, Object> result = manipulationStrategy.apply(profileRequestContext, claimsMap);
         if (result == null) {
-            log.debug("{} Manipulation strategy retruned null, leaving id_token claims untouched.", getLogPrefix());
-            assert idToken != null;
+            log.debug("{} Manipulation strategy returned null, leaving id_token claims untouched.", getLogPrefix());
             doProtocolLog(profileRequestContext, idToken);
             return;
         }
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..796fff20 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
@@ -17,6 +17,8 @@ package net.shibboleth.idp.plugin.oidc.op.profile.impl;
 import java.text.ParseException;
 import java.time.Duration;
 import java.time.Instant;
+import java.util.Collection;
+import java.util.HashMap;
 import java.util.Map;
 import java.util.function.BiFunction;
 import java.util.function.Function;
@@ -29,10 +31,12 @@ import org.opensaml.profile.action.EventIds;
 import org.opensaml.profile.context.ProfileRequestContext;
 import org.opensaml.storage.RevocationCache;
 import org.slf4j.Logger;
+import org.springframework.beans.factory.annotation.Autowired;
 
 import com.nimbusds.jwt.JWTClaimsSet;
 
 import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext;
+import net.shibboleth.idp.plugin.oidc.op.profile.RefreshTokenClaimsSetDecorator;
 import net.shibboleth.idp.plugin.oidc.op.profile.logic.DefaultTokenRevocationLifetimeLookupStrategy;
 import net.shibboleth.idp.plugin.oidc.op.storage.RevocationCacheContexts;
 import net.shibboleth.idp.plugin.oidc.op.token.support.AuthorizeCodeClaimsSet;
@@ -90,6 +94,9 @@ public class SetRefreshTokenToResponseContext extends AbstractOIDCResponseAction
     /** The strategy used for manipulating the token claims set. */
     @Nullable private BiFunction<ProfileRequestContext,Map<String,Object>,Map<String,Object>> manipulationStrategy;
 
+    /** Autowired claims set decorators. */
+    @Nonnull private Collection<RefreshTokenClaimsSetDecorator> tokenClaimsSetDecorators;
+
     /** Strategy used to locate the {@link IdentifierGenerationStrategy} to use. */
     @Nonnull private Function<ProfileRequestContext,IdentifierGenerationStrategy> idGeneratorLookupStrategy;
 
@@ -128,8 +135,11 @@ public class SetRefreshTokenToResponseContext extends AbstractOIDCResponseAction
      * Constructor.
      * 
      * @param sealer sealer to encrypt/hmac refresh token.
+     * @param freeDecorators free-standing decorators to add
      */
-    public SetRefreshTokenToResponseContext(@Nonnull @ParameterName(name = "sealer") final DataSealer sealer) {
+    @Autowired
+    public SetRefreshTokenToResponseContext(@Nonnull @ParameterName(name = "sealer") final DataSealer sealer,
+            @Nullable final Collection<RefreshTokenClaimsSetDecorator> freeDecorators) {
         refreshTokenChainLifetimeLookupStrategy = new RefreshTokenChainLifetimeLookupFunction();
         refreshTokenTimeoutLookupStrategy = new RefreshTokenTimeoutLookupFunction();
         dataSealer = Constraint.isNotNull(sealer, "DataSealer cannot be null");
@@ -141,6 +151,7 @@ public class SetRefreshTokenToResponseContext extends AbstractOIDCResponseAction
         refreshTokenTypeLookupStrategy = new RefreshTokenTypeLookupFunction();
         refreshTokenSerializationStrategies = CollectionSupport.emptyMap();
         xmlSafeIdentifier = true;
+        tokenClaimsSetDecorators = freeDecorators == null ? CollectionSupport.emptyList() : freeDecorators;
     }
 
     /**
@@ -347,6 +358,24 @@ public class SetRefreshTokenToResponseContext extends AbstractOIDCResponseAction
         builder.setRootTokenIdentifier(rootTokenId);
         builder.setDpopProofJwkThumbprint(oidcResponseContext.getDpopProofJwkThumbprint());
         final RefreshTokenClaimsSet claimsSet = builder.build();
+        final JWTClaimsSet jwtClaimsSet = claimsSet.getClaimsSet();
+        assert jwtClaimsSet != null;
+        final Map<String, Object> claimsMap = new HashMap<>(jwtClaimsSet.toJSONObject());
+
+        for (final RefreshTokenClaimsSetDecorator decorator : tokenClaimsSetDecorators) {
+            log.debug("{} Applying decorator {}", getLogPrefix(), decorator);
+            decorator.accept(claimsMap, profileRequestContext);
+            try {
+                final JWTClaimsSet parsedClaimsSet = JWTClaimsSet.parse(claimsMap);
+                assert parsedClaimsSet != null;
+                claimsSet.setClaimsSet(parsedClaimsSet);
+            } catch (final ParseException e) {
+                log.error("{} The resulted claims set after decorator {} could not be parsed",
+                        getLogPrefix(), decorator.getId(), e);
+                ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_PROFILE_CONFIG);
+                return;
+            }
+        }
         
         if (manipulationStrategy != null) {
             log.debug("{} Manipulation strategy has been set, applying it to the claims set {}", getLogPrefix(),
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/BuildAccessTokenTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/BuildAccessTokenTest.java
index 277a57f7..4a2d0313 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/BuildAccessTokenTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/BuildAccessTokenTest.java
@@ -77,7 +77,7 @@ public class BuildAccessTokenTest extends BaseOIDCResponseActionTest {
         respCtx.setScope(new Scope());
         respCtx.getAudience().add("https://rp.example.org");
         
-        action = new BuildAccessToken();
+        action = new BuildAccessToken(null);
         action.setObjectMapper(new ObjectMapper());
     }
 
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/SetAuthorizationCodeToResponseContextTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/SetAuthorizationCodeToResponseContextTest.java
index 2bc9f523..8cdeb2db 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/SetAuthorizationCodeToResponseContextTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/SetAuthorizationCodeToResponseContextTest.java
@@ -73,7 +73,7 @@ public class SetAuthorizationCodeToResponseContextTest extends BaseOIDCResponseA
         respCtx.setAuthTime(Instant.now());
         respCtx.setAcr("0");
         respCtx.setRedirectURI(new URI("http://example.com"));
-        action = new SetAuthorizationCodeToResponseContext();
+        action = new SetAuthorizationCodeToResponseContext(null);
         final DataSealer dataSealer = getDataSealer();
         assert dataSealer != null;
         action.setDataSealer(dataSealer);
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AuthorizeFlowTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AuthorizeFlowTest.java
index 72fdc256..45fcdd87 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AuthorizeFlowTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AuthorizeFlowTest.java
@@ -130,6 +130,7 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
         Assert.assertNotNull(successResponse.getAuthorizationCode());
         Assert.assertNotNull(getSidFromAuthorizeCodeClaimsSet(successResponse));
         Assert.assertNull(successResponse.getIssuer());
+        assertDecoratedAuthorizeCodeClaimsSet(successResponse);
     }
 
     @Test
@@ -149,6 +150,7 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
         Assert.assertNull(successResponse.getAccessToken());
         Assert.assertNotNull(successResponse.getAuthorizationCode());
         Assert.assertNull(successResponse.getIssuer());
+        assertDecoratedAuthorizeCodeClaimsSet(successResponse);
     }
 
     @Test
@@ -169,6 +171,7 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
         Assert.assertNull(successResponse.getAccessToken());
         Assert.assertNotNull(successResponse.getAuthorizationCode());
         Assert.assertNull(successResponse.getIssuer());
+        assertDecoratedAuthorizeCodeClaimsSet(successResponse);
     }
 
     @Test
@@ -189,6 +192,7 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
         Assert.assertNull(successResponse.getAccessToken());
         Assert.assertNotNull(successResponse.getAuthorizationCode());
         Assert.assertNull(successResponse.getIssuer());
+        assertDecoratedAuthorizeCodeClaimsSet(successResponse);
     }
 
     @Test
@@ -209,6 +213,7 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
         Assert.assertNull(successResponse.getAccessToken());
         Assert.assertNotNull(successResponse.getAuthorizationCode());
         Assert.assertNull(successResponse.getIssuer());
+        assertDecoratedAuthorizeCodeClaimsSet(successResponse);
     }
 
     @Test
@@ -333,6 +338,7 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
         Assert.assertNotNull(successResponse.getAuthorizationCode());
         Assert.assertNotNull(getSidFromAuthorizeCodeClaimsSet(successResponse));
         Assert.assertNull(successResponse.getIssuer());
+        assertDecoratedAuthorizeCodeClaimsSet(successResponse);
     }
 
     @Test
@@ -734,6 +740,7 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
         Assert.assertEquals(successResponse.getRedirectionURI().toString(), redirectUri);
         Assert.assertNotNull(successResponse.getIDToken());
         Assert.assertNotNull(getSidFromIDToken(successResponse));
+        assertDecoratedIDToken(successResponse);
         Assert.assertNull(successResponse.getAccessToken());
         Assert.assertNull(successResponse.getAuthorizationCode());
         Assert.assertNull(successResponse.getIssuer());
@@ -793,6 +800,7 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
         Assert.assertEquals(successResponse.getRedirectionURI().toString(), redirectUri);
         Assert.assertNotNull(successResponse.getIDToken());
         Assert.assertNotNull(getSidFromIDToken(successResponse));
+        assertDecoratedIDToken(successResponse);
         Assert.assertNull(successResponse.getAccessToken());
         Assert.assertNull(successResponse.getAuthorizationCode());
         Assert.assertNull(successResponse.getIssuer());
@@ -834,6 +842,7 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
         Assert.assertNull(successResponse.getIDToken().getJWTClaimsSet().getClaim("c_hash"));
         Assert.assertNull(successResponse.getIDToken().getJWTClaimsSet().getClaim("at_hash"));
         Assert.assertNotNull(getSidFromIDToken(successResponse));
+        assertDecoratedIDToken(successResponse);
         Assert.assertNull(successResponse.getAccessToken());
         Assert.assertNull(successResponse.getAuthorizationCode());
         Assert.assertNotNull(successResponse.getIssuer());
@@ -859,6 +868,7 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
         Assert.assertEquals(successResponse.getRedirectionURI().toString(), redirectUri);
         Assert.assertNotNull(successResponse.getIDToken());
         Assert.assertNotNull(getSidFromIDToken(successResponse));
+        assertDecoratedIDToken(successResponse);
         Assert.assertNull(successResponse.getAccessToken());
         Assert.assertNull(successResponse.getAuthorizationCode());
         Assert.assertNull(successResponse.getIssuer());
@@ -883,6 +893,7 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
         Assert.assertEquals(successResponse.getRedirectionURI().toString(), redirectUri);
         Assert.assertNotNull(successResponse.getIDToken());
         Assert.assertNotNull(getSidFromIDToken(successResponse));
+        assertDecoratedIDToken(successResponse);
         Assert.assertNull(successResponse.getAccessToken());
         Assert.assertNull(successResponse.getAuthorizationCode());
         Assert.assertNull(successResponse.getIssuer());
@@ -910,6 +921,7 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
         final String sid1 = getSidFromIDToken(successResponse);
         Assert.assertNotNull(sid1);
         Assert.assertNotNull(successResponse.getAccessToken());
+        assertDecoratedIDToken(successResponse);
         final String sid2 = getSidFromOpaqueAccessTokenClaimsSet(successResponse);
         Assert.assertEquals(sid1, sid2);
         Assert.assertNull(successResponse.getAuthorizationCode());
@@ -920,6 +932,8 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
         final JWTClaimsSet claimsSet = token.getClaimsSet();
         assert claimsSet != null;
         Assert.assertNull(claimsSet.getStringClaim("eduPersonScopedAffiliation"));
+        Assert.assertEquals(claimsSet.getClaim(TestAccessTokenClaimsSetDecorator.KEY),
+                TestAccessTokenClaimsSetDecorator.VALUE);
         Assert.assertNull(successResponse.getIssuer());
     }
 
@@ -956,6 +970,7 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
         final AuthenticationSuccessResponse successResponse = responseMessage.toSuccessResponse();
         Assert.assertEquals(successResponse.getRedirectionURI().toString(), redirectUri);
         Assert.assertNotNull(successResponse.getIDToken());
+        assertDecoratedIDToken(successResponse);
         final String sid1 = getSidFromIDToken(successResponse);
         Assert.assertNotNull(sid1);
         Assert.assertNotNull(successResponse.getAccessToken());
@@ -991,6 +1006,7 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
         final AuthenticationSuccessResponse successResponse = responseMessage.toSuccessResponse();
         Assert.assertEquals(successResponse.getRedirectionURI().toString(), redirectUri);
         Assert.assertNotNull(successResponse.getIDToken());
+        assertDecoratedIDToken(successResponse);
         final String sid1 = getSidFromIDToken(successResponse);
         Assert.assertNotNull(sid1);
         Assert.assertNotNull(successResponse.getAccessToken());
@@ -1023,6 +1039,7 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
         final AuthenticationSuccessResponse successResponse = responseMessage.toSuccessResponse();
         Assert.assertEquals(successResponse.getRedirectionURI().toString(), redirectUri);
         Assert.assertNotNull(successResponse.getIDToken());
+        assertDecoratedIDToken(successResponse);
         final String sid1 = getSidFromIDToken(successResponse);
         Assert.assertNotNull(sid1);
         Assert.assertNotNull(successResponse.getAccessToken());
@@ -1100,7 +1117,7 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
     }
 
     @Test
-    public void testWithHybridIdTokenFlow() throws IOException, SessionException, ParseException {
+    public void testWithHybridIdTokenFlow() throws IOException, SessionException, ParseException, DataSealerException {
         request.setMethod("GET");
         setRequestParameters(List.of(new Pair<>("client_id", "mockClientId"),
                 new Pair<>("response_type", "code id_token"),
@@ -1120,8 +1137,10 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
         Assert.assertNull(successResponse.getIDToken().getJWTClaimsSet().getClaim("at_hash"));
         final String sid1 = getSidFromIDToken(successResponse);
         Assert.assertNotNull(sid1);
+        assertDecoratedIDToken(successResponse);
         Assert.assertNull(successResponse.getAccessToken());
         Assert.assertNotNull(successResponse.getAuthorizationCode());
+        assertDecoratedAuthorizeCodeClaimsSet(successResponse);
         final String sid2 = getSidFromAuthorizeCodeClaimsSet(successResponse);
         Assert.assertEquals(sid1, sid2);
         Assert.assertNull(successResponse.getIssuer());
@@ -1160,6 +1179,7 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
         final AuthenticationSuccessResponse successResponse = responseMessage.toSuccessResponse();
         Assert.assertEquals(successResponse.getRedirectionURI().toString(), redirectUri);
         Assert.assertNotNull(successResponse.getIDToken());
+        assertDecoratedIDToken(successResponse);
         final String sid1 = getSidFromIDToken(successResponse);
         Assert.assertNotNull(sid1);
         Assert.assertNull(successResponse.getAccessToken());
@@ -1217,6 +1237,7 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
         final AuthenticationResponse responseMessage = parseSuccessResponse(result, AuthenticationResponse.class);
         final AuthenticationSuccessResponse successResponse = responseMessage.toSuccessResponse();
         Assert.assertEquals(successResponse.getRedirectionURI().toString(), redirectUri);
+        assertDecoratedIDToken(successResponse);
         final String sid1 = getSidFromIDToken(successResponse);
         Assert.assertNotNull(sid1);
         Assert.assertNull(successResponse.getAccessToken());
@@ -1502,6 +1523,7 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
         final String sid3 = getSidFromIDToken(successResponse);
         Assert.assertEquals(sid1, sid2);
         Assert.assertEquals(sid2, sid3);
+        assertDecoratedIDToken(successResponse);
 
         final IDTokenClaimsSet idToken = new IDTokenClaimsSet(successResponse.getIDToken().getJWTClaimsSet());
         Assert.assertNotNull(idToken);
@@ -1546,6 +1568,7 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
         final String sid3 = getSidFromIDToken(successResponse);
         Assert.assertEquals(sid1, sid2);
         Assert.assertEquals(sid2, sid3);
+        assertDecoratedIDToken(successResponse);
 
         final JWTClaimsSet token =  SignedJWT.parse(successResponse.getAccessToken().getValue()).getJWTClaimsSet();
         Assert.assertEquals(token.getAudience(), List.of(resource, issuer));
@@ -1580,6 +1603,7 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
         final String sid3 = getSidFromIDToken(successResponse);
         Assert.assertEquals(sid1, sid2);
         Assert.assertEquals(sid2, sid3);
+        assertDecoratedIDToken(successResponse);
 
         final JWTClaimsSet token =  SignedJWT.parse(successResponse.getAccessToken().getValue()).getJWTClaimsSet();
         Assert.assertEquals(token.getAudience(), List.of(resourceNonUri, issuer));
@@ -2728,6 +2752,18 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
         }
     }
 
+    protected void assertDecoratedAuthorizeCodeClaimsSet(final AuthorizationSuccessResponse successResponse) {
+        Assert.assertNotNull(successResponse.getAuthorizationCode());
+        final AuthorizeCodeClaimsSet claims;
+        try {
+            claims = AuthorizeCodeClaimsSet.parse(successResponse.getAuthorizationCode().getValue(), getDataSealer());
+            Assert.assertEquals(claims.getClaimsSet().getClaim(TestAuthorizationCodeClaimsSetDecorator.KEY),
+                    TestAuthorizationCodeClaimsSetDecorator.VALUE);
+        } catch (ParseException | DataSealerException e) {
+            Assert.fail();
+        }
+    }
+
     protected String getSidFromOpaqueAccessTokenClaimsSet(final AuthenticationSuccessResponse successResponse) {
         Assert.assertNotNull(successResponse.getAccessToken());
         final AccessTokenClaimsSet claims;
@@ -2761,7 +2797,18 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
             return null;
         }
     }
-    
+
+    protected void assertDecoratedIDToken(final AuthenticationSuccessResponse successResponse) {
+        final JWT idToken = successResponse.getIDToken();
+        Assert.assertNotNull(idToken);
+        try {
+            Assert.assertEquals(idToken.getJWTClaimsSet().getStringClaim(TestIdTokenClaimsSetDecorator.KEY),
+                    TestIdTokenClaimsSetDecorator.VALUE);
+        } catch (ParseException e) {
+            Assert.fail();
+        }
+    }
+
     protected void setRequestParameters(final List<Pair<String, String>> pairs) {
         setRequestParameters(request, pairs);
     }
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/PushedAuthorizeFlowTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/PushedAuthorizeFlowTest.java
index 94d7d189..f95b62af 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/PushedAuthorizeFlowTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/PushedAuthorizeFlowTest.java
@@ -580,6 +580,7 @@ public class PushedAuthorizeFlowTest extends AbstractOidcClientAuthenticationFlo
         Assert.assertNull(object.get("scope"));
         Assert.assertNull(object.get("custom1"));
         Assert.assertEquals(object.get("custom2"), "custom2Value");
+        Assert.assertEquals(object.get(TestRequestUriClaimsSetDecorator.KEY), TestRequestUriClaimsSetDecorator.VALUE);
     }
 
     @SuppressWarnings("null")
@@ -613,6 +614,7 @@ public class PushedAuthorizeFlowTest extends AbstractOidcClientAuthenticationFlo
         Assert.assertEquals(object.get("scope"), "openid profile");
         Assert.assertNull(object.get("custom1"));
         Assert.assertEquals(object.get("custom2"), "custom2Value");
+        Assert.assertEquals(object.get(TestRequestUriClaimsSetDecorator.KEY), TestRequestUriClaimsSetDecorator.VALUE);
     }
 
     @Test
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/TestAccessTokenClaimsSetDecorator.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/TestAccessTokenClaimsSetDecorator.java
new file mode 100644
index 00000000..95744af7
--- /dev/null
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/TestAccessTokenClaimsSetDecorator.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.profile.flow;
+
+import java.util.Map;
+
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import net.shibboleth.idp.plugin.oidc.op.profile.AccessTokenClaimsSetDecorator;
+
+/**
+ * Test implementation for {@link AccessTokenClaimsSetDecorator}.
+ */
+public class TestAccessTokenClaimsSetDecorator implements AccessTokenClaimsSetDecorator {
+
+    public static final String KEY = "customAccessTokenKey";
+    public static final String VALUE = "customAccessTokenValue";
+
+    /** {@inheritDoc} */
+    @Override
+    public void accept(final @Nullable Map<String, Object> claimsSet,
+            final @Nullable ProfileRequestContext profileRequestContext) {
+        if (claimsSet != null && !claimsSet.containsKey(KEY)) {
+            claimsSet.put(KEY, VALUE);
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String getId() {
+        return "testAccessTokenClaimsSetDecorator";
+    }
+
+}
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/TestAuthorizationCodeClaimsSetDecorator.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/TestAuthorizationCodeClaimsSetDecorator.java
new file mode 100644
index 00000000..a7ba2850
--- /dev/null
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/TestAuthorizationCodeClaimsSetDecorator.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.profile.flow;
+
+import java.util.Map;
+
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import net.shibboleth.idp.plugin.oidc.op.profile.AuthorizationCodeClaimsSetDecorator;
+
+/**
+ * Test implementation for {@link AuthorizationCodeClaimsSetDecorator}.
+ */
+public class TestAuthorizationCodeClaimsSetDecorator implements AuthorizationCodeClaimsSetDecorator {
+
+    public static final String KEY = "customAuthorizationCodeKey";
+    public static final String VALUE = "customAuthorizationCodeValue";
+
+    /** {@inheritDoc} */
+    @Override
+    public void accept(final @Nullable Map<String, Object> claimsSet,
+            final @Nullable ProfileRequestContext profileRequestContext) {
+        if (claimsSet != null && !claimsSet.containsKey(KEY)) {
+            claimsSet.put(KEY, VALUE);
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String getId() {
+        return "testAuthorizationCodeClaimsSetDecorator";
+    }
+
+}
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/TestIdTokenClaimsSetDecorator.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/TestIdTokenClaimsSetDecorator.java
new file mode 100644
index 00000000..095a549c
--- /dev/null
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/TestIdTokenClaimsSetDecorator.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.profile.flow;
+
+import java.util.Map;
+
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import net.shibboleth.idp.plugin.oidc.op.profile.IdTokenClaimsSetDecorator;
+
+/**
+ * Test implementation for {@link IdTokenClaimsSetDecorator}.
+ */
+public class TestIdTokenClaimsSetDecorator implements IdTokenClaimsSetDecorator {
+
+    public static final String KEY = "customIdTokenKey";
+    public static final String VALUE = "customIdTokenValue";
+
+    /** {@inheritDoc} */
+    @Override
+    public void accept(final @Nullable Map<String, Object> claimsSet,
+            final @Nullable ProfileRequestContext profileRequestContext) {
+        if (claimsSet != null && !claimsSet.containsKey(KEY)) {
+            claimsSet.put(KEY, VALUE);
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String getId() {
+        return "testIdTokenClaimsSetDecorator";
+    }
+
+}
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/TestRefreshTokenClaimsSetDecorator.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/TestRefreshTokenClaimsSetDecorator.java
new file mode 100644
index 00000000..d0fe7f31
--- /dev/null
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/TestRefreshTokenClaimsSetDecorator.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.profile.flow;
+
+import java.util.Map;
+
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import net.shibboleth.idp.plugin.oidc.op.profile.RefreshTokenClaimsSetDecorator;
+
+/**
+ * Test implementation for {@link RefreshTokenClaimsSetDecorator}.
+ */
+public class TestRefreshTokenClaimsSetDecorator implements RefreshTokenClaimsSetDecorator {
+
+    public static final String KEY = "customRefreshTokenKey";
+    public static final String VALUE = "customRefreshTokenValue";
+
+    /** {@inheritDoc} */
+    @Override
+    public void accept(final @Nullable Map<String, Object> claimsSet,
+            final @Nullable ProfileRequestContext profileRequestContext) {
+        if (claimsSet != null && !claimsSet.containsKey(KEY)) {
+            claimsSet.put(KEY, VALUE);
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String getId() {
+        return "testRefreshTokenClaimsSetDecorator";
+    }
+
+}
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/TestRequestUriClaimsSetDecorator.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/TestRequestUriClaimsSetDecorator.java
new file mode 100644
index 00000000..c2824f1e
--- /dev/null
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/TestRequestUriClaimsSetDecorator.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.profile.flow;
+
+import java.util.Map;
+
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import net.shibboleth.idp.plugin.oidc.op.profile.RequestUriClaimsSetDecorator;
+
+/**
+ * Test implementation for {@link RequestUriClaimsSetDecorator}.
+ */
+public class TestRequestUriClaimsSetDecorator implements RequestUriClaimsSetDecorator {
+
+    public static final String KEY = "customRequestUriKey";
+    public static final String VALUE = "customRequestUriValue";
+
+    /** {@inheritDoc} */
+    @Override
+    public void accept(final @Nullable Map<String, Object> claimsSet,
+            final @Nullable ProfileRequestContext profileRequestContext) {
+        if (claimsSet != null && !claimsSet.containsKey(KEY)) {
+            claimsSet.put(KEY, VALUE);
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String getId() {
+        return "testRequestUriClaimsSetDecorator";
+    }
+
+}
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/TokenFlowTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/TokenFlowTest.java
index 1f970226..a3cbd3cd 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/TokenFlowTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/TokenFlowTest.java
@@ -209,6 +209,7 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
         final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
         final AccessTokenResponse response = parseSuccessResponse(result, AccessTokenResponse.class);
         Assert.assertNotNull(response.getTokens().getAccessToken());
+        assertDecoratedAccessToken(response.getTokens().getAccessToken());
     }
 
     @Test
@@ -270,7 +271,10 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
             Assert.assertNotNull(response.getOIDCTokens().getIDToken().getJWTClaimsSet().getClaim("at_hash"));
             Assert.assertNull(getSidFromAccessToken(response.getTokens().getAccessToken()));
             Assert.assertNull(getSidFromRefreshToken(response.getTokens().getRefreshToken()));
+            assertDecoratedRefreshToken(response.getTokens().getRefreshToken());
             Assert.assertNull(getSidFromJWT(response.getOIDCTokens().getIDToken()));
+            assertDecoratedAccessToken(response.getTokens().getAccessToken());
+            assertDecoratedIdToken(response.getOIDCTokens().getIDToken());
         }
     }
 
@@ -291,11 +295,13 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
         Assert.assertNotNull(response.getTokens().getRefreshToken());
         Assert.assertNull(getSidFromAccessToken(response.getTokens().getAccessToken()));
         Assert.assertNull(getSidFromRefreshToken(response.getTokens().getRefreshToken()));
+        assertDecoratedRefreshToken(response.getTokens().getRefreshToken());
         final SignedJWT jwt = SignedJWT.parse(response.getTokens().getAccessToken().getValue());
         final List<String> audience = jwt.getJWTClaimsSet().getAudience();
         Assert.assertEquals(audience.size(), 2);
         Assert.assertTrue(audience.contains(resourceUri));
         Assert.assertTrue(audience.contains(issuer));
+        assertDecoratedAccessToken(response.getTokens().getAccessToken());
     }
 
     @Test
@@ -316,6 +322,7 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
         Assert.assertNotNull(response.getTokens().getRefreshToken());
         Assert.assertNull(getSidFromAccessToken(response.getTokens().getAccessToken()));
         Assert.assertNull(getSidFromRefreshToken(response.getTokens().getRefreshToken()));
+        assertDecoratedAccessToken(response.getTokens().getAccessToken());
         final List<String> audience = getAudienceFromAccessToken(response.getTokens().getAccessToken());
         Assert.assertEquals(audience.size(), 1);
         Assert.assertTrue(audience.contains(issuer));
@@ -339,7 +346,9 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
         Assert.assertNotNull(response.getTokens().getAccessToken());
         Assert.assertNotNull(response.getTokens().getRefreshToken());
         Assert.assertNull(getSidFromAccessToken(response.getTokens().getAccessToken()));
+        assertDecoratedAccessToken(response.getTokens().getAccessToken());
         Assert.assertNull(getSidFromRefreshToken(response.getTokens().getRefreshToken()));
+        assertDecoratedRefreshToken(response.getTokens().getRefreshToken());
         final SignedJWT jwt = SignedJWT.parse(response.getTokens().getAccessToken().getValue());
         final List<String> audience = jwt.getJWTClaimsSet().getAudience();
         Assert.assertEquals(audience.size(), 1);
@@ -363,7 +372,9 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
         Assert.assertNotNull(response.getTokens().getAccessToken());
         Assert.assertNotNull(response.getTokens().getRefreshToken());
         Assert.assertNull(getSidFromAccessToken(response.getTokens().getAccessToken()));
+        assertDecoratedAccessToken(response.getTokens().getAccessToken());
         Assert.assertNull(getSidFromRefreshToken(response.getTokens().getRefreshToken()));
+        assertDecoratedRefreshToken(response.getTokens().getRefreshToken());
         final SignedJWT jwt = SignedJWT.parse(response.getTokens().getAccessToken().getValue());
         final List<String> audience = jwt.getJWTClaimsSet().getAudience();
         Assert.assertEquals(audience.size(), 2);
@@ -382,11 +393,13 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
         Assert.assertNotNull(response.getTokens().getAccessToken());
         // test that the other requested scopes (profile email offline_access) are stripped out
         Assert.assertEquals(response.getTokens().getAccessToken().getScope().toString(), "openid");
+        assertDecoratedAccessToken(response.getTokens().getAccessToken());
         Assert.assertNull(response.getTokens().getRefreshToken());
         Assert.assertNotNull(response.getOIDCTokens().getIDToken());
         Assert.assertNotNull(response.getOIDCTokens().getIDToken().getJWTClaimsSet().getClaim("at_hash"));
         Assert.assertNull(getSidFromAccessToken(response.getTokens().getAccessToken()));
         Assert.assertNull(getSidFromJWT(response.getOIDCTokens().getIDToken()));
+        assertDecoratedIdToken(response.getOIDCTokens().getIDToken());
     }
 
     @Test
@@ -424,7 +437,9 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
             Assert.assertNotNull(response.getOIDCTokens().getIDToken().getJWTClaimsSet().getClaim("at_hash"));
             Assert.assertNull(getSidFromAccessToken(response.getTokens().getAccessToken()));
             Assert.assertNull(getSidFromRefreshToken(response.getTokens().getRefreshToken()));
+            assertDecoratedRefreshToken(response.getTokens().getRefreshToken());
             Assert.assertNull(getSidFromJWT(response.getOIDCTokens().getIDToken()));
+            assertDecoratedIdToken(response.getOIDCTokens().getIDToken());
         }
     }
 
@@ -440,6 +455,7 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
         Assert.assertNotNull(response.getTokens().getRefreshToken());
         Assert.assertNull(getSidFromAccessToken(response.getTokens().getAccessToken()));
         Assert.assertNull(getSidFromRefreshToken(response.getTokens().getRefreshToken()));
+        assertDecoratedRefreshToken(response.getTokens().getRefreshToken());
     }
 
     @Test
@@ -459,6 +475,7 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
         Assert.assertNotNull(response.getTokens().getRefreshToken());
         Assert.assertNull(getSidFromAccessToken(response.getTokens().getAccessToken()));
         Assert.assertNull(getSidFromRefreshToken(response.getTokens().getRefreshToken()));
+        assertDecoratedRefreshToken(response.getTokens().getRefreshToken());
         final SignedJWT jwt = SignedJWT.parse(response.getTokens().getAccessToken().getValue());
         final List<String> audience = jwt.getJWTClaimsSet().getAudience();
         Assert.assertEquals(audience.size(), 1);
@@ -482,6 +499,7 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
         Assert.assertNotNull(response.getTokens().getRefreshToken());
         Assert.assertNull(getSidFromAccessToken(response.getTokens().getAccessToken()));
         Assert.assertNull(getSidFromRefreshToken(response.getTokens().getRefreshToken()));
+        assertDecoratedRefreshToken(response.getTokens().getRefreshToken());
         final SignedJWT jwt = SignedJWT.parse(response.getTokens().getAccessToken().getValue());
         final List<String> audience = jwt.getJWTClaimsSet().getAudience();
         Assert.assertEquals(audience.size(), 1);
@@ -528,7 +546,9 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
         Assert.assertNotNull(response.getOIDCTokens().getIDToken().getJWTClaimsSet().getClaim("at_hash"));
         Assert.assertEquals(getSidFromAccessToken(response.getTokens().getAccessToken()), sid);
         Assert.assertEquals(getSidFromRefreshToken(response.getTokens().getRefreshToken()), sid);
+        assertDecoratedRefreshToken(response.getTokens().getRefreshToken());
         Assert.assertEquals(getSidFromJWT(response.getOIDCTokens().getIDToken()), sid);
+        assertDecoratedIdToken(response.getOIDCTokens().getIDToken());
     }
 
     @Test
@@ -543,9 +563,11 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
         Assert.assertNotNull(response.getTokens().getAccessToken());
         Assert.assertNotNull(response.getTokens().getRefreshToken());
         Assert.assertNotNull(response.getOIDCTokens().getIDToken());
+        assertDecoratedIdToken(response.getOIDCTokens().getIDToken());
         Assert.assertNotNull(response.getOIDCTokens().getIDToken().getJWTClaimsSet().getClaim("at_hash"));
         Assert.assertEquals(getSidFromAccessToken(response.getTokens().getAccessToken()), sid);
         Assert.assertEquals(getSidFromRefreshTokenJWT(response.getTokens().getRefreshToken()), sid);
+        assertDecoratedRefreshToken(response.getTokens().getRefreshToken());
     }
 
     @Test
@@ -567,6 +589,7 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
         Assert.assertNotNull(response.getTokens().getAccessToken());
         Assert.assertNotNull(response.getTokens().getRefreshToken());
         Assert.assertNotNull(response.getOIDCTokens().getIDToken());
+        assertDecoratedIdToken(response.getOIDCTokens().getIDToken());
         Assert.assertNotNull(response.getOIDCTokens().getIDToken().getJWTClaimsSet().getClaim("at_hash"));
     }
 
@@ -602,6 +625,7 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
         Assert.assertNotNull(response.getTokens().getAccessToken());
         Assert.assertNotNull(response.getTokens().getRefreshToken());
         Assert.assertNotNull(response.getOIDCTokens().getIDToken());
+        assertDecoratedIdToken(response.getOIDCTokens().getIDToken());
         Assert.assertNotNull(response.getOIDCTokens().getIDToken().getJWTClaimsSet().getClaim("at_hash"));
 
         final IDTokenClaimsSet idToken = new IDTokenClaimsSet(response.getOIDCTokens().getIDToken().getJWTClaimsSet());
@@ -662,6 +686,7 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
         Assert.assertNull(response.getTokens().getRefreshToken());
         Assert.assertNotNull(response.getOIDCTokens().getIDToken());
         Assert.assertNotNull(response.getOIDCTokens().getIDToken().getJWTClaimsSet().getClaim("at_hash"));
+        assertDecoratedIdToken(response.getOIDCTokens().getIDToken());
     }
 
     @Test
@@ -676,6 +701,7 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
         Assert.assertNotNull(response.getTokens().getAccessToken());
         Assert.assertNull(response.getTokens().getRefreshToken());
         Assert.assertNotNull(response.getOIDCTokens().getIDToken());
+        assertDecoratedIdToken(response.getOIDCTokens().getIDToken());
         Assert.assertNotNull(response.getOIDCTokens().getIDToken().getJWTClaimsSet().getClaim("at_hash"));
     }
 
@@ -2202,6 +2228,24 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
         }
     }
 
+    protected void assertDecoratedAccessToken(final AccessToken accessToken) {
+        Assert.assertNotNull(accessToken.getValue());
+        final AccessTokenClaimsSet claims;
+        try {
+            claims = AccessTokenClaimsSet.parse(accessToken.getValue(), getDataSealer());
+            Assert.assertEquals(claims.getClaimsSet().getClaim(TestAccessTokenClaimsSetDecorator.KEY),
+                    TestAccessTokenClaimsSetDecorator.VALUE);
+        } catch (final ParseException | DataSealerException e) {
+            try {
+                final SignedJWT jwt = SignedJWT.parse(accessToken.getValue());
+                Assert.assertEquals(jwt.getJWTClaimsSet().getClaim(TestAccessTokenClaimsSetDecorator.KEY),
+                        TestAccessTokenClaimsSetDecorator.VALUE);
+            } catch (final ParseException e1) {
+                Assert.fail();
+            }
+        }
+    }
+
     protected String getSidFromRefreshToken(final RefreshToken refreshToken) {
         Assert.assertNotNull(refreshToken.getValue());
         final RefreshTokenClaimsSet claims;
@@ -2213,6 +2257,26 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
         }
     }
 
+    protected void assertDecoratedRefreshToken(final RefreshToken refreshToken) {
+        Assert.assertNotNull(refreshToken.getValue());
+        final RefreshTokenClaimsSet claims;
+        try {
+            claims = RefreshTokenClaimsSet.parse(refreshToken.getValue(), getDataSealer());
+            Assert.assertEquals(claims.getClaimsSet().getClaim(TestRefreshTokenClaimsSetDecorator.KEY),
+                    TestRefreshTokenClaimsSetDecorator.VALUE);
+        } catch (ParseException | DataSealerException e) {
+            try {
+                final SignedJWT signedJwt = SignedJWT.parse(refreshToken.getValue());
+                final RefreshTokenClaimsSet jwtClaims = RefreshTokenClaimsSet.parse(signedJwt.getJWTClaimsSet()
+                    .getStringClaim(TokenClaimsSet.KEY_SEALED_FOR_OP), getDataSealer());
+                Assert.assertEquals(jwtClaims.getClaimsSet().getClaim(TestRefreshTokenClaimsSetDecorator.KEY),
+                        TestRefreshTokenClaimsSetDecorator.VALUE);
+        } catch (ParseException | DataSealerException e1) {
+            Assert.fail();
+        }
+        }
+    }
+
     protected String getSidFromJWT(final JWT jwt) {
         Assert.assertNotNull(jwt);
         try {
@@ -2222,6 +2286,16 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
         }
     }
 
+    protected void assertDecoratedIdToken(final JWT jwt) {
+        Assert.assertNotNull(jwt);
+        try {
+            Assert.assertEquals(jwt.getJWTClaimsSet().getStringClaim(TestIdTokenClaimsSetDecorator.KEY),
+                    TestIdTokenClaimsSetDecorator.VALUE);
+        } catch (ParseException e) {
+            Assert.fail();
+        }
+    }
+
     protected String getSidFromRefreshTokenJWT(final RefreshToken refreshToken) {
         assertRefreshTokenJWT(refreshToken);
         final RefreshTokenClaimsSet claims;
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ManipulateClaimsForIDTokenTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ManipulateClaimsForIDTokenTest.java
index 0b825ffc..660581f0 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ManipulateClaimsForIDTokenTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ManipulateClaimsForIDTokenTest.java
@@ -48,7 +48,7 @@ public class ManipulateClaimsForIDTokenTest extends BaseOIDCResponseActionTest {
 
     private void init(final BiFunction<ProfileRequestContext,Map<String,Object>,Map<String,Object>> strategy)
             throws ComponentInitializationException {
-        action = new ManipulateClaimsForIDToken();
+        action = new ManipulateClaimsForIDToken(null);
         action.setObjectMapper(new ObjectMapper());
         action.initialize();
         final DefaultOIDCAuthorizationConfiguration config = new DefaultOIDCAuthorizationConfiguration();
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetRefreshTokenToResponseContextTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetRefreshTokenToResponseContextTest.java
index df52fb4e..fd426da4 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetRefreshTokenToResponseContextTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetRefreshTokenToResponseContextTest.java
@@ -109,7 +109,7 @@ public class SetRefreshTokenToResponseContextTest extends BaseOIDCResponseAction
     protected SetRefreshTokenToResponseContext initAction(final Function<ProfileRequestContext,
             BiFunction<ProfileRequestContext,Map<String,Object>,Map<String,Object>>> manipulationStrategy)
                     throws ComponentInitializationException, NoSuchAlgorithmException {
-        final SetRefreshTokenToResponseContext action = new SetRefreshTokenToResponseContext(getDataSealer());
+        final SetRefreshTokenToResponseContext action = new SetRefreshTokenToResponseContext(getDataSealer(), null);
         action.setRevocationCache(revocationCache);
         if (manipulationStrategy != null) {
             action.setTokenClaimsSetManipulationStrategyLookupStrategy(manipulationStrategy);
diff --git a/idp-oidc-extension-impl/src/test/resources/net/shibboleth/idp/module/conf/global.xml b/idp-oidc-extension-impl/src/test/resources/net/shibboleth/idp/module/conf/global.xml
index 0cdc7aeb..4b7b0f9e 100644
--- a/idp-oidc-extension-impl/src/test/resources/net/shibboleth/idp/module/conf/global.xml
+++ b/idp-oidc-extension-impl/src/test/resources/net/shibboleth/idp/module/conf/global.xml
@@ -167,4 +167,9 @@
         </property>
     </bean>
 
+    <bean class="net.shibboleth.idp.plugin.oidc.op.profile.flow.TestIdTokenClaimsSetDecorator" />
+    <bean class="net.shibboleth.idp.plugin.oidc.op.profile.flow.TestRequestUriClaimsSetDecorator" />
+    <bean class="net.shibboleth.idp.plugin.oidc.op.profile.flow.TestAccessTokenClaimsSetDecorator" />
+    <bean class="net.shibboleth.idp.plugin.oidc.op.profile.flow.TestRefreshTokenClaimsSetDecorator" />
+    <bean class="net.shibboleth.idp.plugin.oidc.op.profile.flow.TestAuthorizationCodeClaimsSetDecorator" />
 </beans>

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


More information about the commits mailing list