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 8f38723bb KNOX-3016 - add support for client credentials flow (#876) 8f38723bb is described below commit 8f38723bb6b8111eb93b01697e89dd98fb6f59f2 Author: lmccay <lmc...@apache.org> AuthorDate: Tue Mar 12 18:12:59 2024 -0400 KNOX-3016 - add support for client credentials flow (#876) * KNOX-3016 - add support for client credentials flow --- .../federation/jwt/filter/AbstractJWTFilter.java | 19 +++++++- .../federation/jwt/filter/JWTFederationFilter.java | 41 ++++++++++++++-- ...lientIdAndClientSecretFederationFilterTest.java | 54 ++++++++++++++++++++++ 3 files changed, 109 insertions(+), 5 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 07ce39030..90fd117b9 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 @@ public abstract class AbstractJWTFilter implements Filter { 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 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 + // 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 64ac556d0..9c2001057 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; @@ -239,15 +242,45 @@ public class JWTFederationFilter extends AbstractJWTFilter { } if (parsed == null) { - token = request.getParameter(this.paramName); - if (token != null) { - parsed = Pair.of(TokenType.JWT, token); - } + parsed = parseFromClientCredentialsFlow(request); + } + + if (parsed == null) { + token = request.getParameter(this.paramName); + if (token != null) { + parsed = Pair.of(TokenType.JWT, token); + } } return parsed; } + private Pair<TokenType, String> parseFromClientCredentialsFlow(ServletRequest request) { + Pair<TokenType, String> 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<TokenType, String> parseFromHTTPBasicCredentials(final String header) { Pair<TokenType, String> parsed = null; final String base64Credentials = header.substring(BASIC.length()).trim(); 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 000000000..806125ad1 --- /dev/null +++ b/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/ClientIdAndClientSecretFederationFilterTest.java @@ -0,0 +1,54 @@ +/* + * 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 org.junit.Test; + +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 + @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 + // 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 + @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 + // to do so for our implementation. The username is actually + // set by the JWTProvider when determining that the request + // is a client credentials flow. + } +}