You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@knox.apache.org by lm...@apache.org on 2026/03/11 20:38:04 UTC

(knox) branch master updated: KNOX-3273 - Short Lived Tokens as Client Credentials (#1171)

This is an automated email from the ASF dual-hosted git repository.

lmccay pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/knox.git


The following commit(s) were added to refs/heads/master by this push:
     new 996bf1401 KNOX-3273 - Short Lived Tokens as Client Credentials (#1171)
996bf1401 is described below

commit 996bf140154fede5c80a6f257b4521cb181317b9
Author: lmccay <lm...@apache.org>
AuthorDate: Wed Mar 11 16:37:58 2026 -0400

    KNOX-3273 - Short Lived Tokens as Client Credentials (#1171)
---
 .../federation/jwt/filter/JWTFederationFilter.java | 24 +++++++-
 .../federation/OAuthFlowsFederationFilterTest.java | 67 +++++++++++++++++++++-
 2 files changed, 86 insertions(+), 5 deletions(-)

diff --git a/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/JWTFederationFilter.java b/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/JWTFederationFilter.java
index f89f65566..88c5ad715 100644
--- a/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/JWTFederationFilter.java
+++ b/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/JWTFederationFilter.java
@@ -64,6 +64,9 @@ public class JWTFederationFilter extends AbstractJWTFilter {
   public static final String REFRESH_TOKEN_PARAM = "refresh_token";
   public static final String TOKEN_EXCHANGE = "token_exchange";
   public static final String SUBJECT_TOKEN = "subject_token";
+  public static final String CLIENT_ASSERTION_JWT_BEARER = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer";
+  public static final String CLIENT_ASSERTION_TYPE = "client_assertion_type";
+  public static final String CLIENT_ASSERTION = "client_assertion";
 
   public enum TokenType {
     JWT, Passcode;
@@ -291,6 +294,16 @@ public class JWTFederationFilter extends AbstractJWTFilter {
         &scope=https%3A%2F%2Fgraph.microsoft.com%2F.default
         &client_secret=sampleCredentials
         &grant_type=client_credentials
+
+        or
+
+        POST /token.oauth2 HTTP/1.1
+        Content-Type: application/x-www-form-urlencoded
+
+        grant_type=client_credentials&
+        client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer&
+        client_assertion=eyJhbGciOiJSUzI1NiJ9... <- K8s SA JWT
+        scope=openid profile email
        */
 
       final HttpServletRequest httpRequest = (HttpServletRequest) request;
@@ -303,7 +316,12 @@ public class JWTFederationFilter extends AbstractJWTFilter {
 
     private Pair<TokenType, String> getTokenFromRequestBody(ServletRequest request) {
         final String grantType = request.getParameter(GRANT_TYPE);
+        final String clientAssertionType = request.getParameter(CLIENT_ASSERTION_TYPE);
         if (CLIENT_CREDENTIALS.equals(grantType)) {
+          if (clientAssertionType != null && CLIENT_ASSERTION_JWT_BEARER.equals(clientAssertionType)) {
+            // short lived client assertion token expected
+            return getClientTokenFromParams(request, CLIENT_ASSERTION);
+          }
           // client credentials flow: client_id and client_secret are expected
           // the client_id will be in the token as the token_id
           final String clientSecret = request.getParameter(CLIENT_SECRET);
@@ -311,15 +329,15 @@ public class JWTFederationFilter extends AbstractJWTFilter {
           return Pair.of(TokenType.Passcode, clientSecret);
         } else if (REFRESH_TOKEN.equals(grantType)) {
           // refresh_token flow: the refresh_token parameter contains the actual token
-          return getRefreshOrSubjectToken(request, REFRESH_TOKEN_PARAM);
+          return getClientTokenFromParams(request, REFRESH_TOKEN_PARAM);
         } else if (TOKEN_EXCHANGE.equals(grantType)) {
           // token_exchange flow: the subject_token parameter contains the token to be exchanged
-          return getRefreshOrSubjectToken(request, SUBJECT_TOKEN);
+          return getClientTokenFromParams(request, SUBJECT_TOKEN);
         }
       return null;
     }
 
-  private Pair<TokenType, String> getRefreshOrSubjectToken(final ServletRequest request, final String requestParamName) {
+  private Pair<TokenType, String> getClientTokenFromParams(final ServletRequest request, final String requestParamName) {
     final String refreshOrSubjectToken = request.getParameter(requestParamName);
     if (refreshOrSubjectToken != null) {
       return isJWT(refreshOrSubjectToken) ? Pair.of(TokenType.JWT, refreshOrSubjectToken) : Pair.of(TokenType.Passcode, refreshOrSubjectToken);
diff --git a/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/OAuthFlowsFederationFilterTest.java b/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/OAuthFlowsFederationFilterTest.java
index 1df3b7543..78816a0c5 100644
--- a/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/OAuthFlowsFederationFilterTest.java
+++ b/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/OAuthFlowsFederationFilterTest.java
@@ -35,10 +35,12 @@ import javax.servlet.http.HttpServletResponse;
 import java.nio.charset.StandardCharsets;
 import java.text.ParseException;
 import java.util.Base64;
+import java.util.Date;
 import java.util.Properties;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
 
 
 public class OAuthFlowsFederationFilterTest extends TokenIDAsHTTPBasicCredsFederationFilterTest {
@@ -68,6 +70,36 @@ public class OAuthFlowsFederationFilterTest extends TokenIDAsHTTPBasicCredsFeder
         }
     }
 
+    private void ensureJWTClientCredentials(final HttpServletRequest request, final boolean excludeQueryString) {
+        /*
+        {
+        "alg": "RS256",
+        "kid": "vRZD8cQzbRThdVFzIZljIEgrMIzRa8WU1JDP5gSoQ90"
+        }
+        {
+         "iss": "kubernetes/serviceaccount",
+         "kubernetes.io/serviceaccount/namespace": "poc",
+         "kubernetes.io/serviceaccount/secret.name": "webhdfs-sa-token",
+         "kubernetes.io/serviceaccount/service-account.name": "webhdfs-sa",
+         "kubernetes.io/serviceaccount/service-account.uid": "5cefd6ad-a213-4fdc-bccf-3cdbfd2cabb2",
+         "sub": "system:serviceaccount:poc:webhdfs-sa"
+        }
+        */
+        SignedJWT token = null;
+        try {
+            token = super.getJWT("kubernetes/serviceaccount", "system:serviceaccount:poc:webhdfs-sa",
+                    new Date(new Date().getTime() + 5000));
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+        EasyMock.expect(request.getParameter(JWTFederationFilter.GRANT_TYPE)).andReturn(JWTFederationFilter.CLIENT_CREDENTIALS).anyTimes();
+        EasyMock.expect(request.getParameter(JWTFederationFilter.CLIENT_ASSERTION_TYPE)).andReturn(JWTFederationFilter.CLIENT_ASSERTION_JWT_BEARER).anyTimes();
+        EasyMock.expect(request.getParameter(JWTFederationFilter.CLIENT_ASSERTION)).andReturn(token.serialize()).anyTimes();
+        if (excludeQueryString) {
+            EasyMock.expect(request.getQueryString()).andReturn(null).anyTimes();
+        }
+    }
+
     @Test
     public void testGetWireTokenUsingClientCredentialsFlowWithoutClientId() throws Exception {
         final String passcode = "WTJ4cFpXNTBMV2xrTFRFeU16UTE6OlkyeHBaVzUwTFhObFkzSmxkQzB4TWpNME5RPT0=";
@@ -110,6 +142,37 @@ public class OAuthFlowsFederationFilterTest extends TokenIDAsHTTPBasicCredsFeder
       assertEquals(passcode, wireToken.getRight());
     }
 
+    @Test
+    public void testGetWireJWTUsingClientCredentialsFlow() throws Exception {
+        /*
+        {
+        "alg": "RS256",
+        "kid": "vRZD8cQzbRThdVFzIZljIEgrMIzRa8WU1JDP5gSoQ90"
+        }
+        {
+         "iss": "kubernetes/serviceaccount",
+         "kubernetes.io/serviceaccount/namespace": "poc",
+         "kubernetes.io/serviceaccount/secret.name": "webhdfs-sa-token",
+         "kubernetes.io/serviceaccount/service-account.name": "webhdfs-sa",
+         "kubernetes.io/serviceaccount/service-account.uid": "5cefd6ad-a213-4fdc-bccf-3cdbfd2cabb2",
+         "sub": "system:serviceaccount:poc:webhdfs-sa"
+        }
+        */
+        final HttpServletRequest request = EasyMock.createNiceMock(HttpServletRequest.class);
+        ensureJWTClientCredentials(request, true);
+        EasyMock.replay(request);
+
+        handler.init(new TestFilterConfig(getProperties()));
+        final Pair<TokenType, String> wireToken = ((TestJWTFederationFilter) handler).getWireToken(request);
+
+        EasyMock.verify(request);
+
+        assertNotNull(wireToken);
+        assertEquals(TokenType.JWT, wireToken.getLeft());
+        assertNotNull(wireToken.getRight());
+        assertTrue(handler.isJWT(wireToken.getRight()));
+    }
+
     @Test
     public void testGetWireTokenUsingClientCredentialsBasicAuth() throws Exception {
         final String clientId = "client-id-12345";
@@ -152,7 +215,7 @@ public class OAuthFlowsFederationFilterTest extends TokenIDAsHTTPBasicCredsFeder
         handler.doFilter(request, response, chain);
 
         EasyMock.verify(response);
-        Assert.assertTrue(chain.doFilterCalled);
+        assertTrue(chain.doFilterCalled);
         Assert.assertNotNull(chain.subject);
     }
 
@@ -345,7 +408,7 @@ public class OAuthFlowsFederationFilterTest extends TokenIDAsHTTPBasicCredsFeder
         handler.doFilter(request, response, chain);
 
         EasyMock.verify(response);
-        Assert.assertTrue(chain.doFilterCalled);
+        assertTrue(chain.doFilterCalled);
         Assert.assertNotNull(chain.subject);
     }
 }