Skip to content

Commit 0c88385

Browse files
authored
KNOX-3334 - Introduce ActorChainPrincipal for RFC 8693 instead of ImpersonatedPrincipal (apache#1257)
* KNOX-3334 - Introduce ActorChainPrincipal for RFC 8693 instead of ImpersonatedPrincipal
1 parent 10eedcc commit 0c88385

12 files changed

Lines changed: 830 additions & 28 deletions

File tree

gateway-provider-identity-assertion-common/src/main/java/org/apache/knox/gateway/identityasserter/common/filter/AbstractIdentityAssertionFilter.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
import org.apache.knox.gateway.i18n.GatewaySpiResources;
4747
import org.apache.knox.gateway.i18n.messages.MessagesFactory;
4848
import org.apache.knox.gateway.i18n.resources.ResourcesFactory;
49+
import org.apache.knox.gateway.security.ActorChainPrincipal;
4950
import org.apache.knox.gateway.security.GroupPrincipal;
5051
import org.apache.knox.gateway.security.ImpersonatedPrincipal;
5152
import org.apache.knox.gateway.security.PrimaryPrincipal;
@@ -147,14 +148,19 @@ protected void continueChainAsPrincipal(HttpServletRequestWrapper request, Servl
147148
final Set<TokenIdPrincipal> tokenIdPrincipals = SubjectUtils.getTokenIdPrincipals(currentSubject);
148149
subject.getPrincipals().addAll(tokenIdPrincipals);
149150

151+
// RFC 8693 Token Exchange: Preserve ActorChainPrincipal from the current subject
152+
// This ensures the delegation chain is maintained through identity assertion
153+
final Set<ActorChainPrincipal> actorChainPrincipals = SubjectUtils.getActorChainPrincipal(currentSubject, subject);
154+
subject.getPrincipals().addAll(actorChainPrincipals);
155+
150156
doAs(request, response, chain, subject);
151157
}
152158
else {
153159
doFilterInternal(request, response, chain);
154160
}
155161
}
156162

157-
private void doAs(final ServletRequest request, final ServletResponse response, final FilterChain chain, Subject subject)
163+
private void doAs(final ServletRequest request, final ServletResponse response, final FilterChain chain, Subject subject)
158164
throws IOException, ServletException {
159165
try {
160166
Subject.doAs(

gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/AbstractJWTFilter.java

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import java.util.LinkedHashSet;
3636
import java.util.List;
3737
import java.util.Locale;
38+
import java.util.Map;
3839
import java.util.Set;
3940
import java.util.stream.Collectors;
4041
import java.util.stream.Stream;
@@ -67,6 +68,7 @@
6768
import org.apache.knox.gateway.filter.AbstractGatewayFilter;
6869
import org.apache.knox.gateway.i18n.messages.MessagesFactory;
6970
import org.apache.knox.gateway.provider.federation.jwt.JWTMessages;
71+
import org.apache.knox.gateway.security.ActorChainPrincipalImpl;
7072
import org.apache.knox.gateway.security.PrimaryPrincipal;
7173
import org.apache.knox.gateway.security.SubjectUtils;
7274
import org.apache.knox.gateway.security.TokenIdPrincipal;
@@ -386,14 +388,16 @@ protected Subject createSubjectFromToken(final JWT token) throws UnknownTokenExc
386388
if (expectedPrincipalClaim != null) {
387389
claimvalue = token.getClaim(expectedPrincipalClaim);
388390
}
391+
// Extract actor chain from the JWT token if present (RFC 8693)
392+
List<Map<String, Object>> actorChain = TokenUtils.extractActorChain(token);
389393
// The newly constructed Sets check whether this Subject has been set read-only
390394
// before permitting subsequent modifications. The newly created Sets also prevent
391395
// illegal modifications by ensuring that callers have sufficient permissions.
392396
//
393397
// To modify the Principals Set, the caller must have AuthPermission("modifyPrincipals").
394398
// To modify the public credential Set, the caller must have AuthPermission("modifyPublicCredentials").
395399
// To modify the private credential Set, the caller must have AuthPermission("modifyPrivateCredentials").
396-
return createSubjectFromTokenData(principal, claimvalue);
400+
return createSubjectFromTokenData(principal, claimvalue, null, actorChain);
397401
}
398402

399403
public Subject createSubjectFromTokenIdentifier(final String tokenId) throws UnknownTokenException {
@@ -419,11 +423,19 @@ public Subject createSubjectFromTokenIdentifier(final String tokenId) throws Unk
419423

420424
@SuppressWarnings("rawtypes")
421425
protected Subject createSubjectFromTokenData(final String principal, final String expectedPrincipalClaimValue) {
422-
return createSubjectFromTokenData(principal, expectedPrincipalClaimValue, null);
426+
return createSubjectFromTokenData(principal, expectedPrincipalClaimValue, null, null);
423427
}
424428

425429
@SuppressWarnings("rawtypes")
426430
protected Subject createSubjectFromTokenData(final String principal, final String expectedPrincipalClaimValue, final String tokenId) {
431+
return createSubjectFromTokenData(principal, expectedPrincipalClaimValue, tokenId, null);
432+
}
433+
434+
@SuppressWarnings("rawtypes")
435+
protected Subject createSubjectFromTokenData(final String principal,
436+
final String expectedPrincipalClaimValue,
437+
final String tokenId,
438+
final List<Map<String, Object>> actorChain) {
427439
String claimValue =
428440
(expectedPrincipalClaimValue != null) ? expectedPrincipalClaimValue.toLowerCase(Locale.ROOT) : null;
429441

@@ -435,6 +447,11 @@ protected Subject createSubjectFromTokenData(final String principal, final Strin
435447
principals.add(new TokenIdPrincipal(tokenId));
436448
}
437449

450+
// Add ActorChainPrincipal if an actor chain is present (RFC 8693 token exchange)
451+
if (actorChain != null && !actorChain.isEmpty()) {
452+
principals.add(new ActorChainPrincipalImpl(actorChain));
453+
}
454+
438455
// The newly constructed Sets check whether this Subject has been set read-only
439456
// before permitting subsequent modifications. The newly created Sets also prevent
440457
// illegal modifications by ensuring that callers have sufficient permissions.

gateway-service-knoxtoken/src/main/java/org/apache/knox/gateway/service/knoxtoken/TokenResource.java

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
import org.apache.knox.gateway.config.GatewayConfig;
7171
import org.apache.knox.gateway.context.ContextAttributes;
7272
import org.apache.knox.gateway.i18n.messages.MessagesFactory;
73+
import org.apache.knox.gateway.security.ActorChainPrincipal;
7374
import org.apache.knox.gateway.security.GroupPrincipal;
7475
import org.apache.knox.gateway.security.SubjectUtils;
7576
import org.apache.knox.gateway.security.TokenIdPrincipal;
@@ -1106,21 +1107,41 @@ protected JWT getJWT(String userName, long expires, String jku) throws TokenServ
11061107
jwtAttributesBuilder.setClientId(tokenIdPrincipals.iterator().next().getName());
11071108
}
11081109

1109-
// RFC 8693 Token Exchange: Add the "act" claim if delegated auth is enabled and impersonation occurred
1110-
if (enableDelegatedAuth && SubjectUtils.isImpersonating(subject)) {
1110+
// RFC 8693 Token Exchange: Build the actor chain if delegated auth is enabled
1111+
handleDelegatedAuthentication(subject, jwtAttributesBuilder);
1112+
}
1113+
1114+
jwtAttributes = jwtAttributesBuilder.build();
1115+
token = ts.issueToken(jwtAttributes);
1116+
return token;
1117+
}
1118+
1119+
private void handleDelegatedAuthentication(Subject subject, JWTokenAttributesBuilder jwtAttributesBuilder) {
1120+
if (enableDelegatedAuth) {
1121+
// First check if there's an existing actor chain from a previous token exchange
1122+
Set<ActorChainPrincipal> actorChainPrincipals = subject.getPrincipals(ActorChainPrincipal.class);
1123+
List<Map<String, Object>> existingChain = null;
1124+
if (!actorChainPrincipals.isEmpty()) {
1125+
existingChain = actorChainPrincipals.iterator().next().getActorChain();
1126+
log.generalInfoMessage("Found existing actor chain with " + existingChain.size() + " actors");
1127+
}
1128+
1129+
// Check if impersonation is occurring to add a new actor to the chain
1130+
if (SubjectUtils.isImpersonating(subject)) {
11111131
String primaryPrincipalName = SubjectUtils.getPrimaryPrincipalName(subject);
11121132
String impersonatedPrincipalName = SubjectUtils.getImpersonatedPrincipalName(subject);
11131133
if (primaryPrincipalName != null && impersonatedPrincipalName != null && !primaryPrincipalName.equals(impersonatedPrincipalName)) {
1114-
// The primary principal (the one doing the impersonation) becomes the actor
1115-
jwtAttributesBuilder.setActor(primaryPrincipalName);
1134+
// Build the new actor chain by adding the current actor (primary principal) to the existing chain
1135+
List<Map<String, Object>> newActorChain = TokenUtils.addActorToChain(existingChain, primaryPrincipalName);
1136+
jwtAttributesBuilder.setActorChain(newActorChain);
11161137
log.addingActorClaimToToken(primaryPrincipalName, impersonatedPrincipalName);
11171138
}
1139+
} else if (existingChain != null && !existingChain.isEmpty()) {
1140+
// No new impersonation, but preserve existing actor chain
1141+
jwtAttributesBuilder.setActorChain(existingChain);
1142+
log.generalInfoMessage("Preserving existing actor chain without adding new actor");
11181143
}
11191144
}
1120-
1121-
jwtAttributes = jwtAttributesBuilder.build();
1122-
token = ts.issueToken(jwtAttributes);
1123-
return token;
11241145
}
11251146

11261147
private boolean shouldIncludeGroups() {
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
package org.apache.knox.gateway.service.knoxtoken;
19+
20+
import static org.junit.Assert.assertEquals;
21+
import static org.junit.Assert.assertNotNull;
22+
import static org.junit.Assert.assertTrue;
23+
24+
import java.text.ParseException;
25+
import java.util.ArrayList;
26+
import java.util.LinkedHashMap;
27+
import java.util.List;
28+
import java.util.Map;
29+
30+
import org.apache.knox.gateway.security.ActorChainPrincipal;
31+
import org.apache.knox.gateway.security.ActorChainPrincipalImpl;
32+
import org.apache.knox.gateway.services.security.token.JWTokenAttributesBuilder;
33+
import org.apache.knox.gateway.services.security.token.TokenUtils;
34+
import org.apache.knox.gateway.services.security.token.impl.JWT;
35+
import org.apache.knox.gateway.services.security.token.impl.JWTToken;
36+
import org.junit.Test;
37+
38+
/**
39+
* Test class to verify RFC 8693 actor chain functionality end-to-end.
40+
*/
41+
public class ActorChainTest {
42+
43+
@Test
44+
public void testActorChainPreservation() throws Exception {
45+
// Step 1: Create initial token with actor chain (simulating token from previous exchange)
46+
List<Map<String, Object>> initialChain = new ArrayList<>();
47+
48+
Map<String, Object> actor1 = new LinkedHashMap<>();
49+
actor1.put("sub", "service-a");
50+
actor1.put("iss", "https://issuer.example.com");
51+
initialChain.add(actor1);
52+
53+
Map<String, Object> actor2 = new LinkedHashMap<>();
54+
actor2.put("sub", "service-b");
55+
initialChain.add(actor2);
56+
57+
JWTokenAttributesBuilder builder1 = new JWTokenAttributesBuilder();
58+
builder1.setUserName("end-user")
59+
.setAlgorithm("RS256")
60+
.setExpires(System.currentTimeMillis() + 30000)
61+
.setActorChain(initialChain);
62+
63+
JWTToken token1 = new JWTToken(builder1.build());
64+
65+
// Step 2: Extract actor chain from token (simulating JWT validation)
66+
List<Map<String, Object>> extractedChain = TokenUtils.extractActorChain(token1);
67+
assertNotNull("Extracted chain should not be null", extractedChain);
68+
assertEquals("Should have 2 actors", 2, extractedChain.size());
69+
assertEquals("First actor should be service-a", "service-a", extractedChain.get(0).get("sub"));
70+
assertEquals("Second actor should be service-b", "service-b", extractedChain.get(1).get("sub"));
71+
72+
// Step 3: Create ActorChainPrincipal (simulating what AbstractJWTFilter does)
73+
ActorChainPrincipal actorChainPrincipal = new ActorChainPrincipalImpl(extractedChain);
74+
assertEquals("Current actor should be service-a", "service-a", actorChainPrincipal.getCurrentActor());
75+
assertEquals("Original delegator should be service-b", "service-b", actorChainPrincipal.getOriginalDelegator());
76+
77+
// Step 4: Add new actor to chain (simulating what TokenResource does)
78+
String newActor = "service-c";
79+
List<Map<String, Object>> newChain = TokenUtils.addActorToChain(extractedChain, newActor);
80+
assertEquals("Should have 3 actors", 3, newChain.size());
81+
assertEquals("New actor should be first", newActor, newChain.get(0).get("sub"));
82+
assertEquals("Previous first actor should be second", "service-a", newChain.get(1).get("sub"));
83+
assertEquals("Previous second actor should be third", "service-b", newChain.get(2).get("sub"));
84+
85+
// Step 5: Create new token with extended chain
86+
JWTokenAttributesBuilder builder2 = new JWTokenAttributesBuilder();
87+
builder2.setUserName("end-user")
88+
.setAlgorithm("RS256")
89+
.setExpires(System.currentTimeMillis() + 30000)
90+
.setActorChain(newChain);
91+
92+
JWTToken token2 = new JWTToken(builder2.build());
93+
94+
// Step 6: Verify the new token has the complete chain
95+
List<Map<String, Object>> finalChain = TokenUtils.extractActorChain(token2);
96+
assertNotNull("Final chain should not be null", finalChain);
97+
assertEquals("Should have 3 actors in final token", 3, finalChain.size());
98+
assertEquals("First actor should be service-c", "service-c", finalChain.get(0).get("sub"));
99+
assertEquals("Second actor should be service-a", "service-a", finalChain.get(1).get("sub"));
100+
assertEquals("Third actor should be service-b", "service-b", finalChain.get(2).get("sub"));
101+
102+
// Verify issuer is preserved for actor1
103+
assertEquals("Issuer should be preserved", "https://issuer.example.com", finalChain.get(1).get("iss"));
104+
}
105+
106+
@Test
107+
public void testActorChainInNestedStructure() throws ParseException {
108+
// Create a token with nested actor structure
109+
List<Map<String, Object>> chain = new ArrayList<>();
110+
111+
Map<String, Object> actor1 = new LinkedHashMap<>();
112+
actor1.put("sub", "actor1");
113+
chain.add(actor1);
114+
115+
Map<String, Object> actor2 = new LinkedHashMap<>();
116+
actor2.put("sub", "actor2");
117+
chain.add(actor2);
118+
119+
Map<String, Object> actor3 = new LinkedHashMap<>();
120+
actor3.put("sub", "actor3");
121+
chain.add(actor3);
122+
123+
JWTokenAttributesBuilder builder = new JWTokenAttributesBuilder();
124+
builder.setUserName("testuser")
125+
.setAlgorithm("RS256")
126+
.setExpires(System.currentTimeMillis() + 30000)
127+
.setActorChain(chain);
128+
129+
JWT token = new JWTToken(builder.build());
130+
131+
// Verify the act claim is present
132+
Object actClaim = token.getClaimAsObject(JWTToken.ACT_CLAIM);
133+
assertNotNull("Act claim should be present", actClaim);
134+
assertTrue("Act claim should be a Map", actClaim instanceof Map);
135+
136+
// Verify nested structure
137+
Map<?, ?> level1 = (Map<?, ?>) actClaim;
138+
assertEquals("Level 1 sub should be actor1", "actor1", level1.get("sub"));
139+
140+
Object level2Act = level1.get(JWTToken.ACT_CLAIM);
141+
assertNotNull("Level 2 act should be present", level2Act);
142+
assertTrue("Level 2 act should be a Map", level2Act instanceof Map);
143+
144+
Map<?, ?> level2 = (Map<?, ?>) level2Act;
145+
assertEquals("Level 2 sub should be actor2", "actor2", level2.get("sub"));
146+
147+
Object level3Act = level2.get(JWTToken.ACT_CLAIM);
148+
assertNotNull("Level 3 act should be present", level3Act);
149+
assertTrue("Level 3 act should be a Map", level3Act instanceof Map);
150+
151+
Map<?, ?> level3 = (Map<?, ?>) level3Act;
152+
assertEquals("Level 3 sub should be actor3", "actor3", level3.get("sub"));
153+
}
154+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
package org.apache.knox.gateway.security;
19+
20+
import java.security.Principal;
21+
import java.util.List;
22+
import java.util.Map;
23+
24+
/**
25+
* A Principal that represents the chain of actors (delegation chain) from RFC 8693 token exchange.
26+
*
27+
* <p>This principal is used to represent the 'act' claim chain from a JWT, which provides
28+
* a means to express that delegation has occurred and identify the acting parties to whom
29+
* authority has been delegated.</p>
30+
*
31+
* <p>The actor chain is ordered from most recent (current actor) to oldest (original delegator).
32+
* Each actor in the chain is represented as a Map containing identity claims. According to
33+
* RFC 8693 Section 4.1:</p>
34+
* <ul>
35+
* <li>Identity claims such as 'sub' (subject) and 'iss' (issuer) should be used to identify actors</li>
36+
* <li>The combination of 'iss' and 'sub' may be necessary to uniquely identify an actor</li>
37+
* <li>Non-identity claims (e.g., 'exp', 'nbf', 'aud') are NOT meaningful within 'act' claims
38+
* and should not be used</li>
39+
* </ul>
40+
*
41+
* <p>Example chain structure:</p>
42+
* <pre>
43+
* [
44+
* {"sub": "service-c", "iss": "https://issuer.example.com"}, // Most recent actor
45+
* {"sub": "service-b", "iss": "https://issuer.example.com"}, // Previous actor
46+
* {"sub": "service-a"} // Original delegator
47+
* ]
48+
* </pre>
49+
*
50+
* @see <a href="https://datatracker.ietf.org/doc/html/rfc8693#section-4.1">RFC 8693 Section 4.1 - Actor Claim</a>
51+
*/
52+
public interface ActorChainPrincipal extends Principal {
53+
54+
/**
55+
* Returns the chain of actors in order from most recent to oldest.
56+
*
57+
* <p>Each Map in the list represents an actor's claims, with at minimum a 'sub' claim.
58+
* The first element in the list is the most recent actor (the one who directly
59+
* performed the current delegation), and the last element is the original delegator.</p>
60+
*
61+
* @return an immutable list of actor claim maps, never null but may be empty
62+
*/
63+
List<Map<String, Object>> getActorChain();
64+
65+
/**
66+
* Returns the subject (identity) of the most recent actor in the chain.
67+
*
68+
* <p>This is equivalent to calling {@code getActorChain().get(0).get("sub")}
69+
* if the chain is not empty.</p>
70+
*
71+
* @return the subject of the most recent actor, or null if the chain is empty
72+
*/
73+
String getCurrentActor();
74+
75+
/**
76+
* Returns the subject (identity) of the original delegator (the first actor in the chain).
77+
*
78+
* <p>This is the actor who initiated the delegation chain. It is equivalent to calling
79+
* {@code getActorChain().get(getActorChain().size() - 1).get("sub")}
80+
* if the chain is not empty.</p>
81+
*
82+
* @return the subject of the original delegator, or null if the chain is empty
83+
*/
84+
String getOriginalDelegator();
85+
}

0 commit comments

Comments
 (0)