From eb26d71f1e830aaf14b1e90eb4e692b06d33be3d Mon Sep 17 00:00:00 2001 From: Sandor Molnar Date: Thu, 30 Jul 2026 23:15:26 +0200 Subject: [PATCH] KNOX-3403: RFC 8693 token exchange: use the impersonated subject as the token sub on non-server-managed topologies --- .../service/knoxtoken/TokenResource.java | 25 ++++----- .../knoxtoken/TokenServiceResourceTest.java | 51 +++++++++++++++++++ 2 files changed, 64 insertions(+), 12 deletions(-) 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 8da6f137b1..6d876648bd 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 @@ -920,18 +920,19 @@ protected Response onlyAllowGroupsToBeAddedWhenEnabled() { protected UserContext buildUserContext(HttpServletRequest request) { String userName = request.getUserPrincipal().getName(); String createdBy = null; - // checking the doAs user only makes sense if tokens are managed (this is where we store the userName/createdBy information) - // and if impersonation was enabled before (on HadoopAuth or identity-assertion level) so the the current subject has at least one ImpersonatedPrincipal principal - if (tokenStateService != null) { - 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; - log.tokenImpersonationSuccess(createdBy, userName); - } + // When impersonation is in effect, the issued token's subject must be the effective (impersonated) + // identity, not the primary principal. This applies to traditional doAs as well as RFC 8693 token + // exchange (where the actor is the primary principal and the subject is the impersonated one). + // This is independent of server-managed state: createdBy is only persisted for managed tokens (see + // persistTokenDetails), so computing it here is harmless when there is no token state service. + 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; + log.tokenImpersonationSuccess(createdBy, userName); } } return new UserContext(userName, createdBy); 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 281ea90ea6..8398e168b1 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 @@ -2220,4 +2220,55 @@ public void testNoActClaimWithoutImpersonation() throws Exception { EasyMock.verify(request, context); } + + /** + * KNOX-3403: On a NON-server-managed topology, an impersonated request (traditional doAs or RFC 8693 + * token exchange, where the actor is the primary principal and the subject is the impersonated one) + * must still issue a token whose {@code sub} is the impersonated subject. Previously buildUserContext + * only applied the impersonated identity when {@code tokenStateService != null}, so on a non-managed + * topology the {@code sub} incorrectly fell back to the primary principal (the actor). + */ + @Test + @SuppressForbidden + public void testImpersonatedTokenSubjectOnNonServerManagedTopology() throws Exception { + final String primaryUser = "admin"; // primary principal (authenticated caller / actor) + final String impersonatedUser = "bob"; // impersonated subject (distinct from USER_NAME) + + // No serverManagedTssEnabled argument -> token state service is absent (non-server-managed). + configureCommonExpectations(createDelegatedAuthContextExpectations(true, true)); + Subject subject = createSubjectWithOptionalImpersonation(primaryUser, impersonatedUser); + JWTToken parsedToken = getTokenWithSubject(subject); + + // The token subject must be the impersonated user, not the primary. + assertEquals("Non-server-managed impersonated token must use the impersonated subject as sub", + impersonatedUser, parsedToken.getSubject()); + + // The 'act' claim still records the primary user (delegated auth enabled). + Object actClaim = parsedToken.getClaimAsObject(JWTToken.ACT_CLAIM); + assertNotNull("RFC 8693 'act' claim should be present", actClaim); + assertTrue("'act' claim should be a Map", actClaim instanceof Map); + @SuppressWarnings("unchecked") + Map actClaimMap = (Map) actClaim; + assertEquals("'act' claim should contain the primary user's subject", primaryUser, actClaimMap.get("sub")); + + EasyMock.verify(request, context); + } + + /** + * KNOX-3403: sanity check that decoupling the impersonated-sub from server-managed does not change + * the non-impersonating case - the token {@code sub} remains the authenticated (primary) user on a + * non-server-managed topology. + */ + @Test + @SuppressForbidden + public void testNonImpersonatedTokenSubjectOnNonServerManagedTopology() throws Exception { + configureCommonExpectations(createDelegatedAuthContextExpectations(true, false)); + Subject subject = createSubjectWithOptionalImpersonation(USER_NAME, null); + JWTToken parsedToken = getTokenWithSubject(subject); + + assertEquals(USER_NAME, parsedToken.getSubject()); + assertNull("'act' claim should NOT be present without impersonation", parsedToken.getClaimAsObject(JWTToken.ACT_CLAIM)); + + EasyMock.verify(request, context); + } }