Sandor Molnar created KNOX-3403:
-----------------------------------
Summary: RFC 8693 token exchange: issued token 'sub' is the actor
instead of the subject on non-server-managed topologies
Key: KNOX-3403
URL: https://issues.apache.org/jira/browse/KNOX-3403
Project: Apache Knox
Issue Type: Bug
Components: Server
Affects Versions: 3.0.0
Reporter: Sandor Molnar
Assignee: Sandor Molnar
Fix For: 3.0.0
h2. Summary
When a token is issued via an RFC 8693 token exchange
({{{}grant_type=urn:ietf:params:oauth:grant-type:token-exchange{}}}, with a
{{subject_token}} and an {{{}actor_token{}}}) on a topology whose {{KNOXTOKEN}}
service is *not* server-managed, the issued JWT's {{sub}} is set to the *actor*
(the authenticated caller / primary principal) instead of the *subject* (the
impersonated end user). The impersonated identity is effectively dropped from
{{{}sub{}}}, defeating the on-behalf-of semantics.
h2. Impact
* RFC 8693 OBO on a non-server-managed topology: the exchanged token
represents the acting party (e.g. the service account) rather than the end
user. The {{act}} claim is correct; only {{sub}} is wrong.
* Traditional {{doAs}} is NOT affected - there the identity-assertion request
wrapper already exposes the impersonated user via {{{}getUserPrincipal(){}}},
so the issued {{sub}} is correct.
* Server-managed topologies are NOT affected - the impersonation-aware
username resolution already runs.
h2. Reproduction
# Deploy a NON-server-managed topology with a JWTProvider (trusting both the
subject and actor issuers) + KNOXTOKEN
({{{}knox.token.enable.delegated.auth=true{}}}) + identity-assertion Default.
# Obtain a {{subject_token}} (an end user, e.g. admin) and an {{actor_token}}
for a DIFFERENT principal (e.g. a k8s service account).
# POST the exchange:
{code:java}
grant_type=urn:ietf:params:oauth:grant-type:token-exchange
subject_token=<user JWT>
subject_token_type=urn:ietf:params:oauth:token-type:jwt
actor_token=<service-account JWT>
actor_token_type=urn:ietf:params:oauth:token-type:jwt
{code}
# Decode the issued access_token:
** Actual: {{sub}} = the actor (e.g. system:serviceaccount:...), {{act}} = \{
sub: actor }
** Expected: {{sub}} = the subject (the end user), {{act}} = \{ sub: actor }
# Running the same exchange on a server-managed topology yields the correct
{{sub}} - isolating the cause to the non-server-managed code path.
h2. Root cause
{{TokenResource.buildUserContext(HttpServletRequest)}} initializes the token
username from {{request.getUserPrincipal().getName()}} and only replaces it
with the impersonated principal inside an {{if (tokenStateService != null)}}
(i.e. server-managed) block:
{code:java}
String userName = request.getUserPrincipal().getName();
String createdBy = null;
if (tokenStateService != null) { // <-- only when
server-managed
final Subject subject = SubjectUtils.getCurrentSubject();
if (subject != null && SubjectUtils.isImpersonating(subject)) {
String primaryPrincipalName =
SubjectUtils.getPrimaryPrincipalName(subject);
String impersonatedPrincipalName =
SubjectUtils.getImpersonatedPrincipalName(subject);
if (!primaryPrincipalName.equals(impersonatedPrincipalName)) {
createdBy = primaryPrincipalName;
userName = impersonatedPrincipalName;
}
}
}
{code}
In the RFC 8693 token-exchange flow the principal reaching TokenResource via
{{getUserPrincipal()}} is the actor (primary principal), so the impersonated
subject is only applied by the override above. Because that override is gated
on {{{}tokenStateService != null{}}}, it is skipped on non-server-managed
topologies and {{sub}} falls back to the actor.
The gate predates token exchange and was intended only to guard metadata
storage ({{{}userName{}}}/{{{}createdBy{}}} persisted to the token state
service), not to control the issued token's {{{}sub{}}}.
h2. Fix
Move the impersonation -> {{userName}} resolution out of the
{{tokenStateService != null}} block so the issued token's {{sub}} is the
effective (impersonated) identity whenever
{{SubjectUtils.isImpersonating(subject)}} is true, regardless of server-managed
state. {{createdBy}} continues to be persisted only when server-managed
({{{}persistTokenDetails{}}} already guards on {{{}tokenStateService !=
null{}}}, so computing it otherwise is harmless). Remove the now-stale comment.
h2. Tests
Add coverage in {{TokenServiceResourceTest}} on a NON-server-managed topology:
* An RFC 8693 / impersonated subject (PrimaryPrincipal = actor,
ImpersonatedPrincipal = subject, getUserPrincipal() = actor) yields \{sub ==
subject} and \{act == { sub: actor }}.
* A non-impersonating request still yields \{sub == primary}(no regression).
* Confirm server-managed behavior is unchanged.
h2. Related
Discovered while validating KNOX-3399 (RFC 8693 token exchange) on a non-server
managed exchange topology. Independent of that routing fix.
--
This message was sent by Atlassian Jira
(v8.20.10#820010)