[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 Jun 10 09:23:56 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=2312e86a4477981768d0038523a20c916811c0f9

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

commit 2312e86a4477981768d0038523a20c916811c0f9
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Fri Jun 10 12:23:29 2022 +0300

    JOIDC-111 - Support manipulating claims encoded inside authz code and tokens
    
    https://shibboleth.atlassian.net/browse/JOIDC-111
    
    Improved unit tests.
---
 .../oauth2/profile/impl/BuildAccessTokenTest.java  | 83 +++++++++++++++++++---
 .../profile/impl/BaseOIDCResponseActionTest.java   | 10 +++
 .../SetAuthorizationCodeToResponseContextTest.java | 28 +++++++-
 .../impl/SetRefreshTokenToResponseContextTest.java | 71 +++++++++++++++++-
 4 files changed, 178 insertions(+), 14 deletions(-)

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 c4acd64c..e888d10e 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
@@ -39,12 +39,17 @@ import java.time.Duration;
 import java.time.Instant;
 import java.util.Collection;
 import java.util.Collections;
+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.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
 import org.springframework.webflow.execution.Event;
+import org.testng.Assert;
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
@@ -103,15 +108,36 @@ public class BuildAccessTokenTest extends BaseOIDCResponseActionTest {
     public void testOpaqueSuccess() throws ParseException, DataSealerException, ComponentInitializationException,
             NoSuchAlgorithmException {
 
-        initAction(null);
+        initAction(null, null);
         
         final Event event = action.execute(requestCtx);
         ActionTestingSupport.assertProceedEvent(event);
         
         verifyClaims(respCtx.getSubcontext(AccessTokenContext.class), new Scope(),
-                Collections.singletonList("https://rp.example.org"));
+                Collections.singletonList("https://rp.example.org"), null);
     }
-    
+
+    /**
+     * Basic success case with custom claims.
+     * 
+     * @throws ParseException 
+     * @throws DataSealerException 
+     * @throws ComponentInitializationException 
+     * @throws NoSuchAlgorithmException
+     */
+    @Test
+    public void testOpaqueSuccessWithCustomClaims() throws ParseException, DataSealerException,
+            ComponentInitializationException, NoSuchAlgorithmException {
+
+        initAction(null, prc -> ((prc2, map) -> addEntryToMap(map, "custom_claim", "custom_value")));
+        
+        final Event event = action.execute(requestCtx);
+        ActionTestingSupport.assertProceedEvent(event);
+        
+        verifyClaims(respCtx.getSubcontext(AccessTokenContext.class), new Scope(),
+                Collections.singletonList("https://rp.example.org"), Map.of("custom_claim", "custom_value"));
+    }
+
     /**
      * Basic success case, direct reuse of requested scope/audience.
      * 
@@ -124,28 +150,54 @@ public class BuildAccessTokenTest extends BaseOIDCResponseActionTest {
     public void testJWTSuccess() throws ParseException, ComponentInitializationException, NoSuchAlgorithmException,
             DataSealerException {
 
-        initAction("JWT");
+        initAction("JWT", null);
         
         final Event event = action.execute(requestCtx);
         ActionTestingSupport.assertProceedEvent(event);
         
         verifyClaims(respCtx.getSubcontext(AccessTokenContext.class), new Scope(),
-                Collections.singletonList("https://rp.example.org"));
+                Collections.singletonList("https://rp.example.org"), null);
     }
-    
+
+    /**
+     * Basic success case, direct reuse of requested scope/audience.
+     * 
+     * @throws ParseException 
+     * @throws ComponentInitializationException 
+     * @throws NoSuchAlgorithmException
+     * @throws DataSealerException 
+     */
+    @Test
+    public void testJWTSuccessWithCustomClaim() throws ParseException, ComponentInitializationException,
+            NoSuchAlgorithmException, DataSealerException {
+
+        initAction("JWT", prc -> ((prc2, map) -> addEntryToMap(map, "custom_claim", "custom_value")));
+        
+        final Event event = action.execute(requestCtx);
+        ActionTestingSupport.assertProceedEvent(event);
+        
+        verifyClaims(respCtx.getSubcontext(AccessTokenContext.class), new Scope(),
+                Collections.singletonList("https://rp.example.org"), Map.of("custom_claim", "custom_value"));
+    }
+
     /**
      * Init action bean based on test.
      * 
      * @param type token type to use
+     * @param manipulationStrategy the manipulation strategy to use
      * 
      * @throws ComponentInitializationException
      * @throws NoSuchAlgorithmException
      */
-    private void initAction(@Nullable @NotEmpty final String type)
+    private void initAction(@Nullable @NotEmpty final String type, @Nullable final Function<ProfileRequestContext,
+            BiFunction<ProfileRequestContext,Map<String,Object>,Map<String,Object>>> manipulationStrategy)
                     throws ComponentInitializationException, NoSuchAlgorithmException {
         if ("JWT".equals(type)) {
             action.setAccessTokenTypeLookupStrategy(FunctionSupport.constant("JWT"));
         }
+        if (manipulationStrategy != null) {
+            action.setTokenClaimsSetManipulationStrategyLookupStrategy(manipulationStrategy);
+        }
         action.setDataSealer(getDataSealer());
         action.setClientIDLookupStrategy(FunctionSupport.constant(new ClientID(clientId)));
         action.initialize();
@@ -157,6 +209,7 @@ public class BuildAccessTokenTest extends BaseOIDCResponseActionTest {
      * @param ctx access token context
      * @param scope scope to check for
      * @param audiences audiences to check for
+     * @param customClaims custom claims to check for, may be null
      * 
      * @throws ComponentInitializationException 
      * @throws DataSealerException 
@@ -164,7 +217,7 @@ public class BuildAccessTokenTest extends BaseOIDCResponseActionTest {
      * @throws NoSuchAlgorithmException 
      */
     private void verifyClaims(@Nonnull final AccessTokenContext ctx, @Nonnull final Scope scope,
-            @Nonnull @NonnullElements final Collection<String> audiences)
+            @Nonnull @NonnullElements final Collection<String> audiences, final Map<String, Object> customClaims)
             throws NoSuchAlgorithmException, ParseException, DataSealerException, ComponentInitializationException {
         
         assertEquals(ctx.getLifetime(), Duration.ofMinutes(10));
@@ -182,6 +235,7 @@ public class BuildAccessTokenTest extends BaseOIDCResponseActionTest {
             assertEquals(at.getScope(), scope);
             assertEquals(at.getSubject(), clientId);
             assertEquals(at.getPrincipal(), "jdoe");
+            verifyCustomClaims(at.getClaimsSet(), customClaims);
         } else if (ctx.getJWT() != null) {
             final JWTClaimsSet claims = ctx.getJWT().getJWTClaimsSet();
             assertNotNull(claims);
@@ -194,13 +248,22 @@ public class BuildAccessTokenTest extends BaseOIDCResponseActionTest {
             assertTrue(claims.getIssueTime().toInstant().isBefore(Instant.now()));
             assertEquals(claims.getStringClaim(TokenClaimsSet.KEY_SCOPE), scope.toString());
             assertEquals(claims.getSubject(), clientId);
-            
             final JWTClaimsSet unsealedClaims =
                     JWTClaimsSet.parse(getDataSealer().unwrap(claims.getStringClaim(TokenClaimsSet.KEY_SEALED_FOR_OP)));
             assertEquals(unsealedClaims.getStringClaim(TokenClaimsSet.KEY_USER_PRINCIPAL), "jdoe");
+            verifyCustomClaims(claims, customClaims);
         } else {
             throw new RuntimeException("No token found");
         }
     }
-    
+
+    protected void verifyCustomClaims(final JWTClaimsSet claimsSet, final Map<String, Object> customClaims) {
+        if (customClaims == null) {
+            return;
+        }
+        for (final String claim : customClaims.keySet()) {
+            Assert.assertNotNull(claimsSet.getClaim(claim));
+            Assert.assertEquals(claimsSet.getClaim(claim), customClaims.get(claim));
+        }
+    }
 }
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/BaseOIDCResponseActionTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/BaseOIDCResponseActionTest.java
index f2e0ff6a..1391def6 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/BaseOIDCResponseActionTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/BaseOIDCResponseActionTest.java
@@ -23,6 +23,10 @@ import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Date;
 import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
 import javax.annotation.Nonnull;
 
 import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext;
@@ -226,6 +230,12 @@ public abstract class BaseOIDCResponseActionTest extends OpenSAMLInitBaseTestCas
         
     }
 
+    protected static <K,V> Map<K,V> addEntryToMap(final Map<K,V> map, final K key, final V value) {
+        return Stream.of(map, Map.of(key, value))
+                .flatMap(newMap -> newMap.entrySet().stream())
+                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
+    }
+
     public class MockRevocationCache extends RevocationCache {
 
         boolean revoke;
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetAuthorizationCodeToResponseContextTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetAuthorizationCodeToResponseContextTest.java
index 7dfa45b6..a206fe4b 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetAuthorizationCodeToResponseContextTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetAuthorizationCodeToResponseContextTest.java
@@ -31,8 +31,12 @@ import java.net.URISyntaxException;
 import java.security.NoSuchAlgorithmException;
 import java.text.ParseException;
 import java.time.Instant;
+import java.util.Map;
+import java.util.function.BiFunction;
+import java.util.function.Function;
 
 import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
 import org.springframework.webflow.execution.Event;
 import org.testng.Assert;
 import org.testng.annotations.Test;
@@ -48,6 +52,11 @@ public class SetAuthorizationCodeToResponseContextTest extends BaseOIDCResponseA
     private SetAuthorizationCodeToResponseContext action;
 
     private void init() throws ComponentInitializationException, NoSuchAlgorithmException, URISyntaxException {
+        init(null);
+    }
+
+    private void init(final Function<ProfileRequestContext,
+            BiFunction<ProfileRequestContext,Map<String,Object>,Map<String,Object>>> manipulationStrategy) throws ComponentInitializationException, NoSuchAlgorithmException, URISyntaxException {
         respCtx.setScope(new Scope());
         respCtx.setSubject("subject");
         respCtx.setAuthTime(Instant.now());
@@ -55,11 +64,14 @@ public class SetAuthorizationCodeToResponseContextTest extends BaseOIDCResponseA
         respCtx.setRedirectURI(new URI("http://example.com"));
         action = new SetAuthorizationCodeToResponseContext();
         action.setDataSealer(getDataSealer());
+        if (manipulationStrategy != null) {
+            action.setTokenClaimsSetManipulationStrategyLookupStrategy(manipulationStrategy);
+        }
         action.initialize();
         final SubjectContext subjectCtx = profileRequestCtx.getSubcontext(SubjectContext.class, true);
         subjectCtx.setPrincipalName("userPrin");
     }
-
+    
     /**
      * Basic success case.
      * 
@@ -81,6 +93,20 @@ public class SetAuthorizationCodeToResponseContextTest extends BaseOIDCResponseA
         Assert.assertNotNull(ac);
     }
 
+    @Test
+    public void testSuccessWithCustomClaim() throws ComponentInitializationException, NoSuchAlgorithmException, URISyntaxException,
+            ParseException, DataSealerException {
+        init(prc -> ((prc2, map) -> addEntryToMap(map, "custom_claim", "custom_value")));
+        final Event event = action.execute(requestCtx);
+        ActionTestingSupport.assertProceedEvent(event);
+        Assert.assertNotNull(respCtx.getAuthorizationCode());
+        final AuthorizeCodeClaimsSet ac =
+                AuthorizeCodeClaimsSet.parse(respCtx.getAuthorizationCode().getValue(), getDataSealer());
+        Assert.assertNotNull(ac);
+        Assert.assertNotNull(ac.getClaimsSet().getClaim("custom_claim"));
+        Assert.assertEquals(ac.getClaimsSet().getStringClaim("custom_claim"), "custom_value");
+    }
+
     /**
      * Basic success case plus consent.
      * 
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 fe5468f2..405e3d8e 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
@@ -34,8 +34,12 @@ import java.net.URISyntaxException;
 import java.security.NoSuchAlgorithmException;
 import java.text.ParseException;
 import java.time.Instant;
+import java.util.Map;
+import java.util.function.BiFunction;
+import java.util.function.Function;
 
 import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
 import org.opensaml.storage.RevocationCache;
 import org.opensaml.storage.impl.MemoryStorageService;
 import org.springframework.webflow.execution.Event;
@@ -75,18 +79,28 @@ public class SetRefreshTokenToResponseContextTest extends BaseOIDCResponseAction
                 .setACR(new ACR("0"))
                 .build();
         respCtx.setAuthorizationGrantClaimsSet(claims);
-        action = new SetRefreshTokenToResponseContext(getDataSealer());
         final MemoryStorageService storageService = new MemoryStorageService();
         storageService.setId("id");
         storageService.initialize();
         revocationCache = new RevocationCache();
         revocationCache.setStorage(storageService);
-        action.setRevocationCache(revocationCache);
         enforceRotation = false;
+        action = initAction(null);
+    }
+
+    protected SetRefreshTokenToResponseContext initAction(final Function<ProfileRequestContext,
+            BiFunction<ProfileRequestContext,Map<String,Object>,Map<String,Object>>> manipulationStrategy)
+                    throws ComponentInitializationException, NoSuchAlgorithmException {
+        final SetRefreshTokenToResponseContext action = new SetRefreshTokenToResponseContext(getDataSealer());
+        action.setRevocationCache(revocationCache);
+        if (manipulationStrategy != null) {
+            action.setTokenClaimsSetManipulationStrategyLookupStrategy(manipulationStrategy);
+        }
         action.setEnforceRefreshTokenRotationCondition(prc -> enforceRotation);
         action.initialize();
+        return action;
     }
-
+    
     /**
      * Basic success case.
      * 
@@ -110,6 +124,23 @@ public class SetRefreshTokenToResponseContextTest extends BaseOIDCResponseAction
         Assert.assertEquals(rt.getRootTokenIdentifier(), jit);
     }
 
+    @Test
+    public void testSuccessViaCodeWithCustomClaim() throws ComponentInitializationException, NoSuchAlgorithmException, URISyntaxException,
+            ParseException, DataSealerException {
+        final String jit = respCtx.getAuthorizationGrantClaimsSet().getID();
+        action = initAction(prc -> ((prc2, map) -> addEntryToMap(map, "custom_claim", "custom_value")));
+        final Event event = action.execute(requestCtx);
+        ActionTestingSupport.assertProceedEvent(event);
+        Assert.assertNotNull(respCtx.getRefreshToken());
+        final RefreshTokenClaimsSet rt =
+                RefreshTokenClaimsSet.parse(respCtx.getRefreshToken().getValue(), getDataSealer());
+        Assert.assertNotNull(rt);
+        Assert.assertNotEquals(rt.getID(), jit);
+        Assert.assertEquals(rt.getRootTokenIdentifier(), jit);
+        Assert.assertNotNull(rt.getClaimsSet().getClaim("custom_claim"));
+        Assert.assertEquals(rt.getClaimsSet().getStringClaim("custom_claim"), "custom_value");
+    }
+
     @Test
     public void testSuccessViaRefresh() throws ComponentInitializationException, NoSuchAlgorithmException, URISyntaxException,
             ParseException, DataSealerException {
@@ -141,6 +172,40 @@ public class SetRefreshTokenToResponseContextTest extends BaseOIDCResponseAction
         Assert.assertFalse(revocationCache.isRevoked(RevocationCacheContexts.SINGLE_ACCESS_OR_REFRESH_TOKENS, jit));
     }
 
+    @Test
+    public void testSuccessViaRefreshWithCustomClaim() throws ComponentInitializationException, NoSuchAlgorithmException, URISyntaxException,
+            ParseException, DataSealerException {
+        final String rootTokenId = new SecureRandomIdentifierGenerationStrategy().generateIdentifier();
+        final TokenClaimsSet claims = new RefreshTokenClaimsSet.Builder()
+                .setJWTID(idGenerator)
+                .setClientID(new ClientID())
+                .setIssuer("issuer")
+                .setPrincipal("userPrin")
+                .setSubject("subject")
+                .setIssuedAt(Instant.now())
+                .setExpiresAt(Instant.now())
+                .setAuthenticationTime(Instant.now())
+                .setRedirectURI(new URI("http://example.com"))
+                .setScope(new Scope())
+                .setACR(new ACR("0"))
+                .setRootTokenIdentifier(rootTokenId)
+                .build();
+        final String jit = claims.getID();
+        respCtx.setAuthorizationGrantClaimsSet(claims);
+        action = initAction(prc -> ((prc2, map) -> addEntryToMap(map, "custom_claim", "custom_value")));
+        final Event event = action.execute(requestCtx);
+        ActionTestingSupport.assertProceedEvent(event);
+        Assert.assertNotNull(respCtx.getRefreshToken());
+        final RefreshTokenClaimsSet rt =
+                RefreshTokenClaimsSet.parse(respCtx.getRefreshToken().getValue(), getDataSealer());
+        Assert.assertNotNull(rt);
+        Assert.assertNotEquals(rt.getID(), jit);
+        Assert.assertEquals(rt.getRootTokenIdentifier(), rootTokenId);
+        Assert.assertFalse(revocationCache.isRevoked(RevocationCacheContexts.SINGLE_ACCESS_OR_REFRESH_TOKENS, jit));
+        Assert.assertNotNull(rt.getClaimsSet().getClaim("custom_claim"));
+        Assert.assertEquals(rt.getClaimsSet().getStringClaim("custom_claim"), "custom_value");
+    }
+
     @Test
     public void testSuccessViaRefreshRotationEnforced() throws ComponentInitializationException, NoSuchAlgorithmException, URISyntaxException,
             ParseException, DataSealerException {

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


More information about the commits mailing list