Skip to content

Knox 3334 - Introduce ActorChainPrincipal for RFC 8693 instead of ImpersonatedPrincipal#1257

Open
lmccay wants to merge 2 commits into
apache:masterfrom
lmccay:KNOX-3334
Open

Knox 3334 - Introduce ActorChainPrincipal for RFC 8693 instead of ImpersonatedPrincipal#1257
lmccay wants to merge 2 commits into
apache:masterfrom
lmccay:KNOX-3334

Conversation

@lmccay

@lmccay lmccay commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

(It is very important that you created an Apache Knox JIRA for this change and that the PR title/commit message includes the Apache Knox JIRA ID!)

KNOX-3334 Introduce ActorChainPrincipal for RFC 8693 instead of ImpersonatedPrincipal

What changes were proposed in this pull request?

KNOX-3321 provided an initial implementation for 8693 and adding an 'act' claim to the returned JWT based on the presence of the ImpersonatedPrincipal and having delegated auth enabled on the KnoxToken service.

This falls short of what we need to support token exchanges that already include an 'act' claim in the subject token. To support this properly, we need the previous 'act' claim represented in the Java Subject with the full chain represented. We will then add the next actor subclaim to the chain from within the KnoxToken service, effectively continuing the chain as it flows through the actors for the given request.

To support this, we should introduce a new principal called ActorChainPrincipal which will have an extended interface to provide the list of 'act' claims within the presented token for building out the chain in the new token.

How was this patch tested?

  • Built and ran existing tests
  • Added new tests
  • Manually tested as follows:

curl -ivku admin:admin-password 'https://localhost:8443/gateway/knoxcreds/knoxtoken/api/v1/token?doAs=guest'

{"access_token":"eyJqa3UiOiJodHRwczovL2xvY2FsaG9zdDo4NDQzL2dhdGV3YXkva25veGNyZWRzL2tub3h0b2tlbi9hcGkvdjEvandrcy5qc29uIiwia2lkIjoiS1M2enJFSUsxSFJTbUQ4Ri1FckhnOWc2bHlSQ1A1WUcxLUN0TFFaM2U2byIsInR5cCI6IkpXVCIsImFsZyI6IlJTMjU2In0.eyJzdWIiOiJndWVzdCIsImFjd...uIiwia2lkIjoiS1M2enJFSUsxSFJTbUQ4Ri1FckhnOWc2bHlSQ1A1WUcxLUN0TFFaM2U2byIsImlzcyI6IktOT1hTU08iLCJtYW5hZ2VkLnRva2VuIjoidHJ1ZSIsImtub3guaWQiOiI1YTk4MzM4ZC1kNjlkLTRlNTEtYWNiMS0yNzVhNzZkYzE4M2UifQ.VJOd0pJFwQpWFWv7Xo661pKFMl8md_1UmYf1kLdQIgeVfLhhwWINAKqpD-9Od6YwWIXlfr3SBGbPoHeMQYZp1fEOt4fx1gyFo08VGhDSJMI63FX93KpvDrdeECKwcKimIhnh9H9VEQLP56WOBqn3eYoc8aJFua4Ydh9dC0b0AbnVSrDqoS3hOJLwGsj602NvsIU5IoxmYz8s7rNO7CN6qynxmTp-w4g1Q3skmqU8zki9DvEMJXMRdMsflssFDWrybw0UvzMjjmKQxzKqcb9jsnM3lrsqit7-JEg6kPNA5M5IWIKgkIgt-P_iYAr6ouXt0BxxGfJplK_rF2qxkD98xg","token_id":"5a98338d-d69d-4e51-acb1-275a76dc183e","managed":"true","endpoint_public_cert":"MIIDZDCCAkygAwIBAgIISWFJksv5UD4wDQYJKoZIhvcNAQELBQAwXzESMBAGA1UEAwwJbG9jYWxob3N0MQ0wCwYDVQQLDARUZXN0MQ8wDQYDVQQKDAZIYWRvb3AxDTALBgNVBAcMBFRlc3QxDTALBgNVBAgMBFRlc3QxCzAJBgNVBAYTAlVTMB4XDTI2MDYxMDIxMTg1M1oXDTI3MDYxMDIxMTg1M1owXzESMBAGA1UEAwwJbG9jYWxob3N0MQ0wCwYDVQQLDARUZXN0MQ8wDQYDVQQKDAZIYWRvb3AxDTALBgNVBAcMBFRlc3QxDTALBgNVBAgMBFRlc3QxCzAJBgNVBAYTAlVTMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApSDLZB9y0gV5W9HEmV49GAo4PH9JTmfISUCTGYZ5z6pwU0ierFz0Qm....OCNkLF4fj5iQAqyp+6lF7e1Wj50oaKCTsLkxV+3bn2PVdI7rYR9wS6t6V7MuoaLbgcL/RY8/ec3qUC6NvQIDAQABoyQwIjAgBgNVHREEGTAXggpHV1dZRkc2NU1Rgglsb2NhbGhvc3QwDQYJKoZIhvcNAQELBQADggEBAD8URUGKn4Em8AtVBrLxfwxyvPxWHXUONERbFKizIV2xkTcxgNnoKKUaHX2A8f0DW1OQ1KsiR2mp8ygeZcqgsCcEpPGnzYnfknD58nEDOXPpg2vy64ppIXzXPbR6PUBQo9zuIH5n+JlFhR5dP0WugqdNarKwtNCSjDRmjiLki4quuM8Wd4kSflHZmFwehqUH0Hc4mtdvrrCZUrz3RnK0N/oBW3Mbr5/0gj6CzC4YgKqCipRzCvJ9RKrewOumPLhg5m2Zb6NfDJXVFG4WhrYF+dRX0Dljngy2eLsTOdg4nlenRZYZWZL6/UpnpjSik/J3AFB7CPLStC280QP6m/89R5o=","token_type":"Bearer","expires_in":-1,"passcode":"TldFNU9ETXpPR1F0WkRZNVpDMDBaVFV4TFdGallqRXRNamMxWVRjMlpHTXhPRE5sOjpZVEF5TlRVeFlqSXRPR05rTVMwME56STFMVGhrTlRZdE1USmtNamxpT0dSbVpURmg="}%

curl -ivk -H 'Authorization: Bearer eyJqa3UiOiJodHRwczovL2xvY2FsaG9zdDo4NDQzL2dhdGV3YXkva25veGNyZWRzL2tub3h0b2tlbi9hcGkvdjEvandrcy5qc29uIiwia2lkIjoiS1M2enJFSUsxSFJTbUQ4Ri1....1ZSIsImtub3guaWQiOiI1YTk4MzM4ZC1kNjlkLTRlNTEtYWNiMS0yNzVhNzZkYzE4M2UifQ.VJOd0pJFwQpWFWv7Xo661pKFMl8md_1UmYf1kLdQIgeVfLhhwWINAKqpD-9Od6YwWIXlfr3SBGbPoHeMQYZp1fEOt4fx1gyFo08VGhDSJMI63FX93KpvDrdeECKwcKimIhnh9H9VEQLP56WOBqn3eYoc8aJFua4Ydh9dC0b0AbnVSrDqoS3hOJLwGsj602NvsIU5IoxmYz8s7rNO7CN6qynxmTp-w4g1Q3skmqU8zki9DvEMJXMRdMsflssFDWrybw0UvzMjjmKQxzKqcb9jsnM3lrsqit7-JEg6kPNA5M5IWIKgkIgt-P_iYAr6ouXt0BxxGfJplK_rF2qxkD98xg' 'https://localhost:8443/gateway/knoxidf/knoxtoken/v1/oauth/tokens?doAs=tom'

{"access_token":"eyJqa3UiOiJodHRwczovL2xvY2FsaG9zdDo4NDQzL2dhdGV3YXkva25veGlkZi9rbm94dG9rZW4vdjEvb2F1dGgvandrcy5qc29uIiwia2lkIjoiS1M2enJFSUsxSFJTbUQ4Ri1FckhnOWc2bHlSQ1A1WUcxLUN0TFFaM2U2byIsInR5cCI6IkpXVCIsImFsZyI6IlJTMjU2In0.eyJzdWIiOiJ0b20iLCJhY3QiOn....ZTZvIiwiaXNzIjoiS05PWFNTTyIsIm1hbmFnZWQudG9rZW4iOiJ0cnVlIiwia25veC5pZCI6IjQwYTFmNmNmLWRmYzQtNDU3Ny05MmNjLWEwMTI5M2RkYWZiZCJ9.kUi8Mc7E7njrrh3B-UWgDjJVlWm23rRmd6p9en79Q-6Q_I5qw1LQf6bqUPqTwZ35JgZwfcSodFcSuitJwgOOoAbhx3qZR8mTVhoqNyoUtSqmtixkSvMQwXTBZtcWDKyjc208LZCs6Vv21T_oHXHuInSUZnTjghoM_gh1JrcIkxvLPaz8utL-aWc_1cJvNKNvQtz5uuU_FynLLhBCJQ-dJnKOlJ4-HXZwlnA9QhaEQDW-07n-R7ZhM_urqrrmYdYKNlyRztoCiD7M13pSwdjxf4AQura1vV7AyRwEEfI1gTPZ7L2xP52dVBU1R_sBz83NuXzSOumpGWJa8x4eWOIXsA","refresh_token":"d7622c25-4514-463d-a608-37aadbba0cce","issued_token_type":"urn:ietf:params:oauth:token-type:access_token","token_type":"Bearer","expires_in":-1}

image

@github-actions

Copy link
Copy Markdown

Test Results

22 tests   22 ✅  2s ⏱️
 1 suites   0 💤
 1 files     0 ❌

Results for commit 8d2a990.

@lmccay lmccay requested review from hanicz and smolnar82 June 11, 2026 00:03
Comment on lines +66 to +70
public String getName() {
// Return the current actor's identity as the principal name
String currentActor = getCurrentActor();
return currentActor != null ? currentActor : "";
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did not find any use of this method, I think it can be simply removed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe getName() is part of the Principal interface and cannot be removed.

* <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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe return Optional<String> instead of null?

Comment on lines +1111 to +1133
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");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic deserves its own private method (e.g. handleDelegatedAuth), including the if statement -> here it would only be one method call (e.g. handleDelegatedAuth(...).


// RFC 8693 Token Exchange: Preserve ActorChainPrincipal from the current subject
// This ensures the delegation chain is maintained through identity assertion
final Set<ActorChainPrincipal> actorChainPrincipals = currentSubject.getPrincipals(ActorChainPrincipal.class);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have named methods in SubjectUtils to get different principals from current subject (getCurrentGroupPrincipals, getTokenIdPrincipals, ...), I think we should add getActorChainPrincipals there too.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh, yes, I forgot to go back and do this but meant to.

Comment on lines +161 to +168
if (token == null) {
return Collections.emptyList();
}

Object actClaim = token.getClaimAsObject(JWTToken.ACT_CLAIM);
if (actClaim == null) {
return Collections.emptyList();
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These 2 ifs could be merged:

if (token == null || token.getClaimAsObject(JWTToken.ACT_CLAIM) == null) {
   return Collections.emptyList();
}

Maybe something like this?

public static List<Map<String, Object>> extractActorChain(JWT token) {
    final 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);
}

@lmccay lmccay changed the title Knox 3334 Knox 3334 - Introduce ActorChainPrincipal for RFC 8693 instead of ImpersonatedPrincipal Jun 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants