Skip to content

KNOX-3403: RFC 8693 token exchange: use the impersonated subject as the token sub on non-server-managed topologies - #1336

Merged
smolnar82 merged 1 commit into
apache:masterfrom
smolnar82:KNOX-3403
Jul 31, 2026
Merged

KNOX-3403: RFC 8693 token exchange: use the impersonated subject as the token sub on non-server-managed topologies#1336
smolnar82 merged 1 commit into
apache:masterfrom
smolnar82:KNOX-3403

Conversation

@smolnar82

@smolnar82 smolnar82 commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

KNOX-3403 - Token exchange issues the wrong sub on non-server-managed topologies

What changes were proposed in this pull request?

When a token was issued under impersonation via an RFC 8693 token exchange (subject_token + actor_token) on a topology that is not server-managed, the issued JWT's sub was set to the actor (the primary principal / authenticated caller) instead of the subject (the impersonated end user) - so the token represented the acting party rather than the user, defeating the on-behalf-of semantics. (The act claim was already correct; only sub was wrong.)

Root cause: TokenResource.buildUserContext(...) only replaced the default username (the primary principal, from request.getUserPrincipal()) with the impersonated principal inside an if (tokenStateService != null) block - i.e. only when the topology is server-managed. That 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.

This PR moves the impersonation -> userName resolution out of that gate, so the issued token's sub is the effective (impersonated) identity whenever SubjectUtils.isImpersonating(subject) is true, independent of server-managed state. createdBy continues to be persisted only for managed tokens (persistTokenDetails already guards on tokenStateService != null, so computing it otherwise is harmless). The stale comment was updated.

Scope: only the non-server-managed impersonation path changes. Traditional doAs was already correct (the identity-assertion request wrapper exposes the impersonated user via getUserPrincipal()), server-managed topologies are unchanged, and non-impersonating requests are unchanged.

How was this patch tested?

  • Reproduced end-to-end on a live gateway: an RFC 8693 exchange on a non-server-managed topology returned sub = <actor> (wrong); the same exchange on a server-managed topology returned the correct sub, isolating the cause to this gate.
  • Added unit tests in TokenServiceResourceTest:
    • testImpersonatedTokenSubjectOnNonServerManagedTopology: non-server-managed + impersonation (actor admin, subject bob) asserts sub == bob and act == { sub: admin }.
    • testNonImpersonatedTokenSubjectOnNonServerManagedTopology: non-server-managed, no impersonation asserts sub == <primary> and no act claim (no regression).
  • Confirmed both new tests fail without the fix (expected:<bob> but was:<alice>) and pass with it; full TokenServiceResourceTest passes (73 tests) with 0 Checkstyle violations.

Manual testing:

Step 1 - mint two DISTINCT Knox tokens from sandbox (subject ≠ actor, so act populates)

export SUBJ=$(curl -sku tom:tom-password  "https://localhost:8443/gateway/sandbox/knoxtoken/api/v2/token" | jq -r .access_token)
export ACTOR=$(curl -sku sam:sam-password "https://localhost:8443/gateway/sandbox/knoxtoken/api/v2/token" | jq -r .access_token)

Step 2 - create a NON‑server‑managed exchange topology obo

All tokens here are Knox‑issued (iss=KNOXSSO, typ=JWT), so no JWKS/CA/typ‑ignore needed — Knox verifies them with its own key; conf/topologies/obo.xml looks like this:

<topology>
  <gateway>
    <provider><role>federation</role><name>JWTProvider</name><enabled>true</enabled></provider>
    <provider><role>identity-assertion</role><name>Default</name><enabled>true</enabled></provider>
  </gateway>
  <service>
    <role>KNOXTOKEN</role>
    <param><name>knox.token.enable.delegated.auth</name><value>true</value></param>
  </service>
</topology>

Notes:

  • NO server-managed -> the external‑style validation path
  • Default identity‑assertion is required - it's what maps the exchange subject to the impersonated principal
  • enable.delegated.auth adds the act claim.)

Step 3 - verification

Verified these combinations:

Change on obo subject/actor Expected result What it validates
baseline (non-managed, enable.delegated.auth=true) tom / sam sub=tom, act={sub:sam} the fix
enable.delegated.auth=false tom / sam sub=tom, no act act gated on delegated auth
same identity (both $SUBJ) tom / tom sub=tom, no act actor==subject equality guard
knox.token.exp.server-managed=true on the KNOXTOKEN service tom / sam sub=tom, act={sub:sam} issued-token tracking makes tokenStateService != null, so the correct subject is used (works even without the fix)
knox.token.exp.server-managed=true on the JWTProvider tom / sam 401 "Unknown token" incoming-token validation does a state-store lookup; untracked tokens are rejected (why the exchange topology's JWTProvider must be non-managed)

I used the following curl command and its variations:

curl -sk -X POST "https://localhost:8443/gateway/obo/knoxtoken/api/v2/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" \
  -d "subject_token=$SUBJ" -d "subject_token_type=urn:ietf:params:oauth:token-type:jwt" \
  -d "actor_token=$ACTOR" -d "actor_token_type=urn:ietf:params:oauth:token-type:jwt" \
  | jq -r .access_token | cut -d. -f2 | tr '_-' '/+' | base64 -d 2>/dev/null | jq '{sub, act}'

Step 4 - doAs sanity check on sandbox (should be unchanged by this PR to confirm no regression):

# token management ON
$ curl -sku admin:admin-password "https://localhost:8443/gateway/sandbox/knoxtoken/api/v2/token?doAs=bob"   | jq -r .access_token | cut -d. -f2 | tr '_-' '/+' | base64 -d 
{"sub":"bob","jku":"https://localhost:8443/gateway/sandbox/knoxtoken/api/v2/jwks.json","kid":"GpOl8XfsNAOFzROVhb-HM7vJ5CRqR1GMM7pXHdQ29VQ","iss":"KNOXSSO","exp":1795815685,"managed.token":"true","knox.id":"aa9a0ee1-5b15-4d19-a3bc-2da431dec7cc"}

# token management OFF
$ curl -sku admin:admin-password "https://localhost:8443/gateway/sandbox/knoxtoken/api/v2/token?doAs=bob"   | jq -r .access_token | cut -d. -f2 | tr '_-' '/+' | base64 -d 
{"sub":"bob","jku":"https://localhost:8443/gateway/sandbox/knoxtoken/api/v2/jwks.json","kid":"GpOl8XfsNAOFzROVhb-HM7vJ5CRqR1GMM7pXHdQ29VQ","iss":"KNOXSSO","exp":1795815670,"managed.token":"false","knox.id":"d6391a02-b0a8-4c5d-aa11-d4f44630c2b1"}

Integration Tests

N/A

UI changes

N/A

…he token sub on non-server-managed topologies
@github-actions

Copy link
Copy Markdown

Test Results

40 tests   40 ✅  6s ⏱️
 3 suites   0 💤
 3 files     0 ❌

Results for commit eb26d71.

@lmccay lmccay left a comment

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.

+1

@smolnar82
smolnar82 merged commit da08af1 into apache:master Jul 31, 2026
3 of 4 checks passed
@smolnar82
smolnar82 deleted the KNOX-3403 branch July 31, 2026 05:11
smolnar82 added a commit that referenced this pull request Jul 31, 2026
…he token sub on non-server-managed topologies (#1336)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants