You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@knox.apache.org by sm...@apache.org on 2022/08/25 18:05:31 UTC

[knox] branch master updated: KNOX-2794 - Added cookie auth support into JWT federation provider (#623)

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 eaa77e33e KNOX-2794 - Added cookie auth support into JWT federation provider (#623)
eaa77e33e is described below

commit eaa77e33ef0c8c883cc220e18ded06ef6789e7d9
Author: Sandor Molnar <sm...@apache.org>
AuthorDate: Thu Aug 25 20:05:06 2022 +0200

    KNOX-2794 - Added cookie auth support into JWT federation provider (#623)
---
 .../hadoopauth/filter/HadoopAuthFilterTest.java    |  2 +
 .../provider/federation/jwt/JWTMessages.java       |  3 +
 .../federation/jwt/filter/JWTFederationFilter.java | 75 ++++++++++++++++++++++
 .../jwt/filter/SSOCookieFederationFilter.java      |  2 +-
 .../provider/federation/AbstractJWTFilterTest.java |  8 +++
 .../federation/JWTFederationFilterTest.java        | 73 ++++++++++++++++++++-
 6 files changed, 159 insertions(+), 4 deletions(-)

diff --git a/gateway-provider-security-hadoopauth/src/test/java/org/apache/knox/gateway/hadoopauth/filter/HadoopAuthFilterTest.java b/gateway-provider-security-hadoopauth/src/test/java/org/apache/knox/gateway/hadoopauth/filter/HadoopAuthFilterTest.java
index 25a504f3a..e466ea54e 100644
--- a/gateway-provider-security-hadoopauth/src/test/java/org/apache/knox/gateway/hadoopauth/filter/HadoopAuthFilterTest.java
+++ b/gateway-provider-security-hadoopauth/src/test/java/org/apache/knox/gateway/hadoopauth/filter/HadoopAuthFilterTest.java
@@ -566,6 +566,8 @@ public class HadoopAuthFilterTest {
       expect(filterConfig.getInitParameter(AbstractJWTFilter.JWT_EXPECTED_SIGALG)).andReturn(null).anyTimes();
       expect(filterConfig.getInitParameter(JWTFederationFilter.ALLOWED_JWS_TYPES)).andReturn(null).anyTimes();
       expect(filterConfig.getInitParameter(SignatureVerificationCache.TOKENS_VERIFIED_CACHE_MAX)).andReturn(null).anyTimes();
+      expect(filterConfig.getInitParameter(JWTFederationFilter.KNOX_TOKEN_USE_COOKIE)).andReturn(null).anyTimes();
+      expect(filterConfig.getInitParameter(JWTFederationFilter.KNOX_TOKEN_COOKIE_NAME)).andReturn(null).anyTimes();
     }
 
     final ServletContext servletContext = createMock(ServletContext.class);
diff --git a/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/JWTMessages.java b/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/JWTMessages.java
index 54f4bf721..bdded53f4 100644
--- a/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/JWTMessages.java
+++ b/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/JWTMessages.java
@@ -46,6 +46,9 @@ public interface JWTMessages {
   @Message( level = MessageLevel.WARN, text = "Expected Bearer token is missing." )
   void missingBearerToken();
 
+  @Message( level = MessageLevel.WARN, text = "Expected valid cookie is missing." )
+  void missingValidCookie();
+
   @Message( level = MessageLevel.INFO, text = "Unable to verify token: {0}" )
   void unableToVerifyToken(@StackTrace( level = MessageLevel.ERROR) Exception e);
 
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 60914d6e0..64ac556d0 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
@@ -24,6 +24,7 @@ import java.io.IOException;
 import java.text.ParseException;
 import java.util.Base64;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Locale;
 import java.util.Set;
 import java.util.stream.Stream;
@@ -34,9 +35,11 @@ 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;
@@ -46,6 +49,7 @@ 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 com.nimbusds.jose.JOSEObjectType;
 
@@ -69,6 +73,13 @@ public class JWTFederationFilter extends AbstractJWTFilter {
   public static final String BASIC    = "Basic";
   public static final String TOKEN    = "Token";
   public static final String PASSCODE = "Passcode";
+
+  //cookie verification support
+  public static final String KNOX_TOKEN_USE_COOKIE = "knox.token.use.cookie";
+  public static final String KNOX_TOKEN_COOKIE_NAME = "knox.token.cookie.name";
+  private boolean useCookie; //defaults to false
+  private String cookieName;
+
   private String paramName;
   private Set<String> unAuthenticatedPaths = new HashSet<>(20);
 
@@ -102,6 +113,13 @@ public class JWTFederationFilter extends AbstractJWTFilter {
       allowedJwsTypes.add(JOSEObjectType.JWT);
     }
 
+    //cookie auth support
+    final String useCookieParam = filterConfig.getInitParameter(KNOX_TOKEN_USE_COOKIE);
+    useCookie = StringUtils.isBlank(useCookieParam) ? false : Boolean.parseBoolean(useCookieParam);
+
+    final String cookieNameParam = filterConfig.getInitParameter(KNOX_TOKEN_COOKIE_NAME);
+    cookieName = StringUtils.isBlank(cookieNameParam) ? SSOCookieFederationFilter.DEFAULT_SSO_COOKIE_NAME : cookieNameParam;
+
     // expected claim
     String oidcPrincipalclaim = filterConfig.getInitParameter(TOKEN_PRINCIPAL_CLAIM);
     if (oidcPrincipalclaim != null) {
@@ -136,6 +154,22 @@ public class JWTFederationFilter extends AbstractJWTFilter {
       continueWithAnonymousSubject(request, response, chain);
       return;
     }
+
+    if (useCookie) {
+      try {
+        if (authenticateWithCookies((HttpServletRequest) request, (HttpServletResponse) response, chain)) {
+          // if there was a valid cookie authentication was handled, there is no point in
+          // going forward to check the JWT path in the header
+          return;
+        }
+      } catch (NoValidCookiesException e) {
+        log.missingValidCookie();
+        handleValidationError((HttpServletRequest) request, (HttpServletResponse) response, HttpServletResponse.SC_UNAUTHORIZED,
+            "There is no valid cookie found");
+        return;
+      }
+    }
+
     final Pair<TokenType, String> wireToken = getWireToken(request);
 
     if (wireToken != null && wireToken.getLeft() != null && wireToken.getRight() != null) {
@@ -229,6 +263,36 @@ public class JWTFederationFilter extends AbstractJWTFilter {
       return parsed;
   }
 
+  /*
+   * Attempts to authenticate using session cookies.
+   */
+  private boolean authenticateWithCookies(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
+      throws NoValidCookiesException, ServletException, IOException {
+    final List<Cookie> relevantCookies = CookieUtils.getCookiesForName(request, cookieName);
+    for (Cookie ssoCookie : relevantCookies) {
+      try {
+        final JWT token = new JWTToken(ssoCookie.getValue());
+        if (validateToken(request, response, chain, token)) {
+          final Subject subject = createSubjectFromToken(token);
+          continueWithEstablishedSecurityContext(subject, request, response, chain);
+          // we found a valid cookie we don't need to keep checking anymore
+          return true;
+        }
+      } catch (ParseException | UnknownTokenException ignore) {
+        // Ignore the error since cookie was invalid
+        // Fall through to keep checking if there are more cookies
+      }
+    }
+
+    if (!relevantCookies.isEmpty()) {
+      // No valid cookies found but cookie was present so reject this request and do
+      // no further processing
+      throw new NoValidCookiesException();
+    }
+
+    return false;
+  }
+
   @Override
   protected void handleValidationError(HttpServletRequest request, HttpServletResponse response, int status,
                                        String error) throws IOException {
@@ -268,4 +332,15 @@ public class JWTFederationFilter extends AbstractJWTFilter {
     }
   }
 
+  /**
+   * An exception indicating that cookies are present, but none of them contain a
+   * valid JWT.
+   */
+  @SuppressWarnings("serial")
+  private class NoValidCookiesException extends Exception {
+    NoValidCookiesException() {
+      super("None of the presented cookies are valid.");
+    }
+  }
+
 }
diff --git a/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/SSOCookieFederationFilter.java b/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/SSOCookieFederationFilter.java
index aa712490b..cf6767b6f 100644
--- a/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/SSOCookieFederationFilter.java
+++ b/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/SSOCookieFederationFilter.java
@@ -63,7 +63,7 @@ public class SSOCookieFederationFilter extends AbstractJWTFilter {
   public static final String X_FORWARDED_PROTO = "X-Forwarded-Proto";
 
   private static final String ORIGINAL_URL_QUERY_PARAM = "originalUrl=";
-  private static final String DEFAULT_SSO_COOKIE_NAME = "hadoop-jwt";
+  public static final String DEFAULT_SSO_COOKIE_NAME = "hadoop-jwt";
 
   /* A semicolon separated list of paths that need to bypass authentication */
   private static final String SSO_UNAUTHENTICATED_PATHS_PARAM = "sso.unauthenticated.path.list";
diff --git a/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/AbstractJWTFilterTest.java b/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/AbstractJWTFilterTest.java
index 8a2bffb35..7219a6a73 100644
--- a/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/AbstractJWTFilterTest.java
+++ b/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/AbstractJWTFilterTest.java
@@ -980,6 +980,10 @@ public abstract class AbstractJWTFilterTest  {
     return props;
   }
 
+  protected SignedJWT getJWT(String issuer, String sub, Date expires) throws Exception {
+    return getJWT(issuer, sub, expires, privateKey);
+  }
+
   protected SignedJWT getJWT(String issuer, String sub, Date expires, RSAPrivateKey privateKey)
       throws Exception {
     return getJWT(issuer, sub, expires, new Date(), privateKey, JWSAlgorithm.RS256.getName());
@@ -1069,6 +1073,10 @@ public abstract class AbstractJWTFilterTest  {
 
       subject = Subject.getSubject( AccessController.getContext() );
     }
+
+    public Subject getSubject() {
+      return subject;
+    }
   }
 
   protected interface TokenVerificationCounter {
diff --git a/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/JWTFederationFilterTest.java b/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/JWTFederationFilterTest.java
index 20966bccc..f17897384 100644
--- a/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/JWTFederationFilterTest.java
+++ b/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/JWTFederationFilterTest.java
@@ -17,13 +17,23 @@
  */
 package org.apache.knox.gateway.provider.federation;
 
-import com.nimbusds.jwt.SignedJWT;
+import static org.apache.knox.gateway.provider.federation.jwt.filter.AbstractJWTFilter.JWT_DEFAULT_ISSUER;
+import static org.apache.knox.gateway.provider.federation.jwt.filter.SSOCookieFederationFilter.DEFAULT_SSO_COOKIE_NAME;
+import static org.junit.Assert.assertEquals;
+
+import java.util.Date;
+import java.util.Properties;
+
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.knox.gateway.provider.federation.jwt.filter.JWTFederationFilter;
 import org.easymock.EasyMock;
 import org.junit.Before;
 import org.junit.Test;
 
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
+import com.nimbusds.jwt.SignedJWT;
 
 @SuppressWarnings("PMD.TestClassWithoutTestCases")
 public class JWTFederationFilterTest extends AbstractJWTFilterTest {
@@ -72,4 +82,61 @@ public class JWTFederationFilterTest extends AbstractJWTFilterTest {
 
     EasyMock.verify(response);
   }
+
+  @Test
+  public void testCookieAuthSupportValidCookie() throws Exception {
+    testCookieAuthSupport(true);
+  }
+
+  @Test
+  public void testCookieAuthSupportInvalidCookie() throws Exception {
+    testCookieAuthSupport(false);
+  }
+
+  @Test
+  public void testCookieAuthSupportCustomCookieName() throws Exception {
+    testCookieAuthSupport(true, "customCookie");
+  }
+
+  private void testCookieAuthSupport(boolean validCookie) throws Exception {
+    testCookieAuthSupport(validCookie, null);
+  }
+
+  private void testCookieAuthSupport(boolean validCookie, String customCookieName) throws Exception {
+    final Properties properties = getProperties();
+    properties.put(JWTFederationFilter.KNOX_TOKEN_USE_COOKIE, "true");
+    if (customCookieName != null) {
+      properties.put(JWTFederationFilter.KNOX_TOKEN_COOKIE_NAME, customCookieName);
+    }
+    handler.init(new TestFilterConfig(properties));
+
+    final String subject = "bob";
+    final HttpServletRequest request = EasyMock.createNiceMock(HttpServletRequest.class);
+    final SignedJWT jwt = getJWT(JWT_DEFAULT_ISSUER, subject, new Date(System.currentTimeMillis() + 60000));
+    final Cookie[] cookies = new Cookie[1];
+    final Cookie cookie = EasyMock.createNiceMock(Cookie.class);
+    EasyMock.expect(cookie.getValue()).andReturn(jwt.serialize());
+    final String cookieName = validCookie ? (customCookieName == null ? DEFAULT_SSO_COOKIE_NAME : customCookieName) : "dummyCookie";
+    EasyMock.expect(cookie.getName()).andReturn(cookieName).anyTimes();
+    cookies[0] = cookie;
+    EasyMock.expect(request.getCookies()).andReturn(cookies).anyTimes();
+
+    final HttpServletResponse response = EasyMock.createNiceMock(HttpServletResponse.class);
+    if (!validCookie) {
+      response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
+      EasyMock.expectLastCall().once();
+    }
+    EasyMock.replay(request, response, cookie);
+
+    final TestFilterChain chain = new TestFilterChain();
+    handler.doFilter(request, response, chain);
+
+    if (validCookie) {
+      assertEquals(1, chain.getSubject().getPrincipals().size());
+      assertEquals(subject, chain.getSubject().getPrincipals().iterator().next().getName());
+    } else {
+      EasyMock.verify(response);
+    }
+  }
+
 }