This is an automated email from the ASF dual-hosted git repository.

smolnar82 pushed a commit to branch knox_idf
in repository https://gitbox.apache.org/repos/asf/knox.git

commit 2e1f178065325c2e70e8df984c37484d68da095b
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
    
    (cherry picked from commit 0c8838511f2918b6b13b8293f4db04febe2f5fa0)
---
 .../filter/AbstractIdentityAssertionFilter.java    |   8 +-
 .../federation/jwt/filter/AbstractJWTFilter.java   |  21 +-
 .../gateway/service/knoxtoken/TokenResource.java   |  41 +++-
 .../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 |  39 +++-
 .../security/token/JWTokenAttributesBuilder.java   |  34 ++-
 .../services/security/token/TokenUtils.java        | 132 ++++++++++-
 .../services/security/token/impl/JWTToken.java     |  16 +-
 .../services/security/token/TokenUtilsTest.java    | 244 +++++++++++++++++++++
 12 files changed, 837 insertions(+), 36 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 969eb4543..56ab3b3f8 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;
@@ -1125,16 +1126,8 @@ 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)) {
-        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);
-          log.addingActorClaimToToken(primaryPrincipalName, 
impersonatedPrincipalName);
-        }
-      }
+      // RFC 8693 Token Exchange: Build the actor chain if delegated auth is 
enabled
+      handleDelegatedAuthentication(subject, jwtAttributesBuilder);
     }
 
     if (userContext.userParams != null) {
@@ -1150,6 +1143,34 @@ public class TokenResource {
     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)) {
+          // 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");
+      }
+    }
+  }
+
   private boolean shouldIncludeGroups() {
     return 
Boolean.parseBoolean(request.getParameter(KNOX_TOKEN_INCLUDE_GROUPS));
   }
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 7eb890e29..6e2f9350e 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
@@ -42,11 +42,12 @@ public class JWTokenAttributes {
   private final String issuer;
   private String kid;
   private final String clientId;
-  private final String actor;
-  private final Map<String, Object> customAttributes;
+  private final List<Map<String, Object>> actorChain;
+  private final  Map<String, Object> customAttributes;
 
   JWTokenAttributes(String userName, List<String> audiences, String algorithm, 
long issueTime, 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, Map<String, Object> customAttributes) {
+      char[] signingKeystorePassphrase, boolean managed, String jku, String 
type, Set<String> groups, String kid, String issuer, String clientId, 
List<Map<String, Object>> actorChain,
+                    Map<String, Object> customAttributes) {
     this.userName = userName;
     this.audiences = audiences;
     this.algorithm = algorithm;
@@ -62,7 +63,7 @@ public class JWTokenAttributes {
     this.kid = kid;
     this.issuer = issuer;
     this.clientId = clientId;
-    this.actor = actor;
+    this.actorChain = actorChain;
     this.customAttributes = customAttributes;
   }
     public String getUserName() {
@@ -141,11 +142,31 @@ 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;
   }
 
-    public Map<String, Object> getCustomAttributes() {
-        return customAttributes;
-    }
+  public Map<String, Object> getCustomAttributes() {
+      return customAttributes;
+  }
 }
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 6c2e4ab76..c7bf1adb4 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
@@ -40,7 +40,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;
   private Map<String, Object> customAttributes;
 
   public JWTokenAttributesBuilder setUserName(String userName) {
@@ -122,8 +122,32 @@ 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&lt;Map&lt;String, Object&gt;&gt; chain = new ArrayList&lt;&gt;();
+   * Map&lt;String, Object&gt; actor1 = new HashMap&lt;&gt;();
+   * 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;
   }
 
@@ -133,7 +157,7 @@ public class JWTokenAttributesBuilder {
   }
 
   public JWTokenAttributes build() {
-    return new JWTokenAttributes(userName, (audiences == null ? new 
ArrayList<>() : audiences), algorithm, issueTime, expires, signingKeystoreName, 
signingKeystoreAlias,
-        signingKeystorePassphrase, managed, jku, type, groups, kid, issuer, 
clientId, actor, customAttributes);
+      return new JWTokenAttributes(userName, (audiences == null ? new 
ArrayList<>() : audiences), algorithm, issueTime, expires, signingKeystoreName, 
signingKeystoreAlias,
+        signingKeystorePassphrase, managed, jku, type, groups, kid, issuer, 
clientId, actorChain, customAttributes);
   }
 }
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 73d84f96b..5709888a8 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
@@ -31,8 +31,12 @@ import javax.servlet.FilterConfig;
 import javax.servlet.ServletContext;
 import java.security.interfaces.RSAPublicKey;
 import java.text.ParseException;
-import java.util.LinkedHashMap;
 import java.util.UUID;
+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";
@@ -131,4 +135,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 2b779ab66..78ad648f0 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
@@ -20,6 +20,7 @@ import java.net.URISyntaxException;
 import java.text.ParseException;
 import java.time.Instant;
 import java.util.Date;
+import java.util.Map;
 import java.util.UUID;
 
 import com.nimbusds.jose.JOSEException;
@@ -34,6 +35,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 );
@@ -102,14 +104,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"));
+  }
+}

Reply via email to