This is an automated email from the ASF dual-hosted git repository.
lmccay pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/knox.git
The following commit(s) were added to refs/heads/master by this push:
new 0c8838511 KNOX-3334 - Introduce ActorChainPrincipal for RFC 8693
instead of ImpersonatedPrincipal (#1257)
0c8838511 is described below
commit 0c8838511f2918b6b13b8293f4db04febe2f5fa0
Author: lmccay <[email protected]>
AuthorDate: Fri Jun 12 14:43:20 2026 -0400
KNOX-3334 - Introduce ActorChainPrincipal for RFC 8693 instead of
ImpersonatedPrincipal (#1257)
* KNOX-3334 - Introduce ActorChainPrincipal for RFC 8693 instead of
ImpersonatedPrincipal
---
.../filter/AbstractIdentityAssertionFilter.java | 8 +-
.../federation/jwt/filter/AbstractJWTFilter.java | 21 +-
.../gateway/service/knoxtoken/TokenResource.java | 37 +++-
.../gateway/service/knoxtoken/ActorChainTest.java | 154 +++++++++++++
.../knox/gateway/security/ActorChainPrincipal.java | 85 +++++++
.../gateway/security/ActorChainPrincipalImpl.java | 95 ++++++++
.../apache/knox/gateway/security/SubjectUtils.java | 4 +
.../services/security/token/JWTokenAttributes.java | 31 ++-
.../security/token/JWTokenAttributesBuilder.java | 33 ++-
.../services/security/token/TokenUtils.java | 130 +++++++++++
.../services/security/token/impl/JWTToken.java | 16 +-
.../services/security/token/TokenUtilsTest.java | 244 +++++++++++++++++++++
12 files changed, 830 insertions(+), 28 deletions(-)
diff --git
a/gateway-provider-identity-assertion-common/src/main/java/org/apache/knox/gateway/identityasserter/common/filter/AbstractIdentityAssertionFilter.java
b/gateway-provider-identity-assertion-common/src/main/java/org/apache/knox/gateway/identityasserter/common/filter/AbstractIdentityAssertionFilter.java
index 2b4de329d..4c212a534 100644
---
a/gateway-provider-identity-assertion-common/src/main/java/org/apache/knox/gateway/identityasserter/common/filter/AbstractIdentityAssertionFilter.java
+++
b/gateway-provider-identity-assertion-common/src/main/java/org/apache/knox/gateway/identityasserter/common/filter/AbstractIdentityAssertionFilter.java
@@ -46,6 +46,7 @@ import
org.apache.knox.gateway.filter.security.AbstractIdentityAssertionBase;
import org.apache.knox.gateway.i18n.GatewaySpiResources;
import org.apache.knox.gateway.i18n.messages.MessagesFactory;
import org.apache.knox.gateway.i18n.resources.ResourcesFactory;
+import org.apache.knox.gateway.security.ActorChainPrincipal;
import org.apache.knox.gateway.security.GroupPrincipal;
import org.apache.knox.gateway.security.ImpersonatedPrincipal;
import org.apache.knox.gateway.security.PrimaryPrincipal;
@@ -147,6 +148,11 @@ public abstract class AbstractIdentityAssertionFilter
extends
final Set<TokenIdPrincipal> tokenIdPrincipals =
SubjectUtils.getTokenIdPrincipals(currentSubject);
subject.getPrincipals().addAll(tokenIdPrincipals);
+ // RFC 8693 Token Exchange: Preserve ActorChainPrincipal from the
current subject
+ // This ensures the delegation chain is maintained through identity
assertion
+ final Set<ActorChainPrincipal> actorChainPrincipals =
SubjectUtils.getActorChainPrincipal(currentSubject, subject);
+ subject.getPrincipals().addAll(actorChainPrincipals);
+
doAs(request, response, chain, subject);
}
else {
@@ -154,7 +160,7 @@ public abstract class AbstractIdentityAssertionFilter
extends
}
}
- private void doAs(final ServletRequest request, final ServletResponse
response, final FilterChain chain, Subject subject)
+ private void doAs(final ServletRequest request, final ServletResponse
response, final FilterChain chain, Subject subject)
throws IOException, ServletException {
try {
Subject.doAs(
diff --git
a/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/AbstractJWTFilter.java
b/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/AbstractJWTFilter.java
index f3963c523..de4987caa 100644
---
a/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/AbstractJWTFilter.java
+++
b/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/AbstractJWTFilter.java
@@ -35,6 +35,7 @@ import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
+import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -67,6 +68,7 @@ import org.apache.knox.gateway.config.GatewayConfig;
import org.apache.knox.gateway.filter.AbstractGatewayFilter;
import org.apache.knox.gateway.i18n.messages.MessagesFactory;
import org.apache.knox.gateway.provider.federation.jwt.JWTMessages;
+import org.apache.knox.gateway.security.ActorChainPrincipalImpl;
import org.apache.knox.gateway.security.PrimaryPrincipal;
import org.apache.knox.gateway.security.SubjectUtils;
import org.apache.knox.gateway.security.TokenIdPrincipal;
@@ -386,6 +388,8 @@ public abstract class AbstractJWTFilter implements Filter {
if (expectedPrincipalClaim != null) {
claimvalue = token.getClaim(expectedPrincipalClaim);
}
+ // Extract actor chain from the JWT token if present (RFC 8693)
+ List<Map<String, Object>> actorChain = TokenUtils.extractActorChain(token);
// The newly constructed Sets check whether this Subject has been set
read-only
// before permitting subsequent modifications. The newly created Sets also
prevent
// illegal modifications by ensuring that callers have sufficient
permissions.
@@ -393,7 +397,7 @@ public abstract class AbstractJWTFilter implements Filter {
// To modify the Principals Set, the caller must have
AuthPermission("modifyPrincipals").
// To modify the public credential Set, the caller must have
AuthPermission("modifyPublicCredentials").
// To modify the private credential Set, the caller must have
AuthPermission("modifyPrivateCredentials").
- return createSubjectFromTokenData(principal, claimvalue);
+ return createSubjectFromTokenData(principal, claimvalue, null, actorChain);
}
public Subject createSubjectFromTokenIdentifier(final String tokenId) throws
UnknownTokenException {
@@ -419,11 +423,19 @@ public abstract class AbstractJWTFilter implements Filter
{
@SuppressWarnings("rawtypes")
protected Subject createSubjectFromTokenData(final String principal, final
String expectedPrincipalClaimValue) {
- return createSubjectFromTokenData(principal, expectedPrincipalClaimValue,
null);
+ return createSubjectFromTokenData(principal, expectedPrincipalClaimValue,
null, null);
}
@SuppressWarnings("rawtypes")
protected Subject createSubjectFromTokenData(final String principal, final
String expectedPrincipalClaimValue, final String tokenId) {
+ return createSubjectFromTokenData(principal, expectedPrincipalClaimValue,
tokenId, null);
+ }
+
+ @SuppressWarnings("rawtypes")
+ protected Subject createSubjectFromTokenData(final String principal,
+ final String
expectedPrincipalClaimValue,
+ final String tokenId,
+ final List<Map<String, Object>>
actorChain) {
String claimValue =
(expectedPrincipalClaimValue != null) ?
expectedPrincipalClaimValue.toLowerCase(Locale.ROOT) : null;
@@ -435,6 +447,11 @@ public abstract class AbstractJWTFilter implements Filter {
principals.add(new TokenIdPrincipal(tokenId));
}
+ // Add ActorChainPrincipal if an actor chain is present (RFC 8693 token
exchange)
+ if (actorChain != null && !actorChain.isEmpty()) {
+ principals.add(new ActorChainPrincipalImpl(actorChain));
+ }
+
// The newly constructed Sets check whether this Subject has been set
read-only
// before permitting subsequent modifications. The newly created Sets also
prevent
// illegal modifications by ensuring that callers have sufficient
permissions.
diff --git
a/gateway-service-knoxtoken/src/main/java/org/apache/knox/gateway/service/knoxtoken/TokenResource.java
b/gateway-service-knoxtoken/src/main/java/org/apache/knox/gateway/service/knoxtoken/TokenResource.java
index c0ec90b9d..8da6f137b 100644
---
a/gateway-service-knoxtoken/src/main/java/org/apache/knox/gateway/service/knoxtoken/TokenResource.java
+++
b/gateway-service-knoxtoken/src/main/java/org/apache/knox/gateway/service/knoxtoken/TokenResource.java
@@ -70,6 +70,7 @@ import org.apache.commons.lang3.StringUtils;
import org.apache.knox.gateway.config.GatewayConfig;
import org.apache.knox.gateway.context.ContextAttributes;
import org.apache.knox.gateway.i18n.messages.MessagesFactory;
+import org.apache.knox.gateway.security.ActorChainPrincipal;
import org.apache.knox.gateway.security.GroupPrincipal;
import org.apache.knox.gateway.security.SubjectUtils;
import org.apache.knox.gateway.security.TokenIdPrincipal;
@@ -1106,21 +1107,41 @@ public class TokenResource {
jwtAttributesBuilder.setClientId(tokenIdPrincipals.iterator().next().getName());
}
- // RFC 8693 Token Exchange: Add the "act" claim if delegated auth is
enabled and impersonation occurred
- if (enableDelegatedAuth && SubjectUtils.isImpersonating(subject)) {
+ // RFC 8693 Token Exchange: Build the actor chain if delegated auth is
enabled
+ handleDelegatedAuthentication(subject, jwtAttributesBuilder);
+ }
+
+ jwtAttributes = jwtAttributesBuilder.build();
+ token = ts.issueToken(jwtAttributes);
+ return token;
+ }
+
+ private void handleDelegatedAuthentication(Subject subject,
JWTokenAttributesBuilder jwtAttributesBuilder) {
+ if (enableDelegatedAuth) {
+ // First check if there's an existing actor chain from a previous token
exchange
+ Set<ActorChainPrincipal> actorChainPrincipals =
subject.getPrincipals(ActorChainPrincipal.class);
+ List<Map<String, Object>> existingChain = null;
+ if (!actorChainPrincipals.isEmpty()) {
+ existingChain = actorChainPrincipals.iterator().next().getActorChain();
+ log.generalInfoMessage("Found existing actor chain with " +
existingChain.size() + " actors");
+ }
+
+ // Check if impersonation is occurring to add a new actor to the chain
+ if (SubjectUtils.isImpersonating(subject)) {
String primaryPrincipalName =
SubjectUtils.getPrimaryPrincipalName(subject);
String impersonatedPrincipalName =
SubjectUtils.getImpersonatedPrincipalName(subject);
if (primaryPrincipalName != null && impersonatedPrincipalName != null
&& !primaryPrincipalName.equals(impersonatedPrincipalName)) {
- // The primary principal (the one doing the impersonation) becomes
the actor
- jwtAttributesBuilder.setActor(primaryPrincipalName);
+ // Build the new actor chain by adding the current actor (primary
principal) to the existing chain
+ List<Map<String, Object>> newActorChain =
TokenUtils.addActorToChain(existingChain, primaryPrincipalName);
+ jwtAttributesBuilder.setActorChain(newActorChain);
log.addingActorClaimToToken(primaryPrincipalName,
impersonatedPrincipalName);
}
+ } else if (existingChain != null && !existingChain.isEmpty()) {
+ // No new impersonation, but preserve existing actor chain
+ jwtAttributesBuilder.setActorChain(existingChain);
+ log.generalInfoMessage("Preserving existing actor chain without adding
new actor");
}
}
-
- jwtAttributes = jwtAttributesBuilder.build();
- token = ts.issueToken(jwtAttributes);
- return token;
}
private boolean shouldIncludeGroups() {
diff --git
a/gateway-service-knoxtoken/src/test/java/org/apache/knox/gateway/service/knoxtoken/ActorChainTest.java
b/gateway-service-knoxtoken/src/test/java/org/apache/knox/gateway/service/knoxtoken/ActorChainTest.java
new file mode 100644
index 000000000..22467696d
--- /dev/null
+++
b/gateway-service-knoxtoken/src/test/java/org/apache/knox/gateway/service/knoxtoken/ActorChainTest.java
@@ -0,0 +1,154 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.knox.gateway.service.knoxtoken;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.knox.gateway.security.ActorChainPrincipal;
+import org.apache.knox.gateway.security.ActorChainPrincipalImpl;
+import
org.apache.knox.gateway.services.security.token.JWTokenAttributesBuilder;
+import org.apache.knox.gateway.services.security.token.TokenUtils;
+import org.apache.knox.gateway.services.security.token.impl.JWT;
+import org.apache.knox.gateway.services.security.token.impl.JWTToken;
+import org.junit.Test;
+
+/**
+ * Test class to verify RFC 8693 actor chain functionality end-to-end.
+ */
+public class ActorChainTest {
+
+ @Test
+ public void testActorChainPreservation() throws Exception {
+ // Step 1: Create initial token with actor chain (simulating token from
previous exchange)
+ List<Map<String, Object>> initialChain = new ArrayList<>();
+
+ Map<String, Object> actor1 = new LinkedHashMap<>();
+ actor1.put("sub", "service-a");
+ actor1.put("iss", "https://issuer.example.com");
+ initialChain.add(actor1);
+
+ Map<String, Object> actor2 = new LinkedHashMap<>();
+ actor2.put("sub", "service-b");
+ initialChain.add(actor2);
+
+ JWTokenAttributesBuilder builder1 = new JWTokenAttributesBuilder();
+ builder1.setUserName("end-user")
+ .setAlgorithm("RS256")
+ .setExpires(System.currentTimeMillis() + 30000)
+ .setActorChain(initialChain);
+
+ JWTToken token1 = new JWTToken(builder1.build());
+
+ // Step 2: Extract actor chain from token (simulating JWT validation)
+ List<Map<String, Object>> extractedChain =
TokenUtils.extractActorChain(token1);
+ assertNotNull("Extracted chain should not be null", extractedChain);
+ assertEquals("Should have 2 actors", 2, extractedChain.size());
+ assertEquals("First actor should be service-a", "service-a",
extractedChain.get(0).get("sub"));
+ assertEquals("Second actor should be service-b", "service-b",
extractedChain.get(1).get("sub"));
+
+ // Step 3: Create ActorChainPrincipal (simulating what AbstractJWTFilter
does)
+ ActorChainPrincipal actorChainPrincipal = new
ActorChainPrincipalImpl(extractedChain);
+ assertEquals("Current actor should be service-a", "service-a",
actorChainPrincipal.getCurrentActor());
+ assertEquals("Original delegator should be service-b", "service-b",
actorChainPrincipal.getOriginalDelegator());
+
+ // Step 4: Add new actor to chain (simulating what TokenResource does)
+ String newActor = "service-c";
+ List<Map<String, Object>> newChain =
TokenUtils.addActorToChain(extractedChain, newActor);
+ assertEquals("Should have 3 actors", 3, newChain.size());
+ assertEquals("New actor should be first", newActor,
newChain.get(0).get("sub"));
+ assertEquals("Previous first actor should be second", "service-a",
newChain.get(1).get("sub"));
+ assertEquals("Previous second actor should be third", "service-b",
newChain.get(2).get("sub"));
+
+ // Step 5: Create new token with extended chain
+ JWTokenAttributesBuilder builder2 = new JWTokenAttributesBuilder();
+ builder2.setUserName("end-user")
+ .setAlgorithm("RS256")
+ .setExpires(System.currentTimeMillis() + 30000)
+ .setActorChain(newChain);
+
+ JWTToken token2 = new JWTToken(builder2.build());
+
+ // Step 6: Verify the new token has the complete chain
+ List<Map<String, Object>> finalChain =
TokenUtils.extractActorChain(token2);
+ assertNotNull("Final chain should not be null", finalChain);
+ assertEquals("Should have 3 actors in final token", 3, finalChain.size());
+ assertEquals("First actor should be service-c", "service-c",
finalChain.get(0).get("sub"));
+ assertEquals("Second actor should be service-a", "service-a",
finalChain.get(1).get("sub"));
+ assertEquals("Third actor should be service-b", "service-b",
finalChain.get(2).get("sub"));
+
+ // Verify issuer is preserved for actor1
+ assertEquals("Issuer should be preserved", "https://issuer.example.com",
finalChain.get(1).get("iss"));
+ }
+
+ @Test
+ public void testActorChainInNestedStructure() throws ParseException {
+ // Create a token with nested actor structure
+ List<Map<String, Object>> chain = new ArrayList<>();
+
+ Map<String, Object> actor1 = new LinkedHashMap<>();
+ actor1.put("sub", "actor1");
+ chain.add(actor1);
+
+ Map<String, Object> actor2 = new LinkedHashMap<>();
+ actor2.put("sub", "actor2");
+ chain.add(actor2);
+
+ Map<String, Object> actor3 = new LinkedHashMap<>();
+ actor3.put("sub", "actor3");
+ chain.add(actor3);
+
+ JWTokenAttributesBuilder builder = new JWTokenAttributesBuilder();
+ builder.setUserName("testuser")
+ .setAlgorithm("RS256")
+ .setExpires(System.currentTimeMillis() + 30000)
+ .setActorChain(chain);
+
+ JWT token = new JWTToken(builder.build());
+
+ // Verify the act claim is present
+ Object actClaim = token.getClaimAsObject(JWTToken.ACT_CLAIM);
+ assertNotNull("Act claim should be present", actClaim);
+ assertTrue("Act claim should be a Map", actClaim instanceof Map);
+
+ // Verify nested structure
+ Map<?, ?> level1 = (Map<?, ?>) actClaim;
+ assertEquals("Level 1 sub should be actor1", "actor1", level1.get("sub"));
+
+ Object level2Act = level1.get(JWTToken.ACT_CLAIM);
+ assertNotNull("Level 2 act should be present", level2Act);
+ assertTrue("Level 2 act should be a Map", level2Act instanceof Map);
+
+ Map<?, ?> level2 = (Map<?, ?>) level2Act;
+ assertEquals("Level 2 sub should be actor2", "actor2", level2.get("sub"));
+
+ Object level3Act = level2.get(JWTToken.ACT_CLAIM);
+ assertNotNull("Level 3 act should be present", level3Act);
+ assertTrue("Level 3 act should be a Map", level3Act instanceof Map);
+
+ Map<?, ?> level3 = (Map<?, ?>) level3Act;
+ assertEquals("Level 3 sub should be actor3", "actor3", level3.get("sub"));
+ }
+}
diff --git
a/gateway-spi/src/main/java/org/apache/knox/gateway/security/ActorChainPrincipal.java
b/gateway-spi/src/main/java/org/apache/knox/gateway/security/ActorChainPrincipal.java
new file mode 100644
index 000000000..3d2d99fef
--- /dev/null
+++
b/gateway-spi/src/main/java/org/apache/knox/gateway/security/ActorChainPrincipal.java
@@ -0,0 +1,85 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.knox.gateway.security;
+
+import java.security.Principal;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A Principal that represents the chain of actors (delegation chain) from RFC
8693 token exchange.
+ *
+ * <p>This principal is used to represent the 'act' claim chain from a JWT,
which provides
+ * a means to express that delegation has occurred and identify the acting
parties to whom
+ * authority has been delegated.</p>
+ *
+ * <p>The actor chain is ordered from most recent (current actor) to oldest
(original delegator).
+ * Each actor in the chain is represented as a Map containing identity claims.
According to
+ * RFC 8693 Section 4.1:</p>
+ * <ul>
+ * <li>Identity claims such as 'sub' (subject) and 'iss' (issuer) should be
used to identify actors</li>
+ * <li>The combination of 'iss' and 'sub' may be necessary to uniquely
identify an actor</li>
+ * <li>Non-identity claims (e.g., 'exp', 'nbf', 'aud') are NOT meaningful
within 'act' claims
+ * and should not be used</li>
+ * </ul>
+ *
+ * <p>Example chain structure:</p>
+ * <pre>
+ * [
+ * {"sub": "service-c", "iss": "https://issuer.example.com"}, // Most
recent actor
+ * {"sub": "service-b", "iss": "https://issuer.example.com"}, // Previous
actor
+ * {"sub": "service-a"} // Original
delegator
+ * ]
+ * </pre>
+ *
+ * @see <a
href="https://datatracker.ietf.org/doc/html/rfc8693#section-4.1">RFC 8693
Section 4.1 - Actor Claim</a>
+ */
+public interface ActorChainPrincipal extends Principal {
+
+ /**
+ * Returns the chain of actors in order from most recent to oldest.
+ *
+ * <p>Each Map in the list represents an actor's claims, with at minimum a
'sub' claim.
+ * The first element in the list is the most recent actor (the one who
directly
+ * performed the current delegation), and the last element is the original
delegator.</p>
+ *
+ * @return an immutable list of actor claim maps, never null but may be empty
+ */
+ List<Map<String, Object>> getActorChain();
+
+ /**
+ * Returns the subject (identity) of the most recent actor in the chain.
+ *
+ * <p>This is equivalent to calling {@code getActorChain().get(0).get("sub")}
+ * if the chain is not empty.</p>
+ *
+ * @return the subject of the most recent actor, or null if the chain is
empty
+ */
+ String getCurrentActor();
+
+ /**
+ * Returns the subject (identity) of the original delegator (the first actor
in the chain).
+ *
+ * <p>This is the actor who initiated the delegation chain. It is equivalent
to calling
+ * {@code getActorChain().get(getActorChain().size() - 1).get("sub")}
+ * if the chain is not empty.</p>
+ *
+ * @return the subject of the original delegator, or null if the chain is
empty
+ */
+ String getOriginalDelegator();
+}
diff --git
a/gateway-spi/src/main/java/org/apache/knox/gateway/security/ActorChainPrincipalImpl.java
b/gateway-spi/src/main/java/org/apache/knox/gateway/security/ActorChainPrincipalImpl.java
new file mode 100644
index 000000000..36989ad94
--- /dev/null
+++
b/gateway-spi/src/main/java/org/apache/knox/gateway/security/ActorChainPrincipalImpl.java
@@ -0,0 +1,95 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.knox.gateway.security;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Implementation of ActorChainPrincipal that holds the RFC 8693 actor
delegation chain.
+ */
+public class ActorChainPrincipalImpl implements ActorChainPrincipal {
+ private final List<Map<String, Object>> actorChain;
+
+ /**
+ * Creates a new ActorChainPrincipal with the specified actor chain.
+ *
+ * @param actorChain the list of actor claim maps, ordered from most recent
to oldest
+ */
+ public ActorChainPrincipalImpl(List<Map<String, Object>> actorChain) {
+ this.actorChain = actorChain == null
+ ? Collections.emptyList()
+ : Collections.unmodifiableList(new ArrayList<>(actorChain));
+ }
+
+ @Override
+ public List<Map<String, Object>> getActorChain() {
+ return actorChain;
+ }
+
+ @Override
+ public String getCurrentActor() {
+ if (actorChain.isEmpty()) {
+ return null;
+ }
+ Object sub = actorChain.get(0).get("sub");
+ return sub != null ? sub.toString() : null;
+ }
+
+ @Override
+ public String getOriginalDelegator() {
+ if (actorChain.isEmpty()) {
+ return null;
+ }
+ Object sub = actorChain.get(actorChain.size() - 1).get("sub");
+ return sub != null ? sub.toString() : null;
+ }
+
+ @Override
+ public String getName() {
+ // Return the current actor's identity as the principal name
+ String currentActor = getCurrentActor();
+ return currentActor != null ? currentActor : "";
+ }
+
+ @Override
+ public String toString() {
+ return "ActorChainPrincipal[actors=" + actorChain.size() + ", current=" +
getCurrentActor() + "]";
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+
+ if (!(o instanceof ActorChainPrincipal)) {
+ return false;
+ }
+
+ ActorChainPrincipal that = (ActorChainPrincipal) o;
+ return actorChain.equals(that.getActorChain());
+ }
+
+ @Override
+ public int hashCode() {
+ return actorChain.hashCode();
+ }
+}
diff --git
a/gateway-spi/src/main/java/org/apache/knox/gateway/security/SubjectUtils.java
b/gateway-spi/src/main/java/org/apache/knox/gateway/security/SubjectUtils.java
index 27cfaea29..11db9113f 100644
---
a/gateway-spi/src/main/java/org/apache/knox/gateway/security/SubjectUtils.java
+++
b/gateway-spi/src/main/java/org/apache/knox/gateway/security/SubjectUtils.java
@@ -101,4 +101,8 @@ public class SubjectUtils {
public static Set<TokenIdPrincipal> getTokenIdPrincipals(Subject subject) {
return subject.getPrincipals(TokenIdPrincipal.class);
}
+
+ public static Set<ActorChainPrincipal> getActorChainPrincipal(Subject
currentSubject, Subject subject) {
+ return currentSubject.getPrincipals(ActorChainPrincipal.class);
+ }
}
diff --git
a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/JWTokenAttributes.java
b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/JWTokenAttributes.java
index 1d19a846d..c41f983eb 100644
---
a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/JWTokenAttributes.java
+++
b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/JWTokenAttributes.java
@@ -21,6 +21,7 @@ import java.net.URI;
import java.net.URISyntaxException;
import java.util.Date;
import java.util.List;
+import java.util.Map;
import java.util.Set;
public class JWTokenAttributes {
@@ -40,7 +41,7 @@ public class JWTokenAttributes {
private final String issuer;
private String kid;
private final String clientId;
- private final String actor;
+ private final List<Map<String, Object>> actorChain;
JWTokenAttributes(String userName, List<String> audiences, String algorithm,
long expires, String signingKeystoreName, String signingKeystoreAlias,
char[] signingKeystorePassphrase, boolean managed, String jku, String
type, Set<String> groups, String kid, String issuer) {
@@ -53,7 +54,7 @@ public class JWTokenAttributes {
}
JWTokenAttributes(String userName, List<String> audiences, String algorithm,
long expires, String signingKeystoreName, String signingKeystoreAlias,
- char[] signingKeystorePassphrase, boolean managed, String jku, String
type, Set<String> groups, String kid, String issuer, String clientId, String
actor) {
+ char[] signingKeystorePassphrase, boolean managed, String jku, String
type, Set<String> groups, String kid, String issuer, String clientId,
List<Map<String, Object>> actorChain) {
this.userName = userName;
this.audiences = audiences;
this.algorithm = algorithm;
@@ -68,7 +69,7 @@ public class JWTokenAttributes {
this.kid = kid;
this.issuer = issuer;
this.clientId = clientId;
- this.actor = actor;
+ this.actorChain = actorChain;
}
public String getUserName() {
@@ -143,7 +144,27 @@ public class JWTokenAttributes {
return clientId;
}
- public String getActor() {
- return actor;
+ /**
+ * Get the actor chain for RFC 8693 token exchange.
+ *
+ * <p>The actor chain represents the complete delegation history for this
token.
+ * Each element in the list is a Map of identity claims (such as 'sub' and
'iss')
+ * that identify an actor in the delegation chain. The list is ordered from
most
+ * recent actor (first element) to oldest actor (last element).</p>
+ *
+ * <p>According to RFC 8693 Section 4.1, identity claims within the 'act'
claim
+ * identify the actor. Common claims include:</p>
+ * <ul>
+ * <li>'sub' - the subject/identity of the actor</li>
+ * <li>'iss' - the issuer of the actor's identity</li>
+ * </ul>
+ *
+ * <p>Non-identity claims (e.g., 'exp', 'nbf', 'aud') should NOT be used
within
+ * actor claims as they are not relevant to the validity of the containing
JWT.</p>
+ *
+ * @return the actor chain, or null if no delegation has occurred
+ */
+ public List<Map<String, Object>> getActorChain() {
+ return actorChain;
}
}
diff --git
a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/JWTokenAttributesBuilder.java
b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/JWTokenAttributesBuilder.java
index 50896e61a..b70a84e6e 100644
---
a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/JWTokenAttributesBuilder.java
+++
b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/JWTokenAttributesBuilder.java
@@ -20,6 +20,7 @@ package org.apache.knox.gateway.services.security.token;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
+import java.util.Map;
import java.util.Set;
public class JWTokenAttributesBuilder {
@@ -38,7 +39,7 @@ public class JWTokenAttributesBuilder {
private String kid;
private String issuer = JWTokenAttributes.DEFAULT_ISSUER;
private String clientId;
- private String actor;
+ private List<Map<String, Object>> actorChain;
public JWTokenAttributesBuilder setUserName(String userName) {
this.userName = userName;
@@ -114,13 +115,37 @@ public class JWTokenAttributesBuilder {
return this;
}
- public JWTokenAttributesBuilder setActor(String actor) {
- this.actor = actor;
+ /**
+ * Set the actor chain for RFC 8693 token exchange.
+ *
+ * <p>The actor chain represents the complete delegation history. Each
element in the list
+ * is a Map of identity claims that identify an actor. The list should be
ordered from most
+ * recent actor (first element) to oldest actor (last element).</p>
+ *
+ * <p>According to RFC 8693 Section 4.1, each actor should be identified
using identity claims
+ * such as 'sub' (subject) and 'iss' (issuer). Non-identity claims (e.g.,
'exp', 'nbf', 'aud')
+ * should NOT be included.</p>
+ *
+ * <p>Example:</p>
+ * <pre>
+ * List<Map<String, Object>> chain = new ArrayList<>();
+ * Map<String, Object> actor1 = new HashMap<>();
+ * actor1.put("sub", "service-a");
+ * actor1.put("iss", "https://issuer.example.com");
+ * chain.add(actor1);
+ * builder.setActorChain(chain);
+ * </pre>
+ *
+ * @param actorChain the actor chain, ordered from most recent to oldest
+ * @return this builder
+ */
+ public JWTokenAttributesBuilder setActorChain(List<Map<String, Object>>
actorChain) {
+ this.actorChain = actorChain;
return this;
}
public JWTokenAttributes build() {
return new JWTokenAttributes(userName, (audiences == null ? new
ArrayList<>() : audiences), algorithm, expires, signingKeystoreName,
signingKeystoreAlias,
- signingKeystorePassphrase, managed, jku, type, groups, kid, issuer,
clientId, actor);
+ signingKeystorePassphrase, managed, jku, type, groups, kid, issuer,
clientId, actorChain);
}
}
diff --git
a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenUtils.java
b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenUtils.java
index e9620a425..72fb7a25f 100644
---
a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenUtils.java
+++
b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenUtils.java
@@ -30,7 +30,11 @@ import
org.apache.knox.gateway.services.security.token.impl.JWTToken;
import javax.servlet.FilterConfig;
import javax.servlet.ServletContext;
import java.security.interfaces.RSAPublicKey;
+import java.util.ArrayList;
+import java.util.Collections;
import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
public class TokenUtils {
public static final String ATTR_CURRENT_KNOXSSO_COOKIE_TOKEN_ID =
"currentKnoxSsoCookieTokenId";
@@ -114,4 +118,130 @@ public class TokenUtils {
return hmacSecret != null && StringUtils.isBlank(signingKeystoreName);
}
+ /**
+ * Extract the actor chain from an RFC 8693 'act' claim in a JWT token.
+ *
+ * <p>The 'act' claim in RFC 8693 represents a delegation chain where each
actor
+ * delegated authority to the next. The claim is structured as a nested JSON
object,
+ * where each level contains identity claims (such as 'sub' and 'iss') and
potentially
+ * another nested 'act' claim.</p>
+ *
+ * <p>According to RFC 8693 Section 4.1, the 'act' claim contains identity
claims that
+ * identify the actor. Common identity claims include:</p>
+ * <ul>
+ * <li>'sub' - the subject/identity of the actor</li>
+ * <li>'iss' - the issuer of the actor's identity</li>
+ * </ul>
+ *
+ * <p>Non-identity claims (e.g., 'exp', 'nbf', 'aud') are not relevant to
the validity
+ * of the containing JWT and should not be used within 'act' claims.</p>
+ *
+ * <p>Example JWT 'act' claim structure:</p>
+ * <pre>
+ * {
+ * "sub": "service-a",
+ * "iss": "https://issuer.example.com",
+ * "act": {
+ * "sub": "service-b",
+ * "act": {
+ * "sub": "service-c"
+ * }
+ * }
+ * }
+ * </pre>
+ *
+ * <p>This method flattens this nested structure into a list ordered from
most recent
+ * actor (service-a) to oldest (service-c).</p>
+ *
+ * @param token The JWT token to extract the actor chain from
+ * @return A list of actor claim maps, ordered from most recent to oldest;
empty list if no 'act' claim exists
+ */
+ @SuppressWarnings("unchecked")
+ public static List<Map<String, Object>> extractActorChain(JWT token) {
+ Object currentAct = token == null ? null :
token.getClaimAsObject(JWTToken.ACT_CLAIM);
+ final List<Map<String, Object>> actorChain = new ArrayList<>();
+
+ while (currentAct instanceof Map<?, ?> actorMap) {
+ actorChain.add(new LinkedHashMap<>((Map<String, Object>) actorMap));
+ currentAct = actorMap.get(JWTToken.ACT_CLAIM);
+ }
+
+ return Collections.unmodifiableList(actorChain);
+ }
+
+ /**
+ * Build a new actor chain by adding a new actor to an existing chain.
+ *
+ * <p>This creates a new list with the new actor as the first element (most
recent),
+ * followed by all actors from the existing chain.</p>
+ *
+ * @param existingChain The existing actor chain (may be null or empty)
+ * @param newActor The new actor to add to the chain
+ * @return A new immutable list with the complete actor chain
+ */
+ public static List<Map<String, Object>> addActorToChain(List<Map<String,
Object>> existingChain, String newActor) {
+ if (newActor == null || newActor.isEmpty()) {
+ return existingChain == null ? Collections.emptyList() : existingChain;
+ }
+
+ List<Map<String, Object>> newChain = new ArrayList<>();
+
+ // Add the new actor as the first element
+ Map<String, Object> newActorClaim = new LinkedHashMap<>();
+ newActorClaim.put("sub", newActor);
+ newChain.add(newActorClaim);
+
+ // Add all existing actors
+ if (existingChain != null && !existingChain.isEmpty()) {
+ newChain.addAll(existingChain);
+ }
+
+ return Collections.unmodifiableList(newChain);
+ }
+
+ /**
+ * Convert an actor chain list into the nested structure required for the
JWT 'act' claim.
+ *
+ * <p>This method takes a flat list of actor claim maps (ordered from most
recent to oldest)
+ * and converts it into the nested structure required by RFC 8693.</p>
+ *
+ * <p>Example transformation:</p>
+ * <pre>
+ * Input: [{"sub": "actor1"}, {"sub": "actor2"}, {"sub": "actor3"}]
+ * Output: {
+ * "sub": "actor1",
+ * "act": {
+ * "sub": "actor2",
+ * "act": {
+ * "sub": "actor3"
+ * }
+ * }
+ * }
+ * </pre>
+ *
+ * @param actorChain The flat list of actor claim maps, ordered from most
recent to oldest
+ * @return The nested structure for the 'act' claim, or null if the chain is
empty
+ */
+ @SuppressWarnings("unchecked")
+ public static Map<String, Object> buildNestedActClaim(List<Map<String,
Object>> actorChain) {
+ if (actorChain == null || actorChain.isEmpty()) {
+ return null;
+ }
+
+ // Start from the last actor (oldest) and work backwards
+ Map<String, Object> nestedAct = null;
+ for (int i = actorChain.size() - 1; i >= 0; i--) {
+ Map<String, Object> currentActor = new
LinkedHashMap<>(actorChain.get(i));
+
+ if (nestedAct != null) {
+ // Add the previously built nested structure as the 'act' claim
+ currentActor.put(JWTToken.ACT_CLAIM, nestedAct);
+ }
+
+ nestedAct = currentActor;
+ }
+
+ return nestedAct;
+ }
+
}
diff --git
a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/impl/JWTToken.java
b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/impl/JWTToken.java
index 27f8f6ced..b43a0b29a 100644
---
a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/impl/JWTToken.java
+++
b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/impl/JWTToken.java
@@ -19,6 +19,7 @@ package org.apache.knox.gateway.services.security.token.impl;
import java.net.URISyntaxException;
import java.text.ParseException;
import java.util.Date;
+import java.util.Map;
import java.util.UUID;
import com.nimbusds.jose.JOSEException;
@@ -33,6 +34,7 @@ import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
import org.apache.knox.gateway.i18n.messages.MessagesFactory;
import org.apache.knox.gateway.services.security.token.JWTokenAttributes;
+import org.apache.knox.gateway.services.security.token.TokenUtils;
public class JWTToken implements JWT {
private static JWTProviderMessages log = MessagesFactory.get(
JWTProviderMessages.class );
@@ -100,14 +102,12 @@ public class JWTToken implements JWT {
if (jwtAttributes.getClientId() != null) {
builder.claim(CLIENT_ID_CLAIM, jwtAttributes.getClientId());
}
- if (jwtAttributes.getActor() != null) {
- // RFC 8693 Token Exchange: The "act" (actor) claim provides a means
within a JWT to express
- // that delegation has occurred and identify the acting party to whom
authority has been delegated.
- // The act claim value is a JSON object containing a "sub" claim with
the identity of the actor.
- JWTClaimsSet actClaims = new JWTClaimsSet.Builder()
- .subject(jwtAttributes.getActor())
- .build();
- builder.claim(ACT_CLAIM, actClaims.toJSONObject());
+ // RFC 8693 Token Exchange: The "act" (actor) claim provides a means
within a JWT to express
+ // that delegation has occurred and identify the acting party to whom
authority has been delegated.
+ // The actor chain is converted to the nested structure required by RFC
8693.
+ Map<String, Object> nestedAct =
TokenUtils.buildNestedActClaim(jwtAttributes.getActorChain());
+ if (nestedAct != null) {
+ builder.claim(ACT_CLAIM, nestedAct);
}
// Add a private UUID claim for uniqueness
diff --git
a/gateway-spi/src/test/java/org/apache/knox/gateway/services/security/token/TokenUtilsTest.java
b/gateway-spi/src/test/java/org/apache/knox/gateway/services/security/token/TokenUtilsTest.java
new file mode 100644
index 000000000..a0967e3c9
--- /dev/null
+++
b/gateway-spi/src/test/java/org/apache/knox/gateway/services/security/token/TokenUtilsTest.java
@@ -0,0 +1,244 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * 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 org.apache.knox.gateway.services.security.token;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.knox.gateway.services.security.token.impl.JWTToken;
+import org.junit.Test;
+
+/**
+ * Test class for TokenUtils, focusing on RFC 8693 actor chain functionality.
+ */
+public class TokenUtilsTest {
+
+ @Test
+ public void testExtractActorChain_NoActClaim() throws Exception {
+ // Create a JWT without an 'act' claim
+ JWTokenAttributesBuilder builder = new JWTokenAttributesBuilder();
+ builder.setUserName("testuser")
+ .setAlgorithm("RS256")
+ .setExpires(System.currentTimeMillis() + 30000);
+
+ JWTokenAttributes attributes = builder.build();
+ JWTToken token = new JWTToken(attributes);
+
+ List<Map<String, Object>> actorChain = TokenUtils.extractActorChain(token);
+ assertNotNull(actorChain);
+ assertTrue(actorChain.isEmpty());
+ }
+
+ @Test
+ public void testExtractActorChain_SingleActor() throws Exception {
+ // Create a JWT with a single actor in the chain
+ List<Map<String, Object>> chain = new ArrayList<>();
+ Map<String, Object> actor1 = new LinkedHashMap<>();
+ actor1.put("sub", "actor1");
+ chain.add(actor1);
+
+ JWTokenAttributesBuilder builder = new JWTokenAttributesBuilder();
+ builder.setUserName("testuser")
+ .setAlgorithm("RS256")
+ .setExpires(System.currentTimeMillis() + 30000)
+ .setActorChain(chain);
+
+ JWTokenAttributes attributes = builder.build();
+ JWTToken token = new JWTToken(attributes);
+
+ List<Map<String, Object>> actorChain = TokenUtils.extractActorChain(token);
+ assertNotNull(actorChain);
+ assertEquals(1, actorChain.size());
+ assertEquals("actor1", actorChain.get(0).get("sub"));
+ }
+
+ @Test
+ public void testExtractActorChain_MultipleActors() throws Exception {
+ // Create an actor chain
+ List<Map<String, Object>> originalChain = new ArrayList<>();
+ Map<String, Object> actor1 = new LinkedHashMap<>();
+ actor1.put("sub", "actor1");
+ originalChain.add(actor1);
+
+ Map<String, Object> actor2 = new LinkedHashMap<>();
+ actor2.put("sub", "actor2");
+ originalChain.add(actor2);
+
+ Map<String, Object> actor3 = new LinkedHashMap<>();
+ actor3.put("sub", "actor3");
+ originalChain.add(actor3);
+
+ // Create a JWT with the actor chain
+ JWTokenAttributesBuilder builder = new JWTokenAttributesBuilder();
+ builder.setUserName("testuser")
+ .setAlgorithm("RS256")
+ .setExpires(System.currentTimeMillis() + 30000)
+ .setActorChain(originalChain);
+
+ JWTokenAttributes attributes = builder.build();
+ JWTToken token = new JWTToken(attributes);
+
+ // Extract and verify the chain
+ List<Map<String, Object>> extractedChain =
TokenUtils.extractActorChain(token);
+ assertNotNull(extractedChain);
+ assertEquals(3, extractedChain.size());
+ assertEquals("actor1", extractedChain.get(0).get("sub"));
+ assertEquals("actor2", extractedChain.get(1).get("sub"));
+ assertEquals("actor3", extractedChain.get(2).get("sub"));
+ }
+
+ @Test
+ public void testAddActorToChain_EmptyChain() {
+ List<Map<String, Object>> result = TokenUtils.addActorToChain(null,
"newActor");
+ assertNotNull(result);
+ assertEquals(1, result.size());
+ assertEquals("newActor", result.get(0).get("sub"));
+ }
+
+ @Test
+ public void testAddActorToChain_ExistingChain() {
+ List<Map<String, Object>> existingChain = new ArrayList<>();
+ Map<String, Object> actor1 = new LinkedHashMap<>();
+ actor1.put("sub", "actor1");
+ existingChain.add(actor1);
+
+ Map<String, Object> actor2 = new LinkedHashMap<>();
+ actor2.put("sub", "actor2");
+ existingChain.add(actor2);
+
+ List<Map<String, Object>> result =
TokenUtils.addActorToChain(existingChain, "newActor");
+ assertNotNull(result);
+ assertEquals(3, result.size());
+ // New actor should be first (most recent)
+ assertEquals("newActor", result.get(0).get("sub"));
+ assertEquals("actor1", result.get(1).get("sub"));
+ assertEquals("actor2", result.get(2).get("sub"));
+ }
+
+ @Test
+ public void testAddActorToChain_NullActor() {
+ List<Map<String, Object>> existingChain = new ArrayList<>();
+ Map<String, Object> actor1 = new LinkedHashMap<>();
+ actor1.put("sub", "actor1");
+ existingChain.add(actor1);
+
+ List<Map<String, Object>> result =
TokenUtils.addActorToChain(existingChain, null);
+ assertNotNull(result);
+ assertEquals(1, result.size());
+ assertEquals("actor1", result.get(0).get("sub"));
+ }
+
+ @Test
+ public void testBuildNestedActClaim_EmptyChain() {
+ Map<String, Object> result = TokenUtils.buildNestedActClaim(null);
+ assertNull(result);
+
+ result = TokenUtils.buildNestedActClaim(new ArrayList<>());
+ assertNull(result);
+ }
+
+ @Test
+ public void testBuildNestedActClaim_SingleActor() {
+ List<Map<String, Object>> chain = new ArrayList<>();
+ Map<String, Object> actor1 = new LinkedHashMap<>();
+ actor1.put("sub", "actor1");
+ chain.add(actor1);
+
+ Map<String, Object> result = TokenUtils.buildNestedActClaim(chain);
+ assertNotNull(result);
+ assertEquals("actor1", result.get("sub"));
+ assertNull(result.get("act")); // No nested act claim for single actor
+ }
+
+ @Test
+ public void testBuildNestedActClaim_MultipleActors() {
+ List<Map<String, Object>> chain = new ArrayList<>();
+ Map<String, Object> actor1 = new LinkedHashMap<>();
+ actor1.put("sub", "actor1");
+ chain.add(actor1);
+
+ Map<String, Object> actor2 = new LinkedHashMap<>();
+ actor2.put("sub", "actor2");
+ chain.add(actor2);
+
+ Map<String, Object> actor3 = new LinkedHashMap<>();
+ actor3.put("sub", "actor3");
+ chain.add(actor3);
+
+ Map<String, Object> result = TokenUtils.buildNestedActClaim(chain);
+ assertNotNull(result);
+
+ // Verify the nested structure
+ // Level 1: actor1
+ assertEquals("actor1", result.get("sub"));
+ Map<String, Object> level2 = (Map<String, Object>) result.get("act");
+ assertNotNull(level2);
+
+ // Level 2: actor2
+ assertEquals("actor2", level2.get("sub"));
+ Map<String, Object> level3 = (Map<String, Object>) level2.get("act");
+ assertNotNull(level3);
+
+ // Level 3: actor3 (deepest, no more nesting)
+ assertEquals("actor3", level3.get("sub"));
+ assertNull(level3.get("act"));
+ }
+
+ @Test
+ public void testActorChainRoundTrip() throws Exception {
+ // Create an actor chain
+ List<Map<String, Object>> originalChain = new ArrayList<>();
+ Map<String, Object> actor1 = new LinkedHashMap<>();
+ actor1.put("sub", "actor1");
+ actor1.put("iss", "issuer1");
+ originalChain.add(actor1);
+
+ Map<String, Object> actor2 = new LinkedHashMap<>();
+ actor2.put("sub", "actor2");
+ originalChain.add(actor2);
+
+ Map<String, Object> actor3 = new LinkedHashMap<>();
+ actor3.put("sub", "actor3");
+ originalChain.add(actor3);
+
+ // Create a JWT with the actor chain
+ JWTokenAttributesBuilder builder = new JWTokenAttributesBuilder();
+ builder.setUserName("testuser")
+ .setAlgorithm("RS256")
+ .setExpires(System.currentTimeMillis() + 30000)
+ .setActorChain(originalChain);
+
+ JWTokenAttributes attributes = builder.build();
+ JWTToken token = new JWTToken(attributes);
+
+ // Extract the chain and verify it matches the original
+ List<Map<String, Object>> extractedChain =
TokenUtils.extractActorChain(token);
+ assertNotNull(extractedChain);
+ assertEquals(3, extractedChain.size());
+ assertEquals("actor1", extractedChain.get(0).get("sub"));
+ assertEquals("issuer1", extractedChain.get(0).get("iss"));
+ assertEquals("actor2", extractedChain.get(1).get("sub"));
+ assertEquals("actor3", extractedChain.get(2).get("sub"));
+ }
+}