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 b6f674d42f9da75c3dec492755e3ef00ce57e9b6
Author: hsheinblatt <[email protected]>
AuthorDate: Thu Aug 6 10:50:22 2026 -0700

    KNOX-3408 - Allow no actor token in JWTFederationFilter.handleTokenExchange 
(#1339)
    
    * KNOX-3408 - Regression tests for subject handling in 
JWTFederationFilter.handleTokenExchange and
    TokenExchangePrincipal handling in 
AbstractIdentityAssertionFilter#continueChainAsPrincipal handling
    
    Only unit tests are added for existing functionality.
    
    * KNOX-3408 - Allow no actor token in 
JWTFederationFilter.handleTokenExchange.
    
    ---------
    
    Co-authored-by: Harrison <[email protected]>
---
 ...ctIdentityAssertionFilterTokenExchangeTest.java | 279 +++++++++++++++++++++
 .../federation/jwt/filter/JWTFederationFilter.java |   2 +
 ...JWTFederationFilterHandleTokenExchangeTest.java | 262 +++++++++++++++++++
 3 files changed, 543 insertions(+)

diff --git 
a/gateway-provider-identity-assertion-common/src/test/java/org/apache/knox/gateway/identityasserter/common/filter/AbstractIdentityAssertionFilterTokenExchangeTest.java
 
b/gateway-provider-identity-assertion-common/src/test/java/org/apache/knox/gateway/identityasserter/common/filter/AbstractIdentityAssertionFilterTokenExchangeTest.java
new file mode 100644
index 000000000..f74ddd55f
--- /dev/null
+++ 
b/gateway-provider-identity-assertion-common/src/test/java/org/apache/knox/gateway/identityasserter/common/filter/AbstractIdentityAssertionFilterTokenExchangeTest.java
@@ -0,0 +1,279 @@
+/*
+ * 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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * 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.identityasserter.common.filter;
+
+import org.apache.knox.gateway.audit.log4j.audit.Log4jAuditService;
+import org.apache.knox.gateway.context.ContextAttributes;
+import org.apache.knox.gateway.security.ActorChainPrincipal;
+import org.apache.knox.gateway.security.ActorChainPrincipalImpl;
+import org.apache.knox.gateway.security.ImpersonatedPrincipal;
+import org.apache.knox.gateway.security.PrimaryPrincipal;
+import org.apache.knox.gateway.security.SubjectUtils;
+import org.apache.knox.gateway.security.TokenExchangePrincipalImpl;
+import org.apache.knox.gateway.services.GatewayServices;
+import org.apache.logging.log4j.ThreadContext;
+import org.easymock.EasyMock;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import javax.security.auth.Subject;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.security.PrivilegedExceptionAction;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Regression tests for the RFC 8693 token-exchange processing pipeline:
+ * {@link AbstractIdentityAssertionFilter#continueChainAsPrincipal} handling of
+ * {@code TokenExchangePrincipal} (TEP) and {@code ActorChainPrincipal}.
+ *
+ * <p>Each test constructs a Subject directly (bypassing the JWT filter) and 
runs it through
+ * a minimal anonymous subclass of {@link CommonIdentityAssertionFilter} with 
identity
+ * {@code mapUserPrincipal} (returns input unchanged) and null {@code 
mapGroupPrincipals}
+ * (no group mapping). A {@link SubjectCapturingChain} captures the Subject 
visible to
+ * downstream filters inside whatever doAs context is active at chain 
invocation time.
+ *
+ * <p>Abbreviations used: AIAF for AbstractIdentityAssertionFilter and
+ * TEP for TokenExchangePrincipal.
+ *
+ */
+public class AbstractIdentityAssertionFilterTokenExchangeTest {
+
+  private CommonIdentityAssertionFilter filter;
+  private FilterConfig filterConfig;
+
+  @Before
+  public void setUp() throws Exception {
+    filter = new CommonIdentityAssertionFilter() {
+      @Override
+      public String mapUserPrincipal(String principalName) {
+        return principalName;
+      }
+
+      @Override
+      public String[] mapGroupPrincipals(String name, Subject subject,
+          ServletRequest request) {
+        return null;
+      }
+    };
+
+    ServletContext ctx = EasyMock.createNiceMock(ServletContext.class);
+    
EasyMock.expect(ctx.getAttribute(GatewayServices.GATEWAY_CLUSTER_ATTRIBUTE))
+        .andReturn("test-topology").anyTimes();
+    ctx.setAttribute(
+        EasyMock.eq(ContextAttributes.IMPERSONATION_ENABLED_ATTRIBUTE),
+        EasyMock.anyObject());
+    EasyMock.expectLastCall().anyTimes();
+    EasyMock.replay(ctx);
+
+    filterConfig = EasyMock.createNiceMock(FilterConfig.class);
+    
EasyMock.expect(filterConfig.getServletContext()).andReturn(ctx).anyTimes();
+    EasyMock.expect(filterConfig.getInitParameter(
+        
CommonIdentityAssertionFilter.PRINCIPAL_MAPPING)).andReturn(null).anyTimes();
+    EasyMock.expect(filterConfig.getInitParameter(
+        
CommonIdentityAssertionFilter.GROUP_PRINCIPAL_MAPPING)).andReturn(null).anyTimes();
+    EasyMock.expect(filterConfig.getInitParameter(
+        CommonIdentityAssertionFilter.ADVANCED_PRINCIPAL_MAPPING))
+        .andReturn("username").anyTimes();
+    EasyMock.expect(filterConfig.getInitParameterNames())
+        .andReturn(Collections.emptyEnumeration()).anyTimes();
+    EasyMock.replay(filterConfig);
+
+    filter.init(filterConfig);
+    ThreadContext.put(Log4jAuditService.MDC_AUDIT_CONTEXT_KEY, "dummy");
+  }
+
+  /**
+   * When TEP identifies different actor and subject, AIAF creates a new doAs 
Subject with an
+   * ImpersonatedPrincipal set to the subject identity and PrimaryPrincipal 
preserved as the actor.
+   */
+  @Test
+  public void testTEPWithDifferentActorAndSubjectSetsUpImpersonation() throws 
Exception {
+    Subject subject = buildSubject(
+        new PrimaryPrincipal("sa-actor"),
+        new TokenExchangePrincipalImpl("end-user", null, "sa-actor", null));
+
+    SubjectCapturingChain chain = runFilterWithSubject(subject);
+
+    Assert.assertTrue("chain should have been called", chain.called);
+    Set<ImpersonatedPrincipal> impersonated = 
chain.subject.getPrincipals(ImpersonatedPrincipal.class);
+    Assert.assertEquals("Expected exactly one ImpersonatedPrincipal", 1, 
impersonated.size());
+    Assert.assertEquals("ImpersonatedPrincipal should be end-user", "end-user",
+        impersonated.iterator().next().getName());
+    Set<PrimaryPrincipal> primary = 
chain.subject.getPrincipals(PrimaryPrincipal.class);
+    Assert.assertEquals("Expected exactly one PrimaryPrincipal", 1, 
primary.size());
+    Assert.assertEquals("PrimaryPrincipal should be sa-actor", "sa-actor",
+        primary.iterator().next().getName());
+  }
+
+  /**
+   * When TEP actor and subject are the same identity, no impersonation is 
needed and AIAF
+   * proceeds without adding an ImpersonatedPrincipal to the downstream 
Subject.
+   */
+  @Test
+  public void testTEPWithSameActorAndSubjectSkipsImpersonation() throws 
Exception {
+    Subject subject = buildSubject(
+        new PrimaryPrincipal("alice"),
+        new TokenExchangePrincipalImpl("alice", null, "alice", null));
+
+    SubjectCapturingChain chain = runFilterWithSubject(subject);
+
+    Assert.assertTrue("chain should have been called", chain.called);
+    Assert.assertTrue("ImpersonatedPrincipal set should be empty",
+        chain.subject.getPrincipals(ImpersonatedPrincipal.class).isEmpty());
+  }
+
+  /**
+   * When no TEP is present, AIAF proceeds normally without creating an 
ImpersonatedPrincipal
+   * and the downstream Subject contains no TokenExchangePrincipal.
+   */
+  @Test
+  public void testNoTEPProceedsNormally() throws Exception {
+    Subject subject = buildSubject(new PrimaryPrincipal("alice"));
+
+    SubjectCapturingChain chain = runFilterWithSubject(subject);
+
+    Assert.assertTrue("chain should have been called", chain.called);
+    Assert.assertTrue("ImpersonatedPrincipal set should be empty",
+        chain.subject.getPrincipals(ImpersonatedPrincipal.class).isEmpty());
+    Assert.assertNull("No TokenExchangePrincipal expected",
+        SubjectUtils.getTokenExchangePrincipal(chain.subject));
+  }
+
+  /**
+   * Principal mapping is applied to the subject identity from TEP (not to the 
actor identity).
+   * AIAF calls {@code mapUserPrincipal} on {@code 
tep.getSubjectPrincipalName()} and uses the
+   * mapped result as the ImpersonatedPrincipal; the actor (PrimaryPrincipal) 
is unchanged.
+   */
+  @Test
+  public void testTEPAppliesPrincipalMappingToSubjectNotActor() throws 
Exception {
+    CommonIdentityAssertionFilter mappingFilter = new 
CommonIdentityAssertionFilter() {
+      @Override
+      public String mapUserPrincipal(String principalName) {
+        return "user@external".equals(principalName) ? "localuser" : 
principalName;
+      }
+
+      @Override
+      public String[] mapGroupPrincipals(String name, Subject subject,
+          ServletRequest request) {
+        return null;
+      }
+    };
+    mappingFilter.init(filterConfig);
+
+    Subject subject = buildSubject(
+        new PrimaryPrincipal("sa-actor"),
+        new TokenExchangePrincipalImpl("user@external", null, "sa-actor", 
null));
+
+    SubjectCapturingChain chain = runFilterWithSubject(subject, mappingFilter);
+
+    Set<ImpersonatedPrincipal> impersonated = 
chain.subject.getPrincipals(ImpersonatedPrincipal.class);
+    Assert.assertEquals("Expected exactly one ImpersonatedPrincipal", 1, 
impersonated.size());
+    Assert.assertEquals("ImpersonatedPrincipal should be mapped value", 
"localuser",
+        impersonated.iterator().next().getName());
+    Set<PrimaryPrincipal> primary = 
chain.subject.getPrincipals(PrimaryPrincipal.class);
+    Assert.assertEquals("Expected exactly one PrimaryPrincipal", 1, 
primary.size());
+    Assert.assertEquals("PrimaryPrincipal should be actor (unmapped)", 
"sa-actor",
+        primary.iterator().next().getName());
+  }
+
+  /**
+   * The TokenExchangePrincipal is preserved in the new doAs Subject built by 
AIAF when
+   * impersonation is needed, so downstream filters can still read the 
delegation metadata.
+   */
+  @Test
+  public void testTEPPreservedInDoAsSubject() throws Exception {
+    Subject subject = buildSubject(
+        new PrimaryPrincipal("sa-actor"),
+        new TokenExchangePrincipalImpl("end-user", null, "sa-actor", null));
+
+    SubjectCapturingChain chain = runFilterWithSubject(subject);
+
+    Assert.assertNotNull("TokenExchangePrincipal should be preserved in 
downstream Subject",
+        SubjectUtils.getTokenExchangePrincipal(chain.subject));
+  }
+
+  /**
+   * The ActorChainPrincipal is preserved in the new doAs Subject built by 
AIAF when
+   * impersonation is needed, so the full delegation chain history is 
available downstream.
+   */
+  @Test
+  public void testActorChainPrincipalPreservedInDoAsSubject() throws Exception 
{
+    List<Map<String, Object>> chain = List.of(Map.of("sub", "prior-actor"));
+    Subject subject = buildSubject(
+        new PrimaryPrincipal("sa-actor"),
+        new TokenExchangePrincipalImpl("end-user", null, "sa-actor", null),
+        new ActorChainPrincipalImpl(chain));
+
+    SubjectCapturingChain capturingChain = runFilterWithSubject(subject);
+
+    Set<ActorChainPrincipal> actorChainPrincipals =
+        capturingChain.subject.getPrincipals(ActorChainPrincipal.class);
+    Assert.assertFalse("ActorChainPrincipal should be preserved", 
actorChainPrincipals.isEmpty());
+    Assert.assertEquals("getCurrentActor should be prior-actor", "prior-actor",
+        actorChainPrincipals.iterator().next().getCurrentActor());
+  }
+
+  // ---- Helpers ----
+
+  private static Subject buildSubject(java.security.Principal... principals) {
+    Subject s = new Subject();
+    for (java.security.Principal p : principals) {
+      s.getPrincipals().add(p);
+    }
+    return s;
+  }
+
+  /** Runs the filter inside {@code Subject.doAs(subjectToRun, ...)} using the 
default filter. */
+  private SubjectCapturingChain runFilterWithSubject(Subject subjectToRun) 
throws Exception {
+    return runFilterWithSubject(subjectToRun, filter);
+  }
+
+  /** Runs the filter inside {@code Subject.doAs(subjectToRun, ...)} using the 
given filter. */
+  private SubjectCapturingChain runFilterWithSubject(Subject subjectToRun,
+      CommonIdentityAssertionFilter f) throws Exception {
+    SubjectCapturingChain chain = new SubjectCapturingChain();
+    HttpServletRequest request = 
EasyMock.createNiceMock(HttpServletRequest.class);
+    HttpServletResponse response = 
EasyMock.createNiceMock(HttpServletResponse.class);
+    EasyMock.replay(request, response);
+    Subject.doAs(subjectToRun, (PrivilegedExceptionAction<Void>) () -> {
+      f.doFilter(request, response, chain);
+      return null;
+    });
+    return chain;
+  }
+
+  private static class SubjectCapturingChain implements FilterChain {
+    Subject subject;
+    boolean called;
+
+    @Override
+    public void doFilter(ServletRequest req, ServletResponse resp) {
+      called = true;
+      subject = SubjectUtils.getCurrentSubject();
+    }
+  }
+}
diff --git 
a/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/JWTFederationFilter.java
 
b/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/JWTFederationFilter.java
index 30ab50603..f1abb686a 100644
--- 
a/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/JWTFederationFilter.java
+++ 
b/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/JWTFederationFilter.java
@@ -22,6 +22,8 @@ import org.apache.commons.lang3.tuple.Pair;
 import org.apache.knox.gateway.i18n.messages.MessagesFactory;
 import org.apache.knox.gateway.provider.federation.jwt.JWTMessages;
 import org.apache.knox.gateway.security.PrimaryPrincipal;
+import org.apache.knox.gateway.security.TokenExchangePrincipalImpl;
+import org.apache.knox.gateway.services.security.token.TokenUtils;
 import org.apache.knox.gateway.services.security.token.UnknownTokenException;
 import org.apache.knox.gateway.services.security.token.impl.JWT;
 import org.apache.knox.gateway.services.security.token.impl.JWTToken;
diff --git 
a/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/JWTFederationFilterHandleTokenExchangeTest.java
 
b/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/JWTFederationFilterHandleTokenExchangeTest.java
new file mode 100644
index 000000000..ca23a5ae7
--- /dev/null
+++ 
b/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/JWTFederationFilterHandleTokenExchangeTest.java
@@ -0,0 +1,262 @@
+/*
+ * 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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * 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.provider.federation;
+
+import com.nimbusds.jose.crypto.RSASSASigner;
+import com.nimbusds.jwt.SignedJWT;
+import 
org.apache.knox.gateway.provider.federation.jwt.filter.AbstractJWTFilter;
+import 
org.apache.knox.gateway.provider.federation.jwt.filter.JWTFederationFilter;
+import org.apache.knox.gateway.security.ActorChainPrincipal;
+import org.apache.knox.gateway.security.CommonTokenConstants;
+import org.apache.knox.gateway.security.ImpersonatedPrincipal;
+import org.apache.knox.gateway.security.PrimaryPrincipal;
+import org.apache.knox.gateway.security.SubjectUtils;
+import org.apache.knox.gateway.security.TokenExchangePrincipal;
+import 
org.apache.knox.gateway.services.security.token.JWTokenAttributesBuilder;
+import org.apache.knox.gateway.services.security.token.impl.JWTToken;
+import org.easymock.EasyMock;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.security.Principal;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+
+import static 
org.apache.knox.gateway.provider.federation.jwt.filter.AbstractJWTFilter.JWT_DEFAULT_ISSUER;
+
+/**
+ * Unit tests for the {@link JWTFederationFilter#handleTokenExchange} method 
(OIDC
+ * delegation path). Each test verifies the Subject constructed.
+ *
+ * <p>These tests use {@link TestJWTFederationFilter} with {@link 
TestJWTokenAuthority} (static key,
+ * no mocking needed — both tokens use Knox issuer {@code JWT_DEFAULT_ISSUER} 
which is in the
+ * static expected-issuers list).
+ *
+ * <p>The filter's {@code continueWithEstablishedSecurityContext} runs
+ * {@code Subject.doAs(subject, () -> chain.doFilter(request, response))}. The
+ * {@link AbstractJWTFilterTest.TestFilterChain} captures {@code 
SubjectUtils.getCurrentSubject()}
+ * from within that doAs context, which is exactly the Subject built. All 
principal
+ * assertions use {@code chain.subject.getPrincipals(XxxPrincipal.class)}.
+ */
+public class JWTFederationFilterHandleTokenExchangeTest extends 
AbstractJWTFilterTest {
+
+  static final String ACTOR_ISSUER = "https://actor.oidc.example.com";;
+
+  @Before
+  public void setUp() throws Exception {
+    handler = new TestJWTFederationFilter();
+    ((TestJWTFederationFilter) handler).setTokenService(new 
TestJWTokenAuthority(publicKey));
+    handler.init(new TestFilterConfig(getProperties()));
+  }
+
+  @Override
+  protected void setTokenOnRequest(HttpServletRequest request, SignedJWT jwt) {
+    EasyMock.expect(request.getHeader("Authorization"))
+        .andReturn(JWTFederationFilter.BEARER + jwt.serialize()).anyTimes();
+  }
+
+  @Override
+  protected void setGarbledTokenOnRequest(HttpServletRequest request, 
SignedJWT jwt) {
+    EasyMock.expect(request.getHeader("Authorization"))
+        .andReturn(JWTFederationFilter.BEARER + "ljm" + 
jwt.serialize()).anyTimes();
+  }
+
+  @Override
+  protected String getAudienceProperty() {
+    return JWTFederationFilter.KNOX_TOKEN_AUDIENCES;
+  }
+
+  @Override
+  protected String getVerificationPemProperty() {
+    return JWTFederationFilter.TOKEN_VERIFICATION_PEM;
+  }
+
+  /**
+   * When both subject_token and actor_token are present, the filter 
establishes the actor
+   * (from actor_token.sub) as the PrimaryPrincipal in the resulting Subject.
+   */
+  @Test
+  public void testActorAndSubjectTokensSetActorAsPrimaryPrincipal() throws 
Exception {
+    SignedJWT subjectJwt = getJWT(JWT_DEFAULT_ISSUER, "k8s-sa",
+        new Date(System.currentTimeMillis() + 60000), privateKey);
+    SignedJWT actorJwt = getJWT(JWT_DEFAULT_ISSUER, "actor-svc",
+        new Date(System.currentTimeMillis() + 60000), privateKey);
+
+    HttpServletRequest request = 
buildTokenExchangeRequest(subjectJwt.serialize(), actorJwt.serialize());
+    EasyMock.replay(request);
+    HttpServletResponse response = 
EasyMock.createNiceMock(HttpServletResponse.class);
+    EasyMock.replay(response);
+
+    TestFilterChain chain = new TestFilterChain();
+    handler.doFilter(request, response, chain);
+
+    Assert.assertTrue("doFilterCalled should be true", chain.doFilterCalled);
+    Set<PrimaryPrincipal> principals = 
chain.subject.getPrincipals(PrimaryPrincipal.class);
+    Assert.assertEquals("Expected exactly one PrimaryPrincipal", 1, 
principals.size());
+    Assert.assertEquals("Expected actor as PrimaryPrincipal", "actor-svc",
+        ((Principal) principals.toArray()[0]).getName());
+  }
+
+  /**
+   * When subject_token and actor_token have different issuers, the filter 
creates a
+   * TokenExchangePrincipal that carries the subject and actor identities with 
their respective
+   * issuers. Using different issuers ensures that all four TEP fields can be 
asserted
+   * unambiguously.
+   *
+   * <p>Both issuers are added to the static {@code jwt.expected.issuer} 
whitelist —
+   * {@code TestJWTokenAuthority} accepts either token because they are signed 
with
+   * the same test key.
+   */
+  @Test
+  public void testActorAndSubjectTokensCreateTokenExchangePrincipal() throws 
Exception {
+    Properties props = getProperties();
+    props.setProperty(AbstractJWTFilter.JWT_EXPECTED_ISSUER, 
JWT_DEFAULT_ISSUER + "," + ACTOR_ISSUER);
+    handler.init(new TestFilterConfig(props));
+
+    SignedJWT subjectJwt = getJWT(JWT_DEFAULT_ISSUER, "k8s-sa",
+        new Date(System.currentTimeMillis() + 60000), privateKey);
+    SignedJWT actorJwt = getJWT(ACTOR_ISSUER, "actor-svc",
+        new Date(System.currentTimeMillis() + 60000), privateKey);
+
+    HttpServletRequest request = 
buildTokenExchangeRequest(subjectJwt.serialize(), actorJwt.serialize());
+    EasyMock.replay(request);
+    HttpServletResponse response = 
EasyMock.createNiceMock(HttpServletResponse.class);
+    EasyMock.replay(response);
+
+    TestFilterChain chain = new TestFilterChain();
+    handler.doFilter(request, response, chain);
+
+    Assert.assertTrue("doFilterCalled should be true", chain.doFilterCalled);
+    TokenExchangePrincipal tep = 
SubjectUtils.getTokenExchangePrincipal(chain.subject);
+    Assert.assertNotNull("TokenExchangePrincipal should be present", tep);
+    Assert.assertEquals("Subject principal name", "k8s-sa", 
tep.getSubjectPrincipalName());
+    Assert.assertEquals("Actor principal name", "actor-svc", 
tep.getActorPrincipalName());
+    Assert.assertEquals("Subject issuer", JWT_DEFAULT_ISSUER, 
tep.getSubjectIssuer());
+    Assert.assertEquals("Actor issuer", ACTOR_ISSUER, tep.getActorIssuer());
+  }
+
+  /**
+   * When subject_token carries an {@code act} claim (a prior delegation 
chain), the filter
+   * extracts it and creates an {@code ActorChainPrincipal} in the resulting 
Subject so that
+   * the delegation history is preserved through the filter pipeline.
+   */
+  @Test
+  public void testSubjectTokenWithActClaimCreatesActorChainPrincipal() throws 
Exception {
+    List<Map<String, Object>> actorChainData = List.of(Map.of("sub", 
"prior-actor"));
+    JWTToken subjectToken = new JWTToken(new JWTokenAttributesBuilder()
+        .setUserName("k8s-sa")
+        .setIssuer(JWT_DEFAULT_ISSUER)
+        .setAlgorithm("RS256")
+        .setExpires(System.currentTimeMillis() + 60000)
+        .setActorChain(actorChainData)
+        .build());
+    subjectToken.sign(new RSASSASigner(privateKey));
+
+    SignedJWT actorJwt = getJWT(JWT_DEFAULT_ISSUER, "actor-svc",
+        new Date(System.currentTimeMillis() + 60000), privateKey);
+
+    HttpServletRequest request = 
buildTokenExchangeRequest(subjectToken.toString(), actorJwt.serialize());
+    EasyMock.replay(request);
+    HttpServletResponse response = 
EasyMock.createNiceMock(HttpServletResponse.class);
+    EasyMock.replay(response);
+
+    TestFilterChain chain = new TestFilterChain();
+    handler.doFilter(request, response, chain);
+
+    Assert.assertTrue("doFilterCalled should be true", chain.doFilterCalled);
+    Set<ActorChainPrincipal> actorChainPrincipals = 
chain.subject.getPrincipals(ActorChainPrincipal.class);
+    Assert.assertFalse("ActorChainPrincipal should be present", 
actorChainPrincipals.isEmpty());
+    ActorChainPrincipal acp = actorChainPrincipals.iterator().next();
+    Assert.assertEquals("Expected current actor from act claim", 
"prior-actor", acp.getCurrentActor());
+  }
+
+  /**
+   * With no actor_token provided, the filter proceeds successfully and the 
resulting Subject
+   * has the subject itself as PrimaryPrincipal, no TokenExchangePrincipal, 
and no
+   * ImpersonatedPrincipal.
+   */
+  @Test
+  public void testSubjectTokenOnlySucceeds() throws Exception {
+    SignedJWT subjectJwt = getJWT(JWT_DEFAULT_ISSUER, "k8s-sa",
+        new Date(System.currentTimeMillis() + 60000), privateKey);
+
+    HttpServletRequest request = 
buildTokenExchangeRequestSubjectOnly(subjectJwt.serialize());
+    EasyMock.replay(request);
+    HttpServletResponse response = 
EasyMock.createNiceMock(HttpServletResponse.class);
+    EasyMock.replay(response);
+
+    TestFilterChain chain = new TestFilterChain();
+    handler.doFilter(request, response, chain);
+
+    Assert.assertTrue("doFilterCalled should be true", chain.doFilterCalled);
+    Set<PrimaryPrincipal> principals = 
chain.subject.getPrincipals(PrimaryPrincipal.class);
+    Assert.assertEquals("Expected exactly one PrimaryPrincipal", 1, 
principals.size());
+    Assert.assertEquals("Subject should be its own PrimaryPrincipal", "k8s-sa",
+        ((java.security.Principal) principals.toArray()[0]).getName());
+    Assert.assertNull("No TokenExchangePrincipal expected for subject-only 
exchange",
+        SubjectUtils.getTokenExchangePrincipal(chain.subject));
+    Assert.assertTrue("ImpersonatedPrincipal set should be empty",
+        chain.subject.getPrincipals(ImpersonatedPrincipal.class).isEmpty());
+  }
+
+  /**
+   * Builds a token-exchange request mock with both subject_token and 
actor_token parameters.
+   * The caller must call {@code EasyMock.replay(request)} before using the 
returned mock.
+   *
+   * @param subjectToken serialized subject JWT
+   * @param actorToken   serialized actor JWT
+   * @return a NiceMock HttpServletRequest configured for a token-exchange 
grant
+   */
+  private HttpServletRequest buildTokenExchangeRequest(String subjectToken, 
String actorToken) {
+    HttpServletRequest request = 
EasyMock.createNiceMock(HttpServletRequest.class);
+    EasyMock.expect(request.getRequestURL())
+        .andReturn(new StringBuffer(SERVICE_URL)).anyTimes();
+    EasyMock.expect(request.getParameter(CommonTokenConstants.GRANT_TYPE))
+        .andReturn(JWTFederationFilter.TOKEN_EXCHANGE).anyTimes();
+    EasyMock.expect(request.getParameter(JWTFederationFilter.SUBJECT_TOKEN))
+        .andReturn(subjectToken).anyTimes();
+    EasyMock.expect(request.getParameter(JWTFederationFilter.ACTOR_TOKEN))
+        .andReturn(actorToken).anyTimes();
+    return request;
+  }
+
+  /**
+   * Builds a token-exchange request mock with subject_token only. The 
actor_token parameter
+   * is not mocked, so the NiceMock returns null for {@code 
getParameter(ACTOR_TOKEN)}.
+   * The caller must call {@code EasyMock.replay(request)} before using the 
returned mock.
+   *
+   * @param subjectToken serialized subject JWT
+   * @return a NiceMock HttpServletRequest configured for a subject-only 
token-exchange grant
+   */
+  private HttpServletRequest buildTokenExchangeRequestSubjectOnly(String 
subjectToken) {
+    HttpServletRequest request = 
EasyMock.createNiceMock(HttpServletRequest.class);
+    EasyMock.expect(request.getRequestURL())
+        .andReturn(new StringBuffer(SERVICE_URL)).anyTimes();
+    EasyMock.expect(request.getParameter(CommonTokenConstants.GRANT_TYPE))
+        .andReturn(JWTFederationFilter.TOKEN_EXCHANGE).anyTimes();
+    EasyMock.expect(request.getParameter(JWTFederationFilter.SUBJECT_TOKEN))
+        .andReturn(subjectToken).anyTimes();
+    // ACTOR_TOKEN not mocked — NiceMock returns null for 
getParameter(ACTOR_TOKEN)
+    return request;
+  }
+}

Reply via email to