This is an automated email from the ASF dual-hosted git repository. smolnar 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 a58344816 KNOX-3175 - Client credential flow attributes are read without reading the entire request body (#1070) a58344816 is described below commit a58344816ab2e08fae9025318d7aee0d81f45159 Author: Sandor Molnar <smol...@apache.org> AuthorDate: Fri Aug 1 09:45:09 2025 +0200 KNOX-3175 - Client credential flow attributes are read without reading the entire request body (#1070) --- .../federation/jwt/filter/JWTFederationFilter.java | 115 ++++++++------------- ...lientIdAndClientSecretFederationFilterTest.java | 45 ++++---- 2 files changed, 64 insertions(+), 96 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 b7cc89131..f13b552a4 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 @@ -17,16 +17,31 @@ */ package org.apache.knox.gateway.provider.federation.jwt.filter; -import static java.nio.charset.StandardCharsets.UTF_8; -import static org.apache.knox.gateway.util.AuthFilterUtils.DEFAULT_AUTH_UNAUTHENTICATED_PATHS_PARAM; +import com.nimbusds.jose.JOSEObjectType; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.Pair; +import org.apache.knox.gateway.i18n.messages.MessagesFactory; +import org.apache.knox.gateway.provider.federation.jwt.JWTMessages; +import org.apache.knox.gateway.security.PrimaryPrincipal; +import org.apache.knox.gateway.services.security.token.UnknownTokenException; +import org.apache.knox.gateway.services.security.token.impl.JWT; +import org.apache.knox.gateway.services.security.token.impl.JWTToken; +import org.apache.knox.gateway.util.AuthFilterUtils; +import org.apache.knox.gateway.util.CertificateUtils; +import org.apache.knox.gateway.util.CookieUtils; -import java.io.BufferedReader; +import javax.security.auth.Subject; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; import java.io.IOException; -import java.io.InputStreamReader; import java.net.URI; import java.net.URISyntaxException; -import java.net.URLDecoder; -import java.nio.charset.StandardCharsets; import java.text.ParseException; import java.util.Arrays; import java.util.Base64; @@ -37,30 +52,8 @@ import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; -import javax.security.auth.Subject; -import javax.servlet.FilterChain; -import javax.servlet.FilterConfig; -import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; -import javax.servlet.http.Cookie; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.tuple.Pair; -import org.apache.knox.gateway.i18n.messages.MessagesFactory; -import org.apache.knox.gateway.provider.federation.jwt.JWTMessages; -import org.apache.knox.gateway.security.PrimaryPrincipal; -import org.apache.knox.gateway.services.security.token.UnknownTokenException; -import org.apache.knox.gateway.services.security.token.impl.JWT; -import org.apache.knox.gateway.services.security.token.impl.JWTToken; -import org.apache.knox.gateway.util.AuthFilterUtils; -import org.apache.knox.gateway.util.CertificateUtils; -import org.apache.knox.gateway.util.CookieUtils; -import org.apache.knox.gateway.util.RequestBodyUtils; - -import com.nimbusds.jose.JOSEObjectType; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.apache.knox.gateway.util.AuthFilterUtils.DEFAULT_AUTH_UNAUTHENTICATED_PATHS_PARAM; public class JWTFederationFilter extends AbstractJWTFilter { @@ -274,19 +267,16 @@ public class JWTFederationFilter extends AbstractJWTFilter { private boolean validateClientCredentialsFlow(HttpServletRequest request, HttpServletResponse response, String tokenId) throws IOException { boolean validated = true; - final String requestBodyString = getRequestBodyString(request); - if (requestBodyString != null && !requestBodyString.isEmpty()) { - final String grantType = RequestBodyUtils.getRequestBodyParameter(requestBodyString, GRANT_TYPE); - if (grantType != null && !grantType.isEmpty()) { - final String clientID = RequestBodyUtils.getRequestBodyParameter(requestBodyString, CLIENT_ID); - // if there is no client_id then this is not a client credentials flow - if (clientID != null && !tokenId.equals(clientID)) { - validated = false; - log.wrongPasscodeToken(tokenId); - handleValidationError((HttpServletRequest) request, (HttpServletResponse) response, - HttpServletResponse.SC_UNAUTHORIZED, - MISMATCHING_CLIENT_ID_AND_CLIENT_SECRET); - } + final String grantType = request.getParameter(GRANT_TYPE); + if (grantType != null && !grantType.isEmpty()) { + final String clientID = request.getParameter(CLIENT_ID); + // if there is no client_id then this is not a client credentials flow + if (clientID != null && !tokenId.equals(clientID)) { + validated = false; + log.wrongPasscodeToken(tokenId); + handleValidationError(request, response, + HttpServletResponse.SC_UNAUTHORIZED, + MISMATCHING_CLIENT_ID_AND_CLIENT_SECRET); } } return validated; @@ -346,39 +336,22 @@ public class JWTFederationFilter extends AbstractJWTFilter { &grant_type=client_credentials */ - if (request.getParameter(CLIENT_SECRET) != null) { - throw new SecurityException(); + final HttpServletRequest httpRequest = (HttpServletRequest) request; + final boolean clientSecretPresentAsQueryString = httpRequest.getQueryString() != null && httpRequest.getQueryString().contains("client_secret="); + if (clientSecretPresentAsQueryString) { + throw new SecurityException("client_secret must not be sent as a query parameter"); } return getClientCredentialsFromRequestBody(request); } private Pair<TokenType, String> getClientCredentialsFromRequestBody(ServletRequest request) throws IOException { - try { - final String requestBodyString = getRequestBodyString(request); - final String grantType = RequestBodyUtils.getRequestBodyParameter(requestBodyString, GRANT_TYPE); - if (CLIENT_CREDENTIALS.equals(grantType)) { - // this is indeed a client credentials flow client_id and - // client_secret are expected now the client_id will be in - // the token as the token_id so we will get that later - final String clientSecret = RequestBodyUtils.getRequestBodyParameter(requestBodyString, CLIENT_SECRET); - return Pair.of(TokenType.Passcode, clientSecret); - } - } catch (IOException e) { - log.errorFetchingClientSecret(e.getMessage(), e); - throw e; - } - return null; - } - - private String getRequestBodyString(ServletRequest request) throws IOException { - if (request.getInputStream() != null) { - final BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(request.getInputStream(), StandardCharsets.UTF_8)); - final StringBuilder requestBodyBuilder = new StringBuilder(); - String line; - while ((line = bufferedReader.readLine()) != null) { - requestBodyBuilder.append(line); - } - return URLDecoder.decode(requestBodyBuilder.toString(), StandardCharsets.UTF_8.name()); + final String grantType = request.getParameter(GRANT_TYPE); + if (CLIENT_CREDENTIALS.equals(grantType)) { + // this is indeed a client credentials flow client_id and + // client_secret are expected now the client_id will be in + // the token as the token_id so we will get that later + final String clientSecret = request.getParameter( CLIENT_SECRET); + return Pair.of(TokenType.Passcode, clientSecret); } return null; } diff --git a/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/ClientIdAndClientSecretFederationFilterTest.java b/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/ClientIdAndClientSecretFederationFilterTest.java index 1502684b6..004989c08 100644 --- a/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/ClientIdAndClientSecretFederationFilterTest.java +++ b/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/ClientIdAndClientSecretFederationFilterTest.java @@ -17,25 +17,19 @@ package org.apache.knox.gateway.provider.federation; -import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.tuple.Pair; import org.apache.knox.gateway.provider.federation.jwt.filter.JWTFederationFilter; import org.apache.knox.gateway.provider.federation.jwt.filter.JWTFederationFilter.TokenType; import org.apache.knox.gateway.provider.federation.jwt.filter.SignatureVerificationCache; import org.apache.knox.gateway.services.security.token.TokenMetadata; import org.apache.knox.gateway.services.security.token.TokenStateService; -import org.apache.knox.test.mock.MockServletInputStream; import org.easymock.EasyMock; import org.junit.Assert; import org.junit.Test; import javax.servlet.FilterConfig; -import javax.servlet.ServletInputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; import java.util.Properties; import static org.junit.Assert.assertEquals; @@ -46,29 +40,29 @@ public class ClientIdAndClientSecretFederationFilterTest extends TokenIDAsHTTPBa @Override protected void setTokenOnRequest(HttpServletRequest request, String authUsername, String authPassword) { EasyMock.expect((Object)request.getHeader("Authorization")).andReturn(""); - try { - EasyMock.expect(request.getInputStream()).andAnswer(() -> produceServletInputStream(authPassword)).atLeastOnce(); - } catch (IOException e) { - throw new RuntimeException("Error while setting up expectation for getting client credentials from request body", e); - } + EasyMock.expect((Object)request.getContentType()).andReturn("application/x-www-form-urlencoded").anyTimes(); + ensureClientCredentials(request, authPassword, false); } - private ServletInputStream produceServletInputStream(String clientSecret) { - final String requestBody = JWTFederationFilter.GRANT_TYPE + "=" + JWTFederationFilter.CLIENT_CREDENTIALS + "&" + JWTFederationFilter.CLIENT_SECRET + "=" - + clientSecret; - final InputStream inputStream = IOUtils.toInputStream(requestBody, StandardCharsets.UTF_8); - return new MockServletInputStream(inputStream); + private void ensureClientCredentials(final HttpServletRequest request, final String clientSecret) { + ensureClientCredentials(request, clientSecret, true); + } + + private void ensureClientCredentials(final HttpServletRequest request, final String clientSecret, final boolean excludeQueryString) { + EasyMock.expect(request.getParameter(JWTFederationFilter.GRANT_TYPE)).andReturn(JWTFederationFilter.CLIENT_CREDENTIALS).anyTimes(); + EasyMock.expect(request.getParameter(JWTFederationFilter.CLIENT_SECRET)).andReturn(clientSecret).anyTimes(); + if (excludeQueryString) { + EasyMock.expect(request.getQueryString()).andReturn(null).anyTimes(); + } } @Test public void testGetWireTokenUsingClientCredentialsFlow() throws Exception { - final String clientID = "client_id=clientID"; final String clientSecret = "sup3r5ecreT!"; final HttpServletRequest request = EasyMock.createNiceMock(HttpServletRequest.class); - EasyMock.expect(request.getInputStream()).andAnswer(() -> produceServletInputStream(clientSecret + "&" + - clientID)).atLeastOnce(); + ensureClientCredentials(request, clientSecret); + EasyMock.expect(request.getHeader("Authorization")).andReturn(null).anyTimes(); EasyMock.replay(request); - handler.init(new TestFilterConfig(getProperties())); final Pair<TokenType, String> wireToken = ((TestJWTFederationFilter) handler).getWireToken(request); @@ -105,8 +99,8 @@ public class ClientIdAndClientSecretFederationFilterTest extends TokenIDAsHTTPBa // LJM TODO: this will be needed later for client credentials as Basic auth header //EasyMock.expect(request.getHeader("Authorization")).andReturn(authTokenType + passcodeToken); - EasyMock.expect(request.getInputStream()).andAnswer(() -> produceServletInputStream(passcodeToken + - "&client_id=" + tokenId)).atLeastOnce(); + ensureClientCredentials(request, passcodeToken); + EasyMock.expect(request.getParameter("client_id")).andReturn(tokenId).anyTimes(); final HttpServletResponse response = EasyMock.createNiceMock(HttpServletResponse.class); // response.setStatus(HttpServletResponse.SC_OK); @@ -149,8 +143,8 @@ public class ClientIdAndClientSecretFederationFilterTest extends TokenIDAsHTTPBa // LJM TODO: this will be needed later for client credentials as Basic auth header //EasyMock.expect(request.getHeader("Authorization")).andReturn(authTokenType + passcodeToken); - EasyMock.expect(request.getInputStream()).andAnswer(() -> produceServletInputStream(passcodeToken + - "&client_id=" + tokenId + "invalidating_string")).atLeastOnce(); + ensureClientCredentials(request, passcodeToken); + EasyMock.expect(request.getParameter("client_id")).andReturn(tokenId + "invalidating_string").anyTimes(); final HttpServletResponse response = EasyMock.createNiceMock(HttpServletResponse.class); response.sendError(HttpServletResponse.SC_UNAUTHORIZED, @@ -171,7 +165,8 @@ public class ClientIdAndClientSecretFederationFilterTest extends TokenIDAsHTTPBa @Test(expected = SecurityException.class) public void shouldFailIfClientSecretIsPassedInQueryParams() throws Exception { final HttpServletRequest request = EasyMock.createNiceMock(HttpServletRequest.class); - EasyMock.expect((Object)request.getParameter("client_secret")).andReturn("sup3r5ecreT!"); + final String queryString = "test=test&client_secret=sup3r5ecreT&otherTest=otherTestQueryParam"; + EasyMock.expect(request.getQueryString()).andReturn(queryString).anyTimes(); EasyMock.replay(request); handler.init(new TestFilterConfig(getProperties()));