From 9e9c8f4572ccc094df6b4246698c2f6cec50e0b7 Mon Sep 17 00:00:00 2001 From: Lawrence McCay Date: Fri, 8 Mar 2024 15:21:42 -0500 Subject: [PATCH 1/4] KNOX-3016 - add support for client credentials flow --- .../jwt/filter/AbstractJWTFilter.java | 19 ++++++- .../jwt/filter/JWTFederationFilter.java | 32 ++++++++++-- ...IdAndClientSecretFederationFilterTest.java | 51 +++++++++++++++++++ 3 files changed, 98 insertions(+), 4 deletions(-) create mode 100644 gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/ClientIdAndClientSecretFederationFilterTest.java diff --git a/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/AbstractJWTFilter.java b/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/AbstractJWTFilter.java index 07ce390306..cb65e72017 100644 --- a/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/AbstractJWTFilter.java +++ b/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/AbstractJWTFilter.java @@ -96,6 +96,8 @@ public abstract class AbstractJWTFilter implements Filter { */ public static final String JWT_EXPECTED_SIGALG = "jwt.expected.sigalg"; public static final String JWT_DEFAULT_SIGALG = "RS256"; + public static final String TYPE = "type"; + public static final String CLIENT_ID = "CLIENT_ID"; static JWTMessages log = MessagesFactory.get( JWTMessages.class ); @@ -300,8 +302,23 @@ protected Subject createSubjectFromToken(final JWT token) throws UnknownTokenExc public Subject createSubjectFromTokenIdentifier(final String tokenId) throws UnknownTokenException { TokenMetadata metadata = tokenStateService.getTokenMetadata(tokenId); + String username = null; if (metadata != null) { - return createSubjectFromTokenData(metadata.getUserName(), null); + String type = metadata.getMetadata(TYPE); + // using tokenID and passcode as CLIENT_ID and CLIENT_SECRET will + // result in a metadata item called "type". If the valid is set + // to CLIENT_ID then it will be assumed to be a CLIENT_ID and we + // will use the token id as the username. Since we don't know the + // token id until it is created, the username is always the same + // in the record. Using the token id makes it a unique username for + // audit and the like. + if (CLIENT_ID.equalsIgnoreCase(type)) { + username = tokenId; + } + else { + username = metadata.getUserName(); + } + return createSubjectFromTokenData(username, null); } return null; } 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 64ac556d03..b5bec58975 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 @@ -58,6 +58,9 @@ public class JWTFederationFilter extends AbstractJWTFilter { private static final JWTMessages LOGGER = MessagesFactory.get( JWTMessages.class ); /* A semicolon separated list of paths that need to bypass authentication */ public static final String JWT_UNAUTHENTICATED_PATHS_PARAM = "jwt.unauthenticated.path.list"; + public static final String GRANT_TYPE = "grant_type"; + public static final String CLIENT_CREDENTIALS = "client_credentials"; + public static final String CLIENT_SECRET = "client_secret"; public enum TokenType { JWT, Passcode; @@ -238,10 +241,33 @@ public Pair getWireToken(final ServletRequest request) { } } + /* + POST /{tenant}/oauth2/v2.0/token HTTP/1.1 + Host: login.microsoftonline.com:443 + Content-Type: application/x-www-form-urlencoded + + client_id=535fb089-9ff3-47b6-9bfb-4f1264799865 + &scope=https%3A%2F%2Fgraph.microsoft.com%2F.default + &client_secret=sampleCredentials + &grant_type=client_credentials + */ + + // Let's check whether this is a client credentials oauth request or whether + // the token has been configured for another usecase specific header if (parsed == null) { - token = request.getParameter(this.paramName); - if (token != null) { - parsed = Pair.of(TokenType.JWT, token); + 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 + token = request.getParameter(CLIENT_SECRET); + parsed = Pair.of(TokenType.Passcode, token); + } + else { + token = request.getParameter(this.paramName); + if (token != null) { + parsed = Pair.of(TokenType.JWT, token); + } } } 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 new file mode 100644 index 0000000000..df3691f62e --- /dev/null +++ b/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/ClientIdAndClientSecretFederationFilterTest.java @@ -0,0 +1,51 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 org.easymock.EasyMock; + +import javax.servlet.http.HttpServletRequest; + + +public class ClientIdAndClientSecretFederationFilterTest extends TokenIDAsHTTPBasicCredsFederationFilterTest { + @Override + protected void setTokenOnRequest(HttpServletRequest request, String authUsername, String authPassword) { + EasyMock.expect((Object)request.getHeader("Authorization")).andReturn(""); + EasyMock.expect((Object)request.getParameter("grant_type")).andReturn("client_credentials"); + EasyMock.expect((Object)request.getParameter("client_id")).andReturn(authUsername); + EasyMock.expect((Object)request.getParameter("client_secret")).andReturn(authPassword); + } + + @Override + public void testInvalidUsername() throws Exception { + // there is no way to specify an invalid username for + // client credentials flow or at least no meaningful way + // to do so for our implementation. The client id is + // actually encoded in the client secret and that is used + // for the actual authentication with passcodes. + } + + @Override + public void testInvalidJWTForPasscode() throws Exception { + // there is no way to specify an invalid username for + // client credentials flow or at least no meaningful way + // to do so for our implementation. The username is actually + // set by the JWTProvider when determining that the request + // is a client credentials flow. + } +} From 0515e1cb2331b734d18574ccb26bbb90d96f7f08 Mon Sep 17 00:00:00 2001 From: Lawrence McCay Date: Fri, 8 Mar 2024 17:44:54 -0500 Subject: [PATCH 2/4] KNOX-3016 Address PMD Failures --- .../ClientIdAndClientSecretFederationFilterTest.java | 3 +++ 1 file changed, 3 insertions(+) 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 df3691f62e..806125ad16 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 @@ -18,6 +18,7 @@ import org.easymock.EasyMock; +import org.junit.Test; import javax.servlet.http.HttpServletRequest; @@ -32,6 +33,7 @@ protected void setTokenOnRequest(HttpServletRequest request, String authUsername } @Override + @Test public void testInvalidUsername() throws Exception { // there is no way to specify an invalid username for // client credentials flow or at least no meaningful way @@ -41,6 +43,7 @@ public void testInvalidUsername() throws Exception { } @Override + @Test public void testInvalidJWTForPasscode() throws Exception { // there is no way to specify an invalid username for // client credentials flow or at least no meaningful way From 1fac6519783d12291a125934cc28a596d8e62e42 Mon Sep 17 00:00:00 2001 From: Lawrence McCay Date: Mon, 11 Mar 2024 15:57:26 -0400 Subject: [PATCH 3/4] KNOX-3016 - address review comments --- .../jwt/filter/AbstractJWTFilter.java | 2 +- .../jwt/filter/JWTFederationFilter.java | 57 ++++++++++--------- 2 files changed, 32 insertions(+), 27 deletions(-) diff --git a/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/AbstractJWTFilter.java b/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/AbstractJWTFilter.java index cb65e72017..90fd117b9e 100644 --- a/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/AbstractJWTFilter.java +++ b/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/AbstractJWTFilter.java @@ -306,7 +306,7 @@ public Subject createSubjectFromTokenIdentifier(final String tokenId) throws Unk if (metadata != null) { String type = metadata.getMetadata(TYPE); // using tokenID and passcode as CLIENT_ID and CLIENT_SECRET will - // result in a metadata item called "type". If the valid is set + // result in a metadata item called "type". If the value is set // to CLIENT_ID then it will be assumed to be a CLIENT_ID and we // will use the token id as the username. Since we don't know the // token id until it is created, the username is always the same 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 b5bec58975..a9e785c7e1 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 @@ -241,39 +241,44 @@ public Pair getWireToken(final ServletRequest request) { } } - /* - POST /{tenant}/oauth2/v2.0/token HTTP/1.1 - Host: login.microsoftonline.com:443 - Content-Type: application/x-www-form-urlencoded - - client_id=535fb089-9ff3-47b6-9bfb-4f1264799865 - &scope=https%3A%2F%2Fgraph.microsoft.com%2F.default - &client_secret=sampleCredentials - &grant_type=client_credentials - */ + parsed = parseFromClientCredentialsFlow(request); - // Let's check whether this is a client credentials oauth request or whether - // the token has been configured for another usecase specific header if (parsed == null) { - 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 - token = request.getParameter(CLIENT_SECRET); - parsed = Pair.of(TokenType.Passcode, token); - } - else { - token = request.getParameter(this.paramName); - if (token != null) { - parsed = Pair.of(TokenType.JWT, token); - } - } + token = request.getParameter(this.paramName); + if (token != null) { + parsed = Pair.of(TokenType.JWT, token); + } } return parsed; } + private Pair parseFromClientCredentialsFlow(ServletRequest request) { + Pair parsed = null; + String token = null; + + /* + POST /{tenant}/oauth2/v2.0/token HTTP/1.1 + Host: login.microsoftonline.com:443 + Content-Type: application/x-www-form-urlencoded + + client_id=535fb089-9ff3-47b6-9bfb-4f1264799865 + &scope=https%3A%2F%2Fgraph.microsoft.com%2F.default + &client_secret=sampleCredentials + &grant_type=client_credentials + */ + + 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 + token = request.getParameter(CLIENT_SECRET); + parsed = Pair.of(TokenType.Passcode, token); + } + return parsed; + } + private Pair parseFromHTTPBasicCredentials(final String header) { Pair parsed = null; final String base64Credentials = header.substring(BASIC.length()).trim(); From b1e2439672f92ebb6ea97dccab1b445f748eae9b Mon Sep 17 00:00:00 2001 From: Lawrence McCay Date: Mon, 11 Mar 2024 18:44:44 -0400 Subject: [PATCH 4/4] KNOX-3016 - Fix broken refactor --- .../provider/federation/jwt/filter/JWTFederationFilter.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 a9e785c7e1..9c20010579 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 @@ -241,7 +241,9 @@ public Pair getWireToken(final ServletRequest request) { } } - parsed = parseFromClientCredentialsFlow(request); + if (parsed == null) { + parsed = parseFromClientCredentialsFlow(request); + } if (parsed == null) { token = request.getParameter(this.paramName);