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 2024/03/12 22:13:21 UTC

(knox) branch master updated: KNOX-3016 - add support for client credentials flow (#876)

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 <lm...@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.
+    }
+}