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);
}
}