This is an automated email from the ASF dual-hosted git repository. smolnar82 pushed a commit to branch knox_idf in repository https://gitbox.apache.org/repos/asf/knox.git
commit baefcd58ac3843695fbc838a768aa966d6eed6c1 Author: Sandor Molnar <[email protected]> AuthorDate: Mon Aug 10 16:58:20 2026 +0200 KNOX-3408 - Move act-claim/ActorChainPrincipal coverage into TokenExchangeHandlerTest; drop redundant JWTFederationFilterHandleTokenExchangeTest --- ...JWTFederationFilterHandleTokenExchangeTest.java | 262 --------------------- .../jwt/filter/TokenExchangeHandlerTest.java | 29 +++ 2 files changed, 29 insertions(+), 262 deletions(-) diff --git a/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/JWTFederationFilterHandleTokenExchangeTest.java b/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/JWTFederationFilterHandleTokenExchangeTest.java deleted file mode 100644 index ca23a5ae7..000000000 --- a/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/JWTFederationFilterHandleTokenExchangeTest.java +++ /dev/null @@ -1,262 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * <p> - * http://www.apache.org/licenses/LICENSE-2.0 - * <p> - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ -package org.apache.knox.gateway.provider.federation; - -import com.nimbusds.jose.crypto.RSASSASigner; -import com.nimbusds.jwt.SignedJWT; -import org.apache.knox.gateway.provider.federation.jwt.filter.AbstractJWTFilter; -import org.apache.knox.gateway.provider.federation.jwt.filter.JWTFederationFilter; -import org.apache.knox.gateway.security.ActorChainPrincipal; -import org.apache.knox.gateway.security.CommonTokenConstants; -import org.apache.knox.gateway.security.ImpersonatedPrincipal; -import org.apache.knox.gateway.security.PrimaryPrincipal; -import org.apache.knox.gateway.security.SubjectUtils; -import org.apache.knox.gateway.security.TokenExchangePrincipal; -import org.apache.knox.gateway.services.security.token.JWTokenAttributesBuilder; -import org.apache.knox.gateway.services.security.token.impl.JWTToken; -import org.easymock.EasyMock; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.security.Principal; -import java.util.Date; -import java.util.List; -import java.util.Map; -import java.util.Properties; -import java.util.Set; - -import static org.apache.knox.gateway.provider.federation.jwt.filter.AbstractJWTFilter.JWT_DEFAULT_ISSUER; - -/** - * Unit tests for the {@link JWTFederationFilter#handleTokenExchange} method (OIDC - * delegation path). Each test verifies the Subject constructed. - * - * <p>These tests use {@link TestJWTFederationFilter} with {@link TestJWTokenAuthority} (static key, - * no mocking needed — both tokens use Knox issuer {@code JWT_DEFAULT_ISSUER} which is in the - * static expected-issuers list). - * - * <p>The filter's {@code continueWithEstablishedSecurityContext} runs - * {@code Subject.doAs(subject, () -> chain.doFilter(request, response))}. The - * {@link AbstractJWTFilterTest.TestFilterChain} captures {@code SubjectUtils.getCurrentSubject()} - * from within that doAs context, which is exactly the Subject built. All principal - * assertions use {@code chain.subject.getPrincipals(XxxPrincipal.class)}. - */ -public class JWTFederationFilterHandleTokenExchangeTest extends AbstractJWTFilterTest { - - static final String ACTOR_ISSUER = "https://actor.oidc.example.com"; - - @Before - public void setUp() throws Exception { - handler = new TestJWTFederationFilter(); - ((TestJWTFederationFilter) handler).setTokenService(new TestJWTokenAuthority(publicKey)); - handler.init(new TestFilterConfig(getProperties())); - } - - @Override - protected void setTokenOnRequest(HttpServletRequest request, SignedJWT jwt) { - EasyMock.expect(request.getHeader("Authorization")) - .andReturn(JWTFederationFilter.BEARER + jwt.serialize()).anyTimes(); - } - - @Override - protected void setGarbledTokenOnRequest(HttpServletRequest request, SignedJWT jwt) { - EasyMock.expect(request.getHeader("Authorization")) - .andReturn(JWTFederationFilter.BEARER + "ljm" + jwt.serialize()).anyTimes(); - } - - @Override - protected String getAudienceProperty() { - return JWTFederationFilter.KNOX_TOKEN_AUDIENCES; - } - - @Override - protected String getVerificationPemProperty() { - return JWTFederationFilter.TOKEN_VERIFICATION_PEM; - } - - /** - * When both subject_token and actor_token are present, the filter establishes the actor - * (from actor_token.sub) as the PrimaryPrincipal in the resulting Subject. - */ - @Test - public void testActorAndSubjectTokensSetActorAsPrimaryPrincipal() throws Exception { - SignedJWT subjectJwt = getJWT(JWT_DEFAULT_ISSUER, "k8s-sa", - new Date(System.currentTimeMillis() + 60000), privateKey); - SignedJWT actorJwt = getJWT(JWT_DEFAULT_ISSUER, "actor-svc", - new Date(System.currentTimeMillis() + 60000), privateKey); - - HttpServletRequest request = buildTokenExchangeRequest(subjectJwt.serialize(), actorJwt.serialize()); - EasyMock.replay(request); - HttpServletResponse response = EasyMock.createNiceMock(HttpServletResponse.class); - EasyMock.replay(response); - - TestFilterChain chain = new TestFilterChain(); - handler.doFilter(request, response, chain); - - Assert.assertTrue("doFilterCalled should be true", chain.doFilterCalled); - Set<PrimaryPrincipal> principals = chain.subject.getPrincipals(PrimaryPrincipal.class); - Assert.assertEquals("Expected exactly one PrimaryPrincipal", 1, principals.size()); - Assert.assertEquals("Expected actor as PrimaryPrincipal", "actor-svc", - ((Principal) principals.toArray()[0]).getName()); - } - - /** - * When subject_token and actor_token have different issuers, the filter creates a - * TokenExchangePrincipal that carries the subject and actor identities with their respective - * issuers. Using different issuers ensures that all four TEP fields can be asserted - * unambiguously. - * - * <p>Both issuers are added to the static {@code jwt.expected.issuer} whitelist — - * {@code TestJWTokenAuthority} accepts either token because they are signed with - * the same test key. - */ - @Test - public void testActorAndSubjectTokensCreateTokenExchangePrincipal() throws Exception { - Properties props = getProperties(); - props.setProperty(AbstractJWTFilter.JWT_EXPECTED_ISSUER, JWT_DEFAULT_ISSUER + "," + ACTOR_ISSUER); - handler.init(new TestFilterConfig(props)); - - SignedJWT subjectJwt = getJWT(JWT_DEFAULT_ISSUER, "k8s-sa", - new Date(System.currentTimeMillis() + 60000), privateKey); - SignedJWT actorJwt = getJWT(ACTOR_ISSUER, "actor-svc", - new Date(System.currentTimeMillis() + 60000), privateKey); - - HttpServletRequest request = buildTokenExchangeRequest(subjectJwt.serialize(), actorJwt.serialize()); - EasyMock.replay(request); - HttpServletResponse response = EasyMock.createNiceMock(HttpServletResponse.class); - EasyMock.replay(response); - - TestFilterChain chain = new TestFilterChain(); - handler.doFilter(request, response, chain); - - Assert.assertTrue("doFilterCalled should be true", chain.doFilterCalled); - TokenExchangePrincipal tep = SubjectUtils.getTokenExchangePrincipal(chain.subject); - Assert.assertNotNull("TokenExchangePrincipal should be present", tep); - Assert.assertEquals("Subject principal name", "k8s-sa", tep.getSubjectPrincipalName()); - Assert.assertEquals("Actor principal name", "actor-svc", tep.getActorPrincipalName()); - Assert.assertEquals("Subject issuer", JWT_DEFAULT_ISSUER, tep.getSubjectIssuer()); - Assert.assertEquals("Actor issuer", ACTOR_ISSUER, tep.getActorIssuer()); - } - - /** - * When subject_token carries an {@code act} claim (a prior delegation chain), the filter - * extracts it and creates an {@code ActorChainPrincipal} in the resulting Subject so that - * the delegation history is preserved through the filter pipeline. - */ - @Test - public void testSubjectTokenWithActClaimCreatesActorChainPrincipal() throws Exception { - List<Map<String, Object>> actorChainData = List.of(Map.of("sub", "prior-actor")); - JWTToken subjectToken = new JWTToken(new JWTokenAttributesBuilder() - .setUserName("k8s-sa") - .setIssuer(JWT_DEFAULT_ISSUER) - .setAlgorithm("RS256") - .setExpires(System.currentTimeMillis() + 60000) - .setActorChain(actorChainData) - .build()); - subjectToken.sign(new RSASSASigner(privateKey)); - - SignedJWT actorJwt = getJWT(JWT_DEFAULT_ISSUER, "actor-svc", - new Date(System.currentTimeMillis() + 60000), privateKey); - - HttpServletRequest request = buildTokenExchangeRequest(subjectToken.toString(), actorJwt.serialize()); - EasyMock.replay(request); - HttpServletResponse response = EasyMock.createNiceMock(HttpServletResponse.class); - EasyMock.replay(response); - - TestFilterChain chain = new TestFilterChain(); - handler.doFilter(request, response, chain); - - Assert.assertTrue("doFilterCalled should be true", chain.doFilterCalled); - Set<ActorChainPrincipal> actorChainPrincipals = chain.subject.getPrincipals(ActorChainPrincipal.class); - Assert.assertFalse("ActorChainPrincipal should be present", actorChainPrincipals.isEmpty()); - ActorChainPrincipal acp = actorChainPrincipals.iterator().next(); - Assert.assertEquals("Expected current actor from act claim", "prior-actor", acp.getCurrentActor()); - } - - /** - * With no actor_token provided, the filter proceeds successfully and the resulting Subject - * has the subject itself as PrimaryPrincipal, no TokenExchangePrincipal, and no - * ImpersonatedPrincipal. - */ - @Test - public void testSubjectTokenOnlySucceeds() throws Exception { - SignedJWT subjectJwt = getJWT(JWT_DEFAULT_ISSUER, "k8s-sa", - new Date(System.currentTimeMillis() + 60000), privateKey); - - HttpServletRequest request = buildTokenExchangeRequestSubjectOnly(subjectJwt.serialize()); - EasyMock.replay(request); - HttpServletResponse response = EasyMock.createNiceMock(HttpServletResponse.class); - EasyMock.replay(response); - - TestFilterChain chain = new TestFilterChain(); - handler.doFilter(request, response, chain); - - Assert.assertTrue("doFilterCalled should be true", chain.doFilterCalled); - Set<PrimaryPrincipal> principals = chain.subject.getPrincipals(PrimaryPrincipal.class); - Assert.assertEquals("Expected exactly one PrimaryPrincipal", 1, principals.size()); - Assert.assertEquals("Subject should be its own PrimaryPrincipal", "k8s-sa", - ((java.security.Principal) principals.toArray()[0]).getName()); - Assert.assertNull("No TokenExchangePrincipal expected for subject-only exchange", - SubjectUtils.getTokenExchangePrincipal(chain.subject)); - Assert.assertTrue("ImpersonatedPrincipal set should be empty", - chain.subject.getPrincipals(ImpersonatedPrincipal.class).isEmpty()); - } - - /** - * Builds a token-exchange request mock with both subject_token and actor_token parameters. - * The caller must call {@code EasyMock.replay(request)} before using the returned mock. - * - * @param subjectToken serialized subject JWT - * @param actorToken serialized actor JWT - * @return a NiceMock HttpServletRequest configured for a token-exchange grant - */ - private HttpServletRequest buildTokenExchangeRequest(String subjectToken, String actorToken) { - HttpServletRequest request = EasyMock.createNiceMock(HttpServletRequest.class); - EasyMock.expect(request.getRequestURL()) - .andReturn(new StringBuffer(SERVICE_URL)).anyTimes(); - EasyMock.expect(request.getParameter(CommonTokenConstants.GRANT_TYPE)) - .andReturn(JWTFederationFilter.TOKEN_EXCHANGE).anyTimes(); - EasyMock.expect(request.getParameter(JWTFederationFilter.SUBJECT_TOKEN)) - .andReturn(subjectToken).anyTimes(); - EasyMock.expect(request.getParameter(JWTFederationFilter.ACTOR_TOKEN)) - .andReturn(actorToken).anyTimes(); - return request; - } - - /** - * Builds a token-exchange request mock with subject_token only. The actor_token parameter - * is not mocked, so the NiceMock returns null for {@code getParameter(ACTOR_TOKEN)}. - * The caller must call {@code EasyMock.replay(request)} before using the returned mock. - * - * @param subjectToken serialized subject JWT - * @return a NiceMock HttpServletRequest configured for a subject-only token-exchange grant - */ - private HttpServletRequest buildTokenExchangeRequestSubjectOnly(String subjectToken) { - HttpServletRequest request = EasyMock.createNiceMock(HttpServletRequest.class); - EasyMock.expect(request.getRequestURL()) - .andReturn(new StringBuffer(SERVICE_URL)).anyTimes(); - EasyMock.expect(request.getParameter(CommonTokenConstants.GRANT_TYPE)) - .andReturn(JWTFederationFilter.TOKEN_EXCHANGE).anyTimes(); - EasyMock.expect(request.getParameter(JWTFederationFilter.SUBJECT_TOKEN)) - .andReturn(subjectToken).anyTimes(); - // ACTOR_TOKEN not mocked — NiceMock returns null for getParameter(ACTOR_TOKEN) - return request; - } -} diff --git a/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/jwt/filter/TokenExchangeHandlerTest.java b/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/jwt/filter/TokenExchangeHandlerTest.java index 9b7608828..49561ad66 100644 --- a/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/jwt/filter/TokenExchangeHandlerTest.java +++ b/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/jwt/filter/TokenExchangeHandlerTest.java @@ -21,9 +21,11 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; +import org.apache.knox.gateway.security.ActorChainPrincipal; import org.apache.knox.gateway.security.PrimaryPrincipal; import org.apache.knox.gateway.security.TokenExchangePrincipal; import org.apache.knox.gateway.services.security.token.impl.JWT; +import org.apache.knox.gateway.services.security.token.impl.JWTToken; import org.easymock.EasyMock; import org.junit.Before; import org.junit.Test; @@ -38,6 +40,7 @@ import java.io.IOException; import java.text.ParseException; import java.util.HashMap; import java.util.Map; +import java.util.Set; /** * Unit tests for {@link TokenExchangeHandler} covering the RFC 8693 request-validation and @@ -152,6 +155,22 @@ public class TokenExchangeHandlerTest { assertEquals("svc-dataservice", tep.getActorPrincipalName()); } + @Test + public void testSubjectTokenWithActClaimCreatesActorChainPrincipal() throws Exception { + // subject_token already carries a prior delegation chain (an 'act' claim) ... + filter.valid.put("subtok", jwtWithActClaim("alice", "KNOXSSO", Map.of("sub", "prior-actor"))); + filter.valid.put("acttok", jwt("svc-dataservice", "https://k8s")); + handler.handle(request("subtok", JWT_TYPE, "acttok", JWT_TYPE), response, chain); + + assertTrue(filter.continued); + assertNotNull(filter.establishedSubject); + // ... which is preserved as an ActorChainPrincipal in the exchanged Subject + final Set<ActorChainPrincipal> actorChainPrincipals = + filter.establishedSubject.getPrincipals(ActorChainPrincipal.class); + assertFalse("ActorChainPrincipal should be present", actorChainPrincipals.isEmpty()); + assertEquals("prior-actor", actorChainPrincipals.iterator().next().getCurrentActor()); + } + @Test public void testSubjectValidationFailureDoesNotEstablishContext() throws Exception { // "subtok" is not in the valid map -> parseAndValidateJWT returns null (error already sent) @@ -192,6 +211,16 @@ public class TokenExchangeHandlerTest { return jwt; } + private static JWT jwtWithActClaim(String subject, String issuer, Map<String, Object> actClaim) { + final JWT jwt = EasyMock.createNiceMock(JWT.class); + EasyMock.expect(jwt.getSubject()).andReturn(subject).anyTimes(); + EasyMock.expect(jwt.getIssuer()).andReturn(issuer).anyTimes(); + // subject_token carries a prior delegation chain via its 'act' claim + EasyMock.expect(jwt.getClaimAsObject(JWTToken.ACT_CLAIM)).andReturn(actClaim).anyTimes(); + EasyMock.replay(jwt); + return jwt; + } + /** * A {@link JWTFederationFilter} whose validation and context-establishment callbacks are * replaced with recording stubs, so the handler's own logic can be exercised in isolation.
