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 f54608484 KNOX-3321 - KnoxToken Support for RFC 8693 Token Exchange
act Claim (#1229)
f54608484 is described below
commit f54608484dac74bc76863a3651cc9f0c8be55c40
Author: lmccay <[email protected]>
AuthorDate: Wed May 13 09:25:48 2026 -0400
KNOX-3321 - KnoxToken Support for RFC 8693 Token Exchange act Claim (#1229)
---
.../gateway/service/knoxtoken/TokenResource.java | 16 +++
.../service/knoxtoken/TokenServiceMessages.java | 3 +
.../knoxtoken/TokenServiceResourceTest.java | 127 +++++++++++++++++++++
.../services/security/token/JWTokenAttributes.java | 11 ++
.../security/token/JWTokenAttributesBuilder.java | 8 +-
.../gateway/services/security/token/impl/JWT.java | 2 +
.../services/security/token/impl/JWTToken.java | 29 +++++
7 files changed, 195 insertions(+), 1 deletion(-)
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 6fec10e0f..c0ec90b9d 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
@@ -146,6 +146,7 @@ public class TokenResource {
static final String KNOX_TOKEN_USER_LIMIT_PER_USER = TOKEN_PARAM_PREFIX +
"limit.per.user";
static final String KNOX_TOKEN_USER_LIMIT_EXCEEDED_ACTION =
TOKEN_PARAM_PREFIX + "user.limit.exceeded.action";
private static final String METADATA_QUERY_PARAM_PREFIX = "md_";
+ private static final String TOKEN_ENABLE_DELEGATED_AUTH = TOKEN_PARAM_PREFIX
+ "enable.delegated.auth";
private static final long TOKEN_TTL_DEFAULT = 30000L;
static final String TOKEN_API_PATH = "knoxtoken/api/v1";
static final String RESOURCE_PATH = TOKEN_API_PATH + "/token";
@@ -188,6 +189,7 @@ public class TokenResource {
private int tokenLimitPerUser;
private boolean includeGroupsInTokenAllowed;
private String tokenIssuer;
+ private boolean enableDelegatedAuth;
enum UserLimitExceededAction {REMOVE_OLDEST, RETURN_ERROR};
@@ -270,6 +272,9 @@ public class TokenResource {
? true
: Boolean.parseBoolean(includeGroupsInTokenAllowedParam);
+ String enableDelegatedAuthParam =
context.getInitParameter(TOKEN_ENABLE_DELEGATED_AUTH);
+ enableDelegatedAuth = enableDelegatedAuthParam != null &&
Boolean.parseBoolean(enableDelegatedAuthParam);
+
this.tokenIssuer =
StringUtils.isBlank(context.getInitParameter(KNOX_TOKEN_ISSUER))
? JWTokenAttributes.DEFAULT_ISSUER
: context.getInitParameter(KNOX_TOKEN_ISSUER);
@@ -1100,6 +1105,17 @@ public class TokenResource {
if (!tokenIdPrincipals.isEmpty()) {
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);
+ }
+ }
}
jwtAttributes = jwtAttributesBuilder.build();
diff --git
a/gateway-service-knoxtoken/src/main/java/org/apache/knox/gateway/service/knoxtoken/TokenServiceMessages.java
b/gateway-service-knoxtoken/src/main/java/org/apache/knox/gateway/service/knoxtoken/TokenServiceMessages.java
index 87b647943..278e42434 100644
---
a/gateway-service-knoxtoken/src/main/java/org/apache/knox/gateway/service/knoxtoken/TokenServiceMessages.java
+++
b/gateway-service-knoxtoken/src/main/java/org/apache/knox/gateway/service/knoxtoken/TokenServiceMessages.java
@@ -99,4 +99,7 @@ public interface TokenServiceMessages {
@Message( level = MessageLevel.DEBUG, text = "Token impersonation failed:
{0}" )
void tokenImpersonationFailed(@StackTrace Throwable t);
+ @Message( level = MessageLevel.DEBUG, text = "Adding RFC 8693 'act' claim to
token: actor={0}, subject={1}" )
+ void addingActorClaimToToken(String actor, String subject);
+
}
diff --git
a/gateway-service-knoxtoken/src/test/java/org/apache/knox/gateway/service/knoxtoken/TokenServiceResourceTest.java
b/gateway-service-knoxtoken/src/test/java/org/apache/knox/gateway/service/knoxtoken/TokenServiceResourceTest.java
index 8d64377b6..281ea90ea 100644
---
a/gateway-service-knoxtoken/src/test/java/org/apache/knox/gateway/service/knoxtoken/TokenServiceResourceTest.java
+++
b/gateway-service-knoxtoken/src/test/java/org/apache/knox/gateway/service/knoxtoken/TokenServiceResourceTest.java
@@ -2093,4 +2093,131 @@ public class TokenServiceResourceTest {
return false;
}
}
+
+ /**
+ * Helper method to setup test configuration for delegated auth tests.
+ */
+ private Map<String, String> createDelegatedAuthContextExpectations(boolean
enableDelegatedAuth, boolean enableImpersonation) {
+ final Map<String, String> contextExpectations = new HashMap<>();
+ contextExpectations.put("knox.token.audiences", "recipient1,recipient2");
+ contextExpectations.put("knox.token.ttl", "60000");
+ if (enableImpersonation) {
+
contextExpectations.put(ContextAttributes.IMPERSONATION_ENABLED_ATTRIBUTE,
"true");
+ }
+ if (enableDelegatedAuth) {
+ contextExpectations.put("knox.token.enable.delegated.auth", "true");
+ }
+ return contextExpectations;
+ }
+
+ /**
+ * Helper method to create a Subject with optional impersonation.
+ */
+ private Subject createSubjectWithOptionalImpersonation(String primaryUser,
String impersonatedUser) {
+ final Subject subject = new Subject();
+ subject.getPrincipals().add(new PrimaryPrincipal(primaryUser));
+ if (impersonatedUser != null && !primaryUser.equals(impersonatedUser)) {
+ subject.getPrincipals().add(new ImpersonatedPrincipal(impersonatedUser));
+ }
+ return subject;
+ }
+
+ /**
+ * Helper method to get a token with the given subject context.
+ */
+ @SuppressForbidden
+ private JWTToken getTokenWithSubject(Subject subject) throws Exception {
+ TokenResource tr = new TokenResource();
+ tr.request = request;
+ tr.context = context;
+ tr.init();
+
+ Response retResponse = Subject.doAs(
+ subject,
+ (PrivilegedAction<Response>) tr::doGet);
+
+ assertEquals(200, retResponse.getStatus());
+
+ String retString = retResponse.getEntity().toString();
+ String accessToken = getTagValue(retString, "access_token");
+ assertNotNull(accessToken);
+
+ return new JWTToken(accessToken);
+ }
+
+ /**
+ * Test RFC 8693 Token Exchange delegated authentication with 'act' claim.
+ * When delegated auth is enabled and impersonation is occurring, the token
should include
+ * an 'act' claim with the actor (primary principal) identity.
+ */
+ @Test
+ @SuppressForbidden
+ public void testDelegatedAuthWithActClaim() throws Exception {
+ final String primaryUser = "admin";
+ final String impersonatedUser = "alice";
+
+ configureCommonExpectations(createDelegatedAuthContextExpectations(true,
true), true);
+ Subject subject = createSubjectWithOptionalImpersonation(primaryUser,
impersonatedUser);
+ JWTToken parsedToken = getTokenWithSubject(subject);
+
+ // Verify the subject is the impersonated user
+ assertEquals(impersonatedUser, parsedToken.getSubject());
+
+ // Verify the 'act' claim is present and contains the actor (primary user)
+ Object actClaim = parsedToken.getClaimAsObject(JWTToken.ACT_CLAIM);
+ assertNotNull("RFC 8693 'act' claim should be present when delegated auth
is enabled", actClaim);
+
+ // The 'act' claim should be a Map containing a "sub" field with the
actor's identity
+ assertTrue("'act' claim should be a Map", actClaim instanceof Map);
+ @SuppressWarnings("unchecked")
+ Map<String, Object> actClaimMap = (Map<String, Object>) actClaim;
+ assertEquals("'act' claim should contain the actor's subject",
primaryUser, actClaimMap.get("sub"));
+
+ EasyMock.verify(request, context);
+ }
+
+ /**
+ * Test that 'act' claim is NOT added when delegated auth is disabled.
+ */
+ @Test
+ @SuppressForbidden
+ public void testNoActClaimWhenDelegatedAuthDisabled() throws Exception {
+ final String primaryUser = "admin";
+ final String impersonatedUser = "alice";
+
+ configureCommonExpectations(createDelegatedAuthContextExpectations(false,
true), true);
+ Subject subject = createSubjectWithOptionalImpersonation(primaryUser,
impersonatedUser);
+ JWTToken parsedToken = getTokenWithSubject(subject);
+
+ // Verify the subject is the impersonated user
+ assertEquals(impersonatedUser, parsedToken.getSubject());
+
+ // Verify the 'act' claim is NOT present when delegated auth is disabled
+ Object actClaim = parsedToken.getClaimAsObject(JWTToken.ACT_CLAIM);
+ assertNull("'act' claim should NOT be present when delegated auth is
disabled", actClaim);
+
+ EasyMock.verify(request, context);
+ }
+
+ /**
+ * Test that 'act' claim is NOT added when there is no impersonation.
+ */
+ @Test
+ @SuppressForbidden
+ public void testNoActClaimWithoutImpersonation() throws Exception {
+ final String user = "alice";
+
+ configureCommonExpectations(createDelegatedAuthContextExpectations(true,
false), true);
+ Subject subject = createSubjectWithOptionalImpersonation(user, null);
+ JWTToken parsedToken = getTokenWithSubject(subject);
+
+ // Verify the subject
+ assertEquals(user, parsedToken.getSubject());
+
+ // Verify the 'act' claim is NOT present when there's no impersonation
+ Object actClaim = parsedToken.getClaimAsObject(JWTToken.ACT_CLAIM);
+ assertNull("'act' claim should NOT be present when there is no
impersonation", actClaim);
+
+ EasyMock.verify(request, context);
+ }
}
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 2df7cf53b..1d19a846d 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
@@ -40,6 +40,7 @@ public class JWTokenAttributes {
private final String issuer;
private String kid;
private final String clientId;
+ private final String actor;
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) {
@@ -48,6 +49,11 @@ 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) {
+ this(userName, audiences, algorithm, expires, signingKeystoreName,
signingKeystoreAlias, signingKeystorePassphrase, managed, jku, type, groups,
kid, issuer, clientId, null);
+ }
+
+ 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) {
this.userName = userName;
this.audiences = audiences;
this.algorithm = algorithm;
@@ -62,6 +68,7 @@ public class JWTokenAttributes {
this.kid = kid;
this.issuer = issuer;
this.clientId = clientId;
+ this.actor = actor;
}
public String getUserName() {
@@ -135,4 +142,8 @@ public class JWTokenAttributes {
public String getClientId() {
return clientId;
}
+
+ public String getActor() {
+ return actor;
+ }
}
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 91f4acddf..50896e61a 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
@@ -38,6 +38,7 @@ public class JWTokenAttributesBuilder {
private String kid;
private String issuer = JWTokenAttributes.DEFAULT_ISSUER;
private String clientId;
+ private String actor;
public JWTokenAttributesBuilder setUserName(String userName) {
this.userName = userName;
@@ -113,8 +114,13 @@ public class JWTokenAttributesBuilder {
return this;
}
+ public JWTokenAttributesBuilder setActor(String actor) {
+ this.actor = actor;
+ 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);
+ signingKeystorePassphrase, managed, jku, type, groups, kid, issuer,
clientId, actor);
}
}
diff --git
a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/impl/JWT.java
b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/impl/JWT.java
index e416e63aa..4cb4d151e 100644
---
a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/impl/JWT.java
+++
b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/impl/JWT.java
@@ -41,6 +41,8 @@ public interface JWT {
String getClaim(String claimName);
+ Object getClaimAsObject(String claimName);
+
String getPrincipal();
String getIssuer();
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 22d8dc16f..27f8f6ced 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
@@ -43,6 +43,7 @@ public class JWTToken implements JWT {
public static final String KNOX_JKU_CLAIM = "jku";
public static final String KNOX_GROUPS_CLAIM = "knox.groups";
public static final String CLIENT_ID_CLAIM = "client_id";
+ public static final String ACT_CLAIM = "act";
SignedJWT jwt;
@@ -99,6 +100,15 @@ 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());
+ }
// Add a private UUID claim for uniqueness
builder.claim(KNOX_ID_CLAIM, String.valueOf(UUID.randomUUID()));
@@ -183,6 +193,25 @@ public class JWTToken implements JWT {
return claim;
}
+ /**
+ * Get a claim value as an Object. Useful for nested claims like 'act' which
is a JSON object.
+ *
+ * @param claimName The name of the claim to retrieve
+ * @return The claim value as an Object, or null if the claim doesn't exist
or cannot be parsed
+ */
+ @Override
+ public Object getClaimAsObject(String claimName) {
+ Object claim = null;
+
+ try {
+ claim = jwt.getJWTClaimsSet().getClaim(claimName);
+ } catch (ParseException e) {
+ log.unableToParseToken(e);
+ }
+
+ return claim;
+ }
+
@Override
public String getSubject() {
return getClaim(JWT.SUBJECT);