[java-idp-oidc] branch main updated: JOIDC-111 - Support manipulating claims encoded inside authz code and tokens

Henri Mikkonen henri.mikkonen at iki.fi
Fri May 27 12:41:39 UTC 2022


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=768db0d69ea9201e7cb985ed95cde1332ec26842

The following commit(s) were added to refs/heads/main by this push:
     new 768db0d6 JOIDC-111 - Support manipulating claims encoded inside authz code and tokens
768db0d6 is described below

commit 768db0d69ea9201e7cb985ed95cde1332ec26842
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Fri May 27 15:40:01 2022 +0300

    JOIDC-111 - Support manipulating claims encoded inside authz code and tokens
    
    https://shibboleth.atlassian.net/browse/JOIDC-111
    
    Wired the new profile configuration parameters regarding token set manipulation.
    New function 'shibboleth.oidc.TokenRequestTokenClaimsSetLookupFunction' helps in
    fetching custom values added to the tokens / authz codes.
---
 .../TokenRequestTokenClaimsSetLookupFunction.java  | 52 ++++++++++++++++++++
 .../op/oauth2/profile/impl/BuildAccessToken.java   | 52 +++++++++++++++++++-
 .../SetAuthorizationCodeToResponseContext.java     | 56 +++++++++++++++++++++-
 .../impl/SetRefreshTokenToResponseContext.java     | 56 +++++++++++++++++++++-
 .../META-INF/net.shibboleth.idp/postconfig.xml     |  4 ++
 .../idp/service/relying-party/postconfig.xml       | 12 +++++
 .../oidc/op/profile/flow/AuthorizeFlowTest.java    | 56 +++++++++++++++++++++-
 .../plugin/oidc/op/profile/flow/TokenFlowTest.java | 32 +++++++++++++
 .../src/test/resources/conf/global.xml             | 38 ++++++++++++++-
 .../src/test/resources/conf/relying-party.xml      |  8 ++++
 10 files changed, 361 insertions(+), 5 deletions(-)

diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/TokenRequestTokenClaimsSetLookupFunction.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/TokenRequestTokenClaimsSetLookupFunction.java
new file mode 100644
index 00000000..9ac65825
--- /dev/null
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/TokenRequestTokenClaimsSetLookupFunction.java
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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.context.navigate;
+
+import java.util.function.BiFunction;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import net.shibboleth.idp.plugin.oidc.op.token.support.TokenClaimsSet;
+
+/**
+ * A bi-function that returns value for the given claim via a lookup function. This lookup locates the claim from token
+ * claims set. If token claims set nor the desired claim in the set are not available, null is returned.
+ */
+public class TokenRequestTokenClaimsSetLookupFunction implements BiFunction<ProfileRequestContext, String, Object> {
+
+    /** {@inheritDoc} */
+    @Override
+    @Nullable public Object apply(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final String claim) {
+        final Function<ProfileRequestContext, Object> claimsSetLookup =
+                new AbstractTokenClaimsLookupFunction<Object>() {
+
+            @Override
+            Object doLookup(final @Nonnull TokenClaimsSet tokenClaims) {
+                return tokenClaims.getClaimsSet().getClaim(claim);
+            }
+            
+        };
+        return claimsSetLookup.apply(profileRequestContext);
+    }
+
+}
\ No newline at end of file
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 55a783ed..e36a273b 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
@@ -22,6 +22,7 @@ import java.time.Duration;
 import java.time.Instant;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.function.BiFunction;
 import java.util.function.Function;
 import java.util.function.Predicate;
 
@@ -59,6 +60,7 @@ import net.shibboleth.idp.plugin.oidc.op.token.support.AccessTokenClaimsSet.Buil
 import net.shibboleth.idp.profile.IdPEventIds;
 import net.shibboleth.idp.profile.context.navigate.ResponderIdLookupFunction;
 import net.shibboleth.oidc.profile.config.logic.AttributeConsentFlowEnabledPredicate;
+import net.shibboleth.oidc.profile.config.navigate.AccessTokenClaimsSetManipulationStrategyLookupFunction;
 import net.shibboleth.oidc.profile.config.navigate.AccessTokenLifetimeLookupFunction;
 import net.shibboleth.oidc.profile.config.navigate.AccessTokenTypeLookupFunction;
 
@@ -133,6 +135,14 @@ public class BuildAccessToken extends AbstractOIDCResponseAction {
     /** Strategy used to create the subcontext to hold the token. */
     @Nonnull private Function<ProfileRequestContext,AccessTokenContext> accessTokenContextCreationStrategy;
 
+    /** Lookup function to supply strategy bi-function for manipulating token claims set. */ 
+    @Nonnull
+    private Function<ProfileRequestContext,BiFunction<ProfileRequestContext,Map<String,Object>,Map<String,Object>>>
+        tokenClaimsSetManipulationStrategyLookupStrategy;
+
+    /** The strategy used for manipulating the token claims set */
+    @Nullable private BiFunction<ProfileRequestContext,Map<String,Object>,Map<String,Object>> manipulationStrategy;
+
     /** Authorize Code / Refresh Token the access token is based on, if any. */
     @Nullable private TokenClaimsSet tokenClaimsSet;
 
@@ -175,6 +185,8 @@ public class BuildAccessToken extends AbstractOIDCResponseAction {
         accessTokenContextCreationStrategy = new ChildContextLookup<>(AccessTokenContext.class, true).compose(
                 new ChildContextLookup<>(OIDCAuthenticationResponseContext.class).compose(
                         new OutboundMessageContextLookup()));
+        tokenClaimsSetManipulationStrategyLookupStrategy =
+                new AccessTokenClaimsSetManipulationStrategyLookupFunction();
     }
     
     /**
@@ -298,6 +310,20 @@ public class BuildAccessToken extends AbstractOIDCResponseAction {
                 Constraint.isNotNull(strategy, "AccessTokenContext creation strategy cannot be null");
     }
     
+    /**
+     * Set the lookup function to supply strategy bi-function for manipulating token claims set.
+     * 
+     * @param strategy What to set
+     */
+    public void setTokenClaimsSetManipulationStrategyLookupStrategy(@Nonnull final
+            Function<ProfileRequestContext,BiFunction<ProfileRequestContext,Map<String,Object>,Map<String,Object>>>
+            strategy) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+        tokenClaimsSetManipulationStrategyLookupStrategy =
+                Constraint.isNotNull(strategy, "Manipulation strategy lookup strategy cannot be null");
+    }
+
     /** {@inheritDoc} */
     @Override
     protected void doInitialize() throws ComponentInitializationException {
@@ -374,6 +400,9 @@ public class BuildAccessToken extends AbstractOIDCResponseAction {
             return false;
         }
         accessTokenCtx.setLifetime(lifetime);
+
+        manipulationStrategy = tokenClaimsSetManipulationStrategyLookupStrategy.apply(profileRequestContext);
+
         return true;
     }
 
@@ -460,7 +489,28 @@ public class BuildAccessToken extends AbstractOIDCResponseAction {
         }
         
         final AccessTokenClaimsSet claimsSet = builder.build();
-        
+
+        if (manipulationStrategy != null) {
+            log.debug("{} Manipulation strategy has been set, applying it to the claims set {}", getLogPrefix(),
+                    claimsSet.serialize());
+            final Map<String, Object> result = manipulationStrategy.apply(profileRequestContext,
+                    claimsSet.getClaimsSet().toJSONObject());
+            if (result == null) {
+                log.debug("{} Manipulation strategy returned null, leaving token claims set untouched.", getLogPrefix());
+            } else {
+                log.debug("{} Applying the manipulated claims into the token claims set", getLogPrefix());
+                try {
+                    claimsSet.setClaimsSet(JWTClaimsSet.parse(result));
+                } catch (final ParseException e) {
+                    log.error("{} The resulted claims set could not be transformed into ", getLogPrefix(), e);
+                    ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_PROFILE_CONFIG);
+                    return;
+                }
+            }
+        } else {
+            log.debug("{} No manipulation strategy configured", getLogPrefix());
+        }
+
         try {
             if (jwtTokenType) {
                 accessTokenCtx.setJWT(new PlainJWT(sealClaims(claimsSet.getClaimsSet())));
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetAuthorizationCodeToResponseContext.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetAuthorizationCodeToResponseContext.java
index 9b784a4a..e8142371 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetAuthorizationCodeToResponseContext.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetAuthorizationCodeToResponseContext.java
@@ -17,8 +17,11 @@
 
 package net.shibboleth.idp.plugin.oidc.op.profile.impl;
 
+import java.text.ParseException;
 import java.time.Duration;
 import java.time.Instant;
+import java.util.Map;
+import java.util.function.BiFunction;
 import java.util.function.Function;
 import java.util.function.Predicate;
 
@@ -29,6 +32,7 @@ import org.opensaml.profile.context.ProfileRequestContext;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import com.nimbusds.jwt.JWTClaimsSet;
 import com.nimbusds.oauth2.sdk.Scope;
 import com.nimbusds.openid.connect.sdk.claims.ClaimsSet;
 import net.minidev.json.JSONArray;
@@ -44,6 +48,7 @@ import net.shibboleth.idp.plugin.oidc.op.token.support.AuthorizeCodeClaimsSet;
 import net.shibboleth.idp.profile.IdPEventIds;
 import net.shibboleth.idp.profile.context.navigate.ResponderIdLookupFunction;
 import net.shibboleth.oidc.profile.config.logic.AttributeConsentFlowEnabledPredicate;
+import net.shibboleth.oidc.profile.config.navigate.AuthorizationCodeClaimsSetManipulationStrategyLookupFunction;
 import net.shibboleth.oidc.profile.config.navigate.AuthzCodeLifetimeLookupFunction;
 
 import org.opensaml.messaging.context.navigate.ChildContextLookup;
@@ -102,6 +107,14 @@ public class SetAuthorizationCodeToResponseContext extends AbstractOIDCAuthentic
     /** Strategy used to locate the code challenge method. */
     @Nonnull private Function<ProfileRequestContext, String> codeChallengeMethodLookupStrategy;
 
+    /** Lookup function to supply strategy bi-function for manipulating token claims set. */ 
+    @Nonnull
+    private Function<ProfileRequestContext,BiFunction<ProfileRequestContext,Map<String,Object>,Map<String,Object>>>
+        tokenClaimsSetManipulationStrategyLookupStrategy;
+
+    /** The strategy used for manipulating the token claims set. */
+    @Nullable private BiFunction<ProfileRequestContext,Map<String,Object>,Map<String,Object>> manipulationStrategy;
+
     /** Subject context. */
     @Nullable private SubjectContext subjectCtx;
 
@@ -127,6 +140,8 @@ public class SetAuthorizationCodeToResponseContext extends AbstractOIDCAuthentic
         issuerLookupStrategy = new ResponderIdLookupFunction();
         consentEnabledPredicate = new AttributeConsentFlowEnabledPredicate();
         idGeneratorLookupStrategy = FunctionSupport.constant(new SecureRandomIdentifierGenerationStrategy());
+        tokenClaimsSetManipulationStrategyLookupStrategy =
+                new AuthorizationCodeClaimsSetManipulationStrategyLookupFunction();
     }
 
     /**
@@ -234,6 +249,20 @@ public class SetAuthorizationCodeToResponseContext extends AbstractOIDCAuthentic
                 Constraint.isNotNull(predicate, "predicate used to check if consent is enabled cannot be null");
     }
     
+    /**
+     * Set the lookup function to supply strategy bi-function for manipulating token claims set.
+     * 
+     * @param strategy What to set
+     */
+    public void setTokenClaimsSetManipulationStrategyLookupStrategy(@Nonnull final
+            Function<ProfileRequestContext,BiFunction<ProfileRequestContext,Map<String,Object>,Map<String,Object>>>
+            strategy) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+        tokenClaimsSetManipulationStrategyLookupStrategy =
+                Constraint.isNotNull(strategy, "Manipulation strategy lookup strategy cannot be null");
+    }
+
     /** {@inheritDoc} */
     @Override
     protected void doInitialize() throws ComponentInitializationException {
@@ -281,7 +310,9 @@ public class SetAuthorizationCodeToResponseContext extends AbstractOIDCAuthentic
             // Default method is "plain"
             codeChallenge = (codeChallengeMethod != null ? codeChallengeMethod : "plain") + codeChallenge;
         }
-        
+
+        manipulationStrategy = tokenClaimsSetManipulationStrategyLookupStrategy.apply(profileRequestContext);
+
         return true;
     }
 
@@ -328,6 +359,29 @@ public class SetAuthorizationCodeToResponseContext extends AbstractOIDCAuthentic
                 .setConsentedClaims(consented)
                 .setConsentEnabled(consentEnabledPredicate.test(profileRequestContext))
                 .build();
+
+        if (manipulationStrategy != null) {
+            log.debug("{} Manipulation strategy has been set, applying it to the claims set {}", getLogPrefix(),
+                    claimsSet.serialize());
+            final Map<String, Object> result = manipulationStrategy.apply(profileRequestContext,
+                    claimsSet.getClaimsSet().toJSONObject());
+            if (result == null) {
+                log.debug("{} Manipulation strategy returned null, leaving token claims set untouched.",
+                        getLogPrefix());
+            } else {
+                log.debug("{} Applying the manipulated claims into the token claims set", getLogPrefix());
+                try {
+                    claimsSet.setClaimsSet(JWTClaimsSet.parse(result));
+                } catch (final ParseException e) {
+                    log.error("{} The resulted claims set could not be transformed into ", getLogPrefix(), e);
+                    ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_PROFILE_CONFIG);
+                    return;
+                }
+            }
+        } else {
+            log.debug("{} No manipulation strategy configured", getLogPrefix());
+        }
+
         // We set token claims set to response context for possible access token generation.
         responseCtx.setAuthorizationGrantClaimsSet(claimsSet);
         try {
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 81afb7d4..efa73e06 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,8 +17,11 @@
 
 package net.shibboleth.idp.plugin.oidc.op.profile.impl;
 
+import java.text.ParseException;
 import java.time.Duration;
 import java.time.Instant;
+import java.util.Map;
+import java.util.function.BiFunction;
 import java.util.function.Function;
 
 import javax.annotation.Nonnull;
@@ -29,6 +32,7 @@ import org.opensaml.profile.context.ProfileRequestContext;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import com.nimbusds.jwt.JWTClaimsSet;
 import com.nimbusds.openid.connect.sdk.OIDCScopeValue;
 
 import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext;
@@ -36,6 +40,7 @@ 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.oidc.profile.config.navigate.RefreshTokenClaimsSetManipulationStrategyLookupFunction;
 import net.shibboleth.oidc.profile.config.navigate.RefreshTokenLifetimeLookupFunction;
 
 import org.opensaml.profile.action.ActionSupport;
@@ -62,6 +67,14 @@ public class SetRefreshTokenToResponseContext extends AbstractOIDCResponseAction
     /** Strategy used to obtain the refresh token lifetime. */
     @Nonnull private Function<ProfileRequestContext,Duration> refreshTokenLifetimeLookupStrategy;
 
+    /** Lookup function to supply strategy bi-function for manipulating token claims set. */ 
+    @Nonnull
+    private Function<ProfileRequestContext,BiFunction<ProfileRequestContext,Map<String,Object>,Map<String,Object>>>
+        tokenClaimsSetManipulationStrategyLookupStrategy;
+
+    /** The strategy used for manipulating the token claims set. */
+    @Nullable private BiFunction<ProfileRequestContext,Map<String,Object>,Map<String,Object>> manipulationStrategy;
+
     /** Authorize Code / Refresh Token the refresh token will be based on. */
     @Nullable private TokenClaimsSet tokenClaimsSet;
     
@@ -76,6 +89,8 @@ public class SetRefreshTokenToResponseContext extends AbstractOIDCResponseAction
     public SetRefreshTokenToResponseContext(@Nonnull @ParameterName(name = "sealer") final DataSealer sealer) {
         refreshTokenLifetimeLookupStrategy = new RefreshTokenLifetimeLookupFunction();
         dataSealer = Constraint.isNotNull(sealer, "DataSealer cannot be null");
+        tokenClaimsSetManipulationStrategyLookupStrategy =
+                new RefreshTokenClaimsSetManipulationStrategyLookupFunction();
     }
 
     /**
@@ -91,6 +106,20 @@ public class SetRefreshTokenToResponseContext extends AbstractOIDCResponseAction
                 Constraint.isNotNull(strategy, "Refresh token lifetime lookup strategy cannot be null");
     }
 
+    /**
+     * Set the lookup function to supply strategy bi-function for manipulating token claims set.
+     * 
+     * @param strategy What to set
+     */
+    public void setTokenClaimsSetManipulationStrategyLookupStrategy(@Nonnull final
+            Function<ProfileRequestContext,BiFunction<ProfileRequestContext,Map<String,Object>,Map<String,Object>>>
+            strategy) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+        tokenClaimsSetManipulationStrategyLookupStrategy =
+                Constraint.isNotNull(strategy, "Manipulation strategy lookup strategy cannot be null");
+    }
+
     /** {@inheritDoc} */
     @Override
     protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
@@ -118,7 +147,9 @@ public class SetRefreshTokenToResponseContext extends AbstractOIDCResponseAction
             ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
             return false;
         }
-        
+
+        manipulationStrategy = tokenClaimsSetManipulationStrategyLookupStrategy.apply(profileRequestContext);
+
         return true;
     }
 
@@ -128,6 +159,29 @@ public class SetRefreshTokenToResponseContext extends AbstractOIDCResponseAction
         final Instant dateExp = Instant.now().plus(refreshTokenLifetime);
         final RefreshTokenClaimsSet claimsSet =
                 new RefreshTokenClaimsSet.Builder(tokenClaimsSet, Instant.now(), dateExp).build();
+
+        if (manipulationStrategy != null) {
+            log.debug("{} Manipulation strategy has been set, applying it to the claims set {}", getLogPrefix(),
+                    claimsSet.serialize());
+            final Map<String, Object> result = manipulationStrategy.apply(profileRequestContext,
+                    claimsSet.getClaimsSet().toJSONObject());
+            if (result == null) {
+                log.debug("{} Manipulation strategy returned null, leaving token claims set untouched.",
+                        getLogPrefix());
+            } else {
+                log.debug("{} Applying the manipulated claims into the token claims set", getLogPrefix());
+                try {
+                    claimsSet.setClaimsSet(JWTClaimsSet.parse(result));
+                } catch (final ParseException e) {
+                    log.error("{} The resulted claims set could not be transformed into ", getLogPrefix(), e);
+                    ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_PROFILE_CONFIG);
+                    return;
+                }
+            }
+        } else {
+            log.debug("{} No manipulation strategy configured", getLogPrefix());
+        }
+
         try {
             getOidcResponseContext().setRefreshToken(claimsSet.serialize(dataSealer));
             log.debug("{} Setting refresh token {} as {} to response context ", getLogPrefix(), claimsSet.serialize(),
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
index ce0505dc..acbfef4a 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
@@ -543,4 +543,8 @@
         </property>
     </bean>    
 
+    <bean id="shibboleth.oidc.TokenRequestTokenClaimsSetLookupFunction"
+            class="net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.TokenRequestTokenClaimsSetLookupFunction">
+    </bean>
+
 </beans>
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml
index f84d4742..99f7170d 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml
@@ -240,6 +240,10 @@
             <bean parent="shibboleth.MDDrivenBeanProperty" p:propertyName="IDTokenManipulationStrategy"
                 p:propertyType="#{T(java.util.function.Function)}" />
         </property>
+        <property name="accessTokenClaimsSetManipulationStrategyLookupStrategy">
+            <bean parent="shibboleth.MDDrivenBeanProperty" p:propertyName="accessTokenClaimsSetManipulationStrategy"
+                p:propertyType="#{T(java.util.function.Function)}" />
+        </property>
     </bean>
     
     <bean id="OIDC.SSO.MDDriven" parent="AbstractMDDrivenOIDCSSOProfile" lazy-init="true"
@@ -295,6 +299,10 @@
                 <constructor-arg value="false" />
             </bean>
         </property>
+        <property name="authorizationCodeClaimsSetManipulationStrategyLookupStrategy">
+            <bean parent="shibboleth.MDDrivenBeanProperty" p:propertyName="authorizationCodeClaimsSetManipulationStrategy"
+                p:propertyType="#{T(java.util.function.Function)}" />
+        </property>
     </bean>
     
     <bean id="OIDC.UserInfo.MDDriven" parent="AbstractMDDrivenOIDCProfile" lazy-init="true"
@@ -396,6 +404,10 @@
             <bean parent="shibboleth.MDDrivenDurationProperty" p:propertyName="refreshTokenLifetime"
                 p:defaultValue="%{idp.oidc.refreshToken.defaultLifetime:PT2H}" />
         </property>
+        <property name="refreshTokenClaimsSetManipulationStrategyLookupStrategy">
+            <bean parent="shibboleth.MDDrivenBeanProperty" p:propertyName="refreshTokenClaimsSetManipulationStrategy"
+                p:propertyType="#{T(java.util.function.Function)}" />
+        </property>
     </bean>
     
     <bean id="OAUTH2.TokenAudience.MDDriven" parent="AbstractMDDrivenOIDCProfile" lazy-init="true"
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 89abe9c7..575adcfd 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
@@ -39,7 +39,6 @@ import com.nimbusds.jwt.JWT;
 import com.nimbusds.jwt.JWTClaimsSet;
 import com.nimbusds.jwt.PlainJWT;
 import com.nimbusds.jwt.SignedJWT;
-import com.nimbusds.oauth2.sdk.ErrorResponse;
 import com.nimbusds.oauth2.sdk.Response;
 import com.nimbusds.oauth2.sdk.Scope;
 import com.nimbusds.openid.connect.sdk.AuthenticationErrorResponse;
@@ -66,6 +65,7 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
     String redirectUri = "https://example.org/cb";
     String clientId = "mockClientId";
     String clientIdIssInResponse = "mockClientIdIssInResponse";
+    String clientIdCustomTokens = "mockClientIdCustomTokens";
     String clientSecret = "mockClientSecretmockClientSecretmockClientSecretmockClientSecretmockClientSecret";
     Scope scope = Scope.parse("openid profile email");
     
@@ -466,6 +466,33 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
         Assert.assertNull(token.getClaimsSet().getStringClaim("eduPersonScopedAffiliation"));
     }
 
+    @Test
+    public void testWithHybridIdTokenTokenFlowWithCustomTokenClaim() throws IOException, SessionException, ParseException, DataSealerException {
+        request.setMethod("GET");
+        request.setQueryString("client_id=mockClientIdCustomTokens&response_type=code+id_token+token&scope=openid%20profile"
+                + "&redirect_uri="+ redirectUri + "&nonce=idhas3h23hi13h1o2i32");
+        storeMetadata(storageService, clientIdCustomTokens, clientSecret, scope, redirectUri);
+
+        initializeThreadLocals();
+        
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        final AuthenticationResponse responseMessage = parseSuccessResponse(result, AuthenticationResponse.class);
+        final AuthenticationSuccessResponse successResponse = responseMessage.toSuccessResponse();
+        Assert.assertEquals(successResponse.getRedirectionURI().toString(), redirectUri);
+        Assert.assertNotNull(successResponse.getIDToken());
+        Assert.assertNotNull(successResponse.getAccessToken());
+        Assert.assertNotNull(successResponse.getAuthorizationCode());
+        Assert.assertNull(successResponse.getIssuer());
+
+        final AccessTokenClaimsSet token =
+                AccessTokenClaimsSet.parse(successResponse.getAccessToken().getValue(), getDataSealer());
+        Assert.assertEquals(token.getAudience(), Collections.singletonList(issuer));
+        Assert.assertNull(token.getClaimsSet().getStringClaim("eduPersonScopedAffiliation"));
+        final String customClaim = token.getClaimsSet().getStringClaim("custom_access_token_claim");
+        Assert.assertNotNull(customClaim);
+        Assert.assertEquals(customClaim, "value2");
+    }
+
     @Test
     public void testWithHybridIdTokenTokenFlowAndResource() throws IOException, SessionException, ParseException {
         request.setMethod("GET");
@@ -706,6 +733,32 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
         Assert.assertEquals(email.getClaimRequirement(), ClaimRequirement.ESSENTIAL);
     }
 
+    @Test
+    public void testWithAuthorizationCodeFlowWithCustomClaimInCode() throws IOException, SessionException, DataSealerException, ParseException {
+        request.setMethod("GET");
+        request.setQueryString("client_id=mockClientIdCustomTokens&response_type=code&scope=openid%20profile"
+                + "&claims=%7B%22userinfo%22%3A%7B%22email%22%3A%7B%22essential%22%3Atrue%7D%7D%7D"
+                + "&redirect_uri=" + redirectUri);
+        storeMetadata(storageService, clientIdCustomTokens, clientSecret, scope, redirectUri);
+
+        initializeThreadLocals();
+
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        final AuthenticationResponse responseMessage = parseSuccessResponse(result, AuthenticationResponse.class);
+        final AuthenticationSuccessResponse successResponse = responseMessage.toSuccessResponse();
+        Assert.assertEquals(successResponse.getRedirectionURI().toString(), redirectUri);
+        Assert.assertNull(successResponse.getIDToken());
+        Assert.assertNull(successResponse.getAccessToken());
+        Assert.assertNotNull(successResponse.getAuthorizationCode());
+        Assert.assertNull(successResponse.getIssuer());
+
+        final AuthorizeCodeClaimsSet code = 
+                AuthorizeCodeClaimsSet.parse(successResponse.getAuthorizationCode().getValue(), getDataSealer());
+        final Object customClaim = code.getClaimsSet().getClaim("custom_code_claim");
+        Assert.assertNotNull(customClaim);
+        Assert.assertEquals(customClaim, "value1");
+    }
+
     @Test
     public void testWithAuthorizationCodeFlowUsingSAMLMetadata() throws IOException, SessionException {
         request.setMethod("GET");
@@ -1007,6 +1060,7 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
         removeMetadata(storageService, "mockClientIdPKCEPlainUnforced");
         removeMetadata(storageService, "mockClientIdPKCEPlain");
         removeMetadata(storageService, "mockClientIdPKCES256");
+        removeMetadata(storageService, "mockClientIdCustomTokens");
         removeMetadata(storageService, clientIdIssInResponse);
     }
 
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 b1d6b8b0..c2b4edd1 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
@@ -52,6 +52,7 @@ import com.nimbusds.openid.connect.sdk.claims.ClaimsSet;
 import net.minidev.json.JSONObject;
 import net.shibboleth.idp.plugin.oidc.op.profile.impl.ValidateGrantTest;
 import net.shibboleth.idp.plugin.oidc.op.token.support.AccessTokenClaimsSet;
+import net.shibboleth.idp.plugin.oidc.op.token.support.RefreshTokenClaimsSet;
 import net.shibboleth.idp.plugin.oidc.op.token.support.TokenClaimsSet;
 import net.shibboleth.utilities.java.support.collection.Pair;
 import net.shibboleth.utilities.java.support.security.DataSealerException;
@@ -70,6 +71,7 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
     String clientIdPkcePlainPublic = "mockPublicClientIdPKCEPlain";
     String clientIdPkcePlainUnforcedPublic = "mockPublicClientIdPKCEPlainUnforced";
     String clientIdPkceS256Public = "mockPublicClientIdPKCES256";
+    String clientIdCustomTokens = "mockClientIdCustomTokens";
     String codeVerifier = "9234567812345678123456781234567812345678123456781234567812345678";
 
     Scope scope = Scope.parse("openid profile email offline_access");
@@ -87,6 +89,7 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
         removeMetadata(storageService, clientId);
         removeMetadata(storageService, clientIdPkcePlain);
         removeMetadata(storageService, clientIdPkceS256);
+        removeMetadata(storageService, clientIdCustomTokens);
     }
     
     @Test
@@ -178,6 +181,35 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
         Assert.assertNotNull(response.getOIDCTokens().getIDToken());
     }
 
+    @Test
+    public void testValidGrantWithCustomTokens() throws Exception {
+        initializeGrantAndRequest(clientIdCustomTokens, createRequestParameters(redirectUri, "authorization_code",
+                buildAuthorizationCode(clientIdCustomTokens), clientId));
+        storeConsent(storageService, "jdoe", clientIdCustomTokens, "mail");
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        final OIDCTokenResponse response = parseSuccessResponse(result, OIDCTokenResponse.class);
+        Assert.assertNotNull(response.getTokens().getAccessToken());
+        Assert.assertNotNull(response.getTokens().getRefreshToken());
+        Assert.assertNotNull(response.getOIDCTokens().getIDToken());
+
+        final AccessTokenClaimsSet accessToken =
+                AccessTokenClaimsSet.parse(response.getTokens().getAccessToken().getValue(), getDataSealer());
+        final String atCustomAtClaim = accessToken.getClaimsSet().getStringClaim("custom_access_token_claim");
+        final String atCustomRtClaim = accessToken.getClaimsSet().getStringClaim("custom_refresh_token_claim");
+        Assert.assertNotNull(atCustomAtClaim);
+        Assert.assertEquals(atCustomAtClaim, "value2");
+        Assert.assertNull(atCustomRtClaim);
+
+        final RefreshTokenClaimsSet refreshToken =
+                RefreshTokenClaimsSet.parse(response.getTokens().getRefreshToken().getValue(), getDataSealer());
+        final String rtCustomAtClaim = refreshToken.getClaimsSet().getStringClaim("custom_access_token_claim");
+        final String rtCustomRtClaim = refreshToken.getClaimsSet().getStringClaim("custom_refresh_token_claim");
+        Assert.assertNotNull(rtCustomRtClaim);
+        Assert.assertEquals(rtCustomRtClaim, "value3");
+        Assert.assertNull(rtCustomAtClaim);
+
+    }
+
     @Test
     public void testValidGrantWithWrongRegisteredAuthType() throws Exception {
         initializeGrantAndRequest(clientId, createRequestParameters(redirectUri, "authorization_code",
diff --git a/idp-oidc-extension-impl/src/test/resources/conf/global.xml b/idp-oidc-extension-impl/src/test/resources/conf/global.xml
index 80c4d163..74c52462 100644
--- a/idp-oidc-extension-impl/src/test/resources/conf/global.xml
+++ b/idp-oidc-extension-impl/src/test/resources/conf/global.xml
@@ -38,5 +38,41 @@
     <!-- Copy of IdP signing key for tests. -->
     <bean id="testbed.DefaultRSSigningCredential" parent="shibboleth.JWKCredential"
         p:resource="%{idp.signing.oidc.rs.key}" />
-    
+
+    <bean id="testAuthorizationCodeManipulation" parent="shibboleth.BiFunctions.Scripted" factory-method="inlineScript">
+        <constructor-arg name="scriptSource">
+            <value>
+             <![CDATA[
+                newMap = input2;
+                newMap.put("custom_code_claim", "value1");
+                newMap;
+             ]]>
+            </value>
+        </constructor-arg>
+    </bean>
+
+    <bean id="testAccessTokenManipulation" parent="shibboleth.BiFunctions.Scripted" p:customObject-ref="shibboleth.oidc.TokenRequestTokenClaimsSetLookupFunction" factory-method="inlineScript">
+        <constructor-arg name="scriptSource">
+            <value>
+            <![CDATA[
+                newMap = input2;
+                newMap.put("custom_access_token_claim", "value2");
+                newMap;
+             ]]>
+            </value>
+        </constructor-arg>
+    </bean>
+
+    <bean id="testRefreshTokenManipulation" parent="shibboleth.BiFunctions.Scripted" p:customObject-ref="shibboleth.oidc.TokenRequestTokenClaimsSetLookupFunction" factory-method="inlineScript">
+        <constructor-arg name="scriptSource">
+            <value>
+            <![CDATA[
+                newMap = input2;
+                newMap.put("custom_refresh_token_claim", "value3");
+                newMap;
+             ]]>
+            </value>
+        </constructor-arg>
+    </bean>
+
 </beans>
diff --git a/idp-oidc-extension-impl/src/test/resources/conf/relying-party.xml b/idp-oidc-extension-impl/src/test/resources/conf/relying-party.xml
index 5f040a41..e6da1687 100644
--- a/idp-oidc-extension-impl/src/test/resources/conf/relying-party.xml
+++ b/idp-oidc-extension-impl/src/test/resources/conf/relying-party.xml
@@ -58,6 +58,14 @@
     </bean>
 
     <util:list id="shibboleth.RelyingPartyOverrides">
+        <bean parent="RelyingPartyByName" c:relyingPartyIds="mockClientIdCustomTokens">
+            <property name="profileConfigurations">
+                 <list>
+                     <bean parent="OIDC.SSO.MDDriven" p:authorizationCodeClaimsSetManipulationStrategy-ref="testAuthorizationCodeManipulation" p:accessTokenClaimsSetManipulationStrategy-ref="testAccessTokenManipulation" />
+                     <bean parent="OAUTH2.Token.MDDriven" p:forcePKCE="false" p:allowPKCEPlain="true" p:accessTokenClaimsSetManipulationStrategy-ref="testAccessTokenManipulation" p:refreshTokenClaimsSetManipulationStrategy-ref="testRefreshTokenManipulation"/>
+                 </list>
+            </property>
+        </bean>
         <bean parent="RelyingPartyByName" c:relyingPartyIds="mockClientIdIssInResponse">
             <property name="profileConfigurations">
                  <list>

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


More information about the commits mailing list