This is an automated email from the ASF dual-hosted git repository.

pzampino 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 7dd8b4318 KNOX-3073 - Token verification fallback to Knox keys 
behavior should configurable (#949)
7dd8b4318 is described below

commit 7dd8b4318c8a685985b08cd2870bf212be814db2
Author: Phil Zampino <[email protected]>
AuthorDate: Fri Nov 8 16:52:15 2024 -0500

    KNOX-3073 - Token verification fallback to Knox keys behavior should 
configurable (#949)
---
 .../hadoopauth/filter/HadoopAuthFilterTest.java    |   2 +
 .../federation/jwt/filter/AbstractJWTFilter.java   |  17 ++-
 .../provider/federation/AbstractJWTFilterTest.java | 130 ++++++++++++++++++++-
 .../JWTAsHTTPBasicCredsFederationFilterTest.java   |  34 ++++++
 .../provider/federation/SSOCookieProviderTest.java |  28 +++++
 5 files changed, 208 insertions(+), 3 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 c06876524..934959f27 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
@@ -17,6 +17,7 @@
  */
 package org.apache.knox.gateway.hadoopauth.filter;
 
+import static 
org.apache.knox.gateway.provider.federation.jwt.filter.AbstractJWTFilter.JWT_INSTANCE_KEY_FALLBACK;
 import static org.easymock.EasyMock.anyString;
 import static org.easymock.EasyMock.capture;
 import static org.easymock.EasyMock.captureInt;
@@ -577,6 +578,7 @@ public class HadoopAuthFilterTest {
     
expect(filterConfig.getInitParameter("support.jwt")).andReturn(supportJwt).anyTimes();
     
expect(filterConfig.getInitParameter("hadoop.auth.unauthenticated.path.list")).andReturn(null).anyTimes();
     
expect(filterConfig.getInitParameter("clusterName")).andReturn("topology1").anyTimes();
+    
expect(filterConfig.getInitParameter(JWT_INSTANCE_KEY_FALLBACK)).andReturn("false").anyTimes();
     final boolean isJwtSupported = Boolean.parseBoolean(supportJwt);
     if (isJwtSupported) {
       
expect(filterConfig.getInitParameter(JWTFederationFilter.KNOX_TOKEN_AUDIENCES)).andReturn(null).anyTimes();
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 fb1517a73..5668066a4 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
@@ -31,6 +31,7 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Date;
 import java.util.HashSet;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Locale;
 import java.util.Set;
@@ -102,6 +103,9 @@ 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 JWT_INSTANCE_KEY_FALLBACK = 
"jwt.instance.key.fallback";
+  public static final boolean JWT_INSTANCE_KEY_FALLBACK_DEFAULT = false;
+
   static JWTMessages log = MessagesFactory.get( JWTMessages.class );
 
   private static AuditService auditService = 
AuditServiceFactory.getAuditService();
@@ -116,13 +120,14 @@ public abstract class AbstractJWTFilter implements Filter 
{
   private String expectedIssuer;
   private String expectedSigAlg;
   protected String expectedPrincipalClaim;
-  protected Set<URI> expectedJWKSUrls = new HashSet();
+  protected Set<URI> expectedJWKSUrls = new LinkedHashSet();
   protected Set<JOSEObjectType> allowedJwsTypes;
 
   private TokenStateService tokenStateService;
   private TokenMAC tokenMAC;
   protected long idleTimeoutSeconds = -1;
   protected String topologyName;
+  protected boolean isJwtInstanceKeyFallback = 
JWT_INSTANCE_KEY_FALLBACK_DEFAULT;
 
   @Override
   public abstract void doFilter(ServletRequest request, ServletResponse 
response, FilterChain chain)
@@ -158,6 +163,9 @@ public abstract class AbstractJWTFilter implements Filter {
     // Setup the verified tokens cache
     topologyName = context != null ? (String) 
context.getAttribute(GatewayServices.GATEWAY_CLUSTER_ATTRIBUTE) : null;
     signatureVerificationCache = 
SignatureVerificationCache.getInstance(topologyName, filterConfig);
+
+    String fallbackConfig = 
filterConfig.getInitParameter(JWT_INSTANCE_KEY_FALLBACK);
+    isJwtInstanceKeyFallback = fallbackConfig != null ? 
Boolean.parseBoolean(fallbackConfig) : JWT_INSTANCE_KEY_FALLBACK_DEFAULT;
   }
 
   protected void configureExpectedParameters(FilterConfig filterConfig) {
@@ -512,17 +520,22 @@ public abstract class AbstractJWTFilter implements Filter 
{
     // If it has not yet been verified, then perform the verification now
     if (!verified) {
       try {
+        boolean attemptedPEMVerification  = false;
+        boolean attemptedJWKSVerification = false;
+
         if (publicKey != null) {
+          attemptedPEMVerification = true;
           verified = authority.verifyToken(token, publicKey);
           log.pemVerificationResultMessage(verified);
         }
 
         if (!verified && expectedJWKSUrls != null && 
!expectedJWKSUrls.isEmpty()) {
+          attemptedJWKSVerification = true;
           verified = authority.verifyToken(token, expectedJWKSUrls, 
expectedSigAlg, allowedJwsTypes);
           log.jwksVerificationResultMessage(verified);
         }
 
-        if(!verified) {
+        if(!verified && ((!attemptedPEMVerification && 
!attemptedJWKSVerification) || isJwtInstanceKeyFallback)) {
           verified = authority.verifyToken(token);
           log.signingKeyVerificationResultMessage(verified);
         }
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 bb255a19c..0b2dcbc7a 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
@@ -75,6 +75,8 @@ import java.util.Set;
 import java.util.UUID;
 import java.util.concurrent.TimeUnit;
 
+import static 
org.apache.knox.gateway.provider.federation.jwt.filter.AbstractJWTFilter.JWT_INSTANCE_KEY_FALLBACK;
+import static 
org.apache.knox.gateway.provider.federation.jwt.filter.JWTFederationFilter.JWKS_URL;
 import static org.junit.Assert.fail;
 
 public abstract class AbstractJWTFilterTest  {
@@ -627,6 +629,9 @@ public abstract class AbstractJWTFilterTest  {
       /* Add a failing PEM */
       props.put(getVerificationPemProperty(), failingPem);
 
+      /* Turn fallback to signing key on  */
+      props.put(JWT_INSTANCE_KEY_FALLBACK, "true");
+
       /* This handler is setup with a publicKey, corresponding privateKey is 
used to sign the JWT below */
       handler.init(new TestFilterConfig(props));
 
@@ -660,7 +665,7 @@ public abstract class AbstractJWTFilterTest  {
    * This will test the signature verification chain.
    * Specifically the flow when provided PEM is not invalid and
    * knox signing key is valid.
-   *
+   * AND JWT_INSTANCE_KEY_FALLBACK is true
    * NOTE: here valid means can validate JWT.
    * @throws Exception
    */
@@ -668,6 +673,7 @@ public abstract class AbstractJWTFilterTest  {
   public void testSignatureVerificationChainWithPEMandSignature() throws 
Exception {
     try {
       Properties props = getProperties();
+      props.put(JWT_INSTANCE_KEY_FALLBACK, "true");
       KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
       kpg.initialize(2048);
 
@@ -709,6 +715,128 @@ public abstract class AbstractJWTFilterTest  {
     }
   }
 
+  @Test
+  public void testNoPEMOrJwksWithoutFallback() throws Exception {
+    // Test fallback disabled, but not PEM configured.
+    // You can't disable key fallback without specifying an explicit 
verification method.
+    boolean verified = doTestSignatureVerificationChain(null, null, false);
+    Assert.assertTrue("Token should have been verified.", verified);
+  }
+
+  @Test
+  public void testNoPEMOrJwksWithFallback() throws Exception {
+    boolean verified = doTestSignatureVerificationChain(null, null, true);
+    Assert.assertTrue("Token should have been verified by falling back to 
keys.", verified);
+  }
+
+  @Test
+  public void testInvalidPEMNoJwksWithFallback() throws Exception {
+    boolean verified = doTestSignatureVerificationChain(pem, null, true);
+    Assert.assertTrue("Token should have been verified by falling back to 
keys.", verified);
+  }
+
+  @Test
+  public void testInvalidPEMNoJwksWithoutFallback() throws Exception {
+    String invalidPEM = generateInvalidPEM();
+    boolean verified = doTestSignatureVerificationChain(invalidPEM, null, 
false);
+    Assert.assertFalse("Token should NOT have been verified.", verified);
+  }
+
+  @Test
+  public void testNoPEMInvalidJwksWithoutFallback() throws Exception {
+    boolean verified = doTestSignatureVerificationChain(null, 
"https://localhost/nonesense";, false);
+    Assert.assertFalse("Token should have NOT been verified.", verified);
+  }
+
+  @Test
+  public void testNoPEMInvalidJwksWithFallback() throws Exception {
+    boolean verified = doTestSignatureVerificationChain(null, 
"https://localhost/nonesense";, true);
+    Assert.assertTrue("Token should have been verified by falling back to 
keys.", verified);
+  }
+
+  @Test
+  public void testInvalidPEMInvalidJwksWithoutFallback() throws Exception {
+    String invalidPEM = generateInvalidPEM();
+    boolean verified = doTestSignatureVerificationChain(invalidPEM, 
"https://localhost/nonesense";, false);
+    Assert.assertFalse("Token should NOT have been verified.", verified);
+  }
+
+  @Test
+  public void testInvalidPEMInvalidJwksWithFallback() throws Exception {
+    String invalidPEM = generateInvalidPEM();
+    boolean verified = doTestSignatureVerificationChain(invalidPEM, 
"https://localhost/nonesense";, true);
+    Assert.assertTrue("Token should have been verified by falling back to 
keys.", verified);
+  }
+
+  protected String generateInvalidPEM() throws Exception {
+    KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
+    kpg.initialize(2048);
+
+    KeyPair KPair = kpg.generateKeyPair();
+    String dn = 
buildDistinguishedName(InetAddress.getLocalHost().getHostName());
+    Certificate cert = X509CertificateUtil.generateCertificate(dn, KPair, 365, 
"SHA1withRSA");
+    byte[] data = cert.getEncoded();
+    Base64 encoder = new Base64( 76, "\n".getBytes( StandardCharsets.US_ASCII 
) );
+    return new String(encoder.encodeToString( data ).getBytes( 
StandardCharsets.US_ASCII ), StandardCharsets.US_ASCII).trim();
+  }
+
+  /**
+   * This will test the signature verification chain in the following order
+   * 1. PEM - check if PEM is configured and signature is validated
+   * 2. JWKS - check if endpoint id configured if not skip
+   * 3. Knox signing key - if the above two fail try to validate using knox 
signing cert
+   * @throws Exception
+   */
+  public boolean doTestSignatureVerificationChain(final String testPEM,
+                                                  final String testJwks,
+                                                  final boolean 
fallbackToKeys) throws Exception {
+    boolean isVerified = false;
+
+    try {
+      Properties props = getProperties();
+      props.put(getAudienceProperty(), "bar");
+
+      if (testPEM != null) {
+        // Add a test PEM
+        props.put(getVerificationPemProperty(), testPEM);
+      }
+
+      if (testJwks != null) {
+        // Add the test JWKS URL
+        props.put(JWKS_URL, testJwks);
+      }
+
+      // Configure fallback to signing key on
+      props.put(JWT_INSTANCE_KEY_FALLBACK, String.valueOf(fallbackToKeys));
+
+      // This handler is setup with a publicKey, corresponding privateKey is 
used to sign the JWT below
+      handler.init(new TestFilterConfig(props));
+
+      SignedJWT jwt = getJWT(AbstractJWTFilter.JWT_DEFAULT_ISSUER, "alice",
+              new Date(new Date().getTime() + TimeUnit.MINUTES.toMillis(10)), 
privateKey);
+
+      HttpServletRequest request = 
EasyMock.createNiceMock(HttpServletRequest.class);
+      setTokenOnRequest(request, jwt);
+
+      EasyMock.expect(request.getRequestURL()).andReturn(new 
StringBuffer(SERVICE_URL)).anyTimes();
+      EasyMock.expect(request.getPathInfo()).andReturn("resource").anyTimes();
+      EasyMock.expect(request.getQueryString()).andReturn(null);
+      HttpServletResponse response = 
EasyMock.createNiceMock(HttpServletResponse.class);
+      
EasyMock.expect(response.encodeRedirectURL(SERVICE_URL)).andReturn(SERVICE_URL);
+      
EasyMock.expect(response.getOutputStream()).andAnswer(DummyServletOutputStream::new).anyTimes();
+      EasyMock.replay(request, response);
+
+      TestFilterChain chain = new TestFilterChain();
+      handler.doFilter(request, response, chain);
+      isVerified = chain.doFilterCalled;
+
+    } catch (ServletException se) {
+      fail("Should NOT have thrown a ServletException.");
+    }
+
+    return isVerified;
+  }
+
   @Test
   public void testInvalidIssuer() throws Exception {
     try {
diff --git 
a/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/JWTAsHTTPBasicCredsFederationFilterTest.java
 
b/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/JWTAsHTTPBasicCredsFederationFilterTest.java
index a5549ade3..0041ab3d0 100644
--- 
a/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/JWTAsHTTPBasicCredsFederationFilterTest.java
+++ 
b/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/JWTAsHTTPBasicCredsFederationFilterTest.java
@@ -111,5 +111,39 @@ public class JWTAsHTTPBasicCredsFederationFilterTest 
extends AbstractJWTFilterTe
         }
     }
 
+    @Override
+    @Test
+    public void testNoPEMInvalidJwksWithoutFallback() throws Exception {
+        // No-op: This filter does not appear to support the JWKS URL(s) 
config like the
+        // JWTFederationFilter does, so this test does not apply
+    }
+
+    @Override
+    @Test
+    public void testNoPEMInvalidJwksWithFallback() throws Exception {
+        // No-op: This filter does not appear to support the JWKS URL(s) 
config like the
+        // JWTFederationFilter does, so this test does not apply
+    }
+
+    @Override
+    @Test
+    public void testInvalidPEMNoJwksWithoutFallback() throws Exception {
+        // No-op: This filter does not appear to support the JWKS URL(s) 
config like the
+        // JWTFederationFilter does, so this test does not apply
+    }
+
+    @Override
+    @Test
+    public void testInvalidPEMInvalidJwksWithoutFallback() throws Exception {
+        // No-op: This filter does not appear to support the JWKS URL(s) 
config like the
+        // JWTFederationFilter does, so this test does not apply
+    }
+
+    @Override
+    @Test
+    public void testInvalidPEMInvalidJwksWithFallback() throws Exception {
+        // No-op: This filter does not appear to support the JWKS URL(s) 
config like the
+        // JWTFederationFilter does, so this test does not apply
+    }
 }
 
diff --git 
a/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/SSOCookieProviderTest.java
 
b/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/SSOCookieProviderTest.java
index e38e6a871..f5f1a6e5d 100644
--- 
a/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/SSOCookieProviderTest.java
+++ 
b/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/SSOCookieProviderTest.java
@@ -330,6 +330,34 @@ public class SSOCookieProviderTest extends 
AbstractJWTFilterTest {
     Assert.assertEquals(loginURL, 
"https://remotehost/notgateway/knoxsso/api/v1/websso?originalUrl="; + 
"https://remotehost/resource";);
   }
 
+  @Override
+  @Test
+  public void testNoPEMInvalidJwksWithoutFallback() throws Exception {
+    // No-op: The SSOCookieProvider does not appear to support the JWKS URL(s) 
config like the
+    // JWTFederationFilter does, so this test does not apply
+  }
+
+  @Override
+  @Test
+  public void testNoPEMInvalidJwksWithFallback() throws Exception {
+    // No-op: The SSOCookieProvider does not appear to support the JWKS URL(s) 
config like the
+    // JWTFederationFilter does, so this test does not apply
+  }
+
+  @Override
+  @Test
+  public void testInvalidPEMInvalidJwksWithoutFallback() throws Exception {
+    // No-op: The SSOCookieProvider does not appear to support the JWKS URL(s) 
config like the
+    // JWTFederationFilter does, so this test does not apply
+  }
+
+  @Override
+  @Test
+  public void testInvalidPEMInvalidJwksWithFallback() throws Exception {
+    // No-op: The SSOCookieProvider does not appear to support the JWKS URL(s) 
config like the
+    // JWTFederationFilter does, so this test does not apply
+  }
+
   @Test
   public void testIdleTimoutExceeded() throws Exception {
     final TokenStateService tokenStateService = 
EasyMock.createNiceMock(TokenStateService.class);

Reply via email to