[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