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 64f469b  KNOX-2544 - Token-based providers should cache successful 
(including 3rd-party) token verifications (#440)
64f469b is described below

commit 64f469bec7f67f3f63b78e8abc6cce1dbd9fdeac
Author: Phil Zampino <[email protected]>
AuthorDate: Mon May 3 14:17:23 2021 -0400

    KNOX-2544 - Token-based providers should cache successful (including 
3rd-party) token verifications (#440)
---
 .../hadoopauth/filter/HadoopAuthFilterTest.java    |   4 +-
 .../provider/federation/jwt/JWTMessages.java       |   3 +
 .../federation/jwt/filter/AbstractJWTFilter.java   |  76 ++++------
 .../jwt/filter/SignatureVerificationCache.java     | 137 ++++++++++++++++++
 .../provider/federation/AbstractJWTFilterTest.java | 109 +++++++-------
 .../provider/federation/TestFilterConfig.java      |  68 +++++++++
 ...okenIDAsHTTPBasicCredsFederationFilterTest.java |   5 +
 .../federation/jwt/filter/JWTTestUtils.java        |  81 +++++++++++
 .../jwt/filter/SignatureVerificationCacheTest.java | 161 +++++++++++++++++++++
 9 files changed, 543 insertions(+), 101 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 f81da17..96f5804 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
@@ -33,6 +33,7 @@ import org.apache.knox.gateway.GatewayFilter;
 import org.apache.knox.gateway.config.GatewayConfig;
 import 
org.apache.knox.gateway.provider.federation.jwt.filter.AbstractJWTFilter;
 import 
org.apache.knox.gateway.provider.federation.jwt.filter.JWTFederationFilter;
+import 
org.apache.knox.gateway.provider.federation.jwt.filter.SignatureVerificationCache;
 import org.apache.knox.gateway.services.GatewayServices;
 import org.apache.knox.gateway.services.security.AliasService;
 import org.apache.knox.gateway.topology.Topology;
@@ -171,10 +172,11 @@ public class HadoopAuthFilterTest {
       
expect(filterConfig.getInitParameter(JWTFederationFilter.TOKEN_VERIFICATION_PEM)).andReturn(null).anyTimes();
       
expect(filterConfig.getInitParameter(AbstractJWTFilter.JWT_EXPECTED_ISSUER)).andReturn(null).anyTimes();
       
expect(filterConfig.getInitParameter(AbstractJWTFilter.JWT_EXPECTED_SIGALG)).andReturn(null).anyTimes();
-      
expect(filterConfig.getInitParameter(AbstractJWTFilter.JWT_VERIFIED_CACHE_MAX)).andReturn(null).anyTimes();
+      
expect(filterConfig.getInitParameter(SignatureVerificationCache.JWT_VERIFIED_CACHE_MAX)).andReturn(null).anyTimes();
     }
 
     final ServletContext servletContext = createMock(ServletContext.class);
+    
expect(servletContext.getAttribute(GatewayServices.GATEWAY_CLUSTER_ATTRIBUTE)).andReturn("test").anyTimes();
     
expect(servletContext.getAttribute("signer.secret.provider.object")).andReturn(null).atLeastOnce();
     if (isJwtSupported) {
       
expect(servletContext.getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE)).andReturn(null).anyTimes();
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 8a01811..23494fe 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
@@ -73,4 +73,7 @@ public interface JWTMessages {
             text = "Missing token passcode." )
   void missingTokenPasscode();
 
+  @Message( level = MessageLevel.INFO, text = "Initialized token signature 
verification cache for the {0} topology." )
+  void initializedSignatureVerificationCache(String topology);
+
 }
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 45a3ce6..6eeaf37 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
@@ -41,8 +41,6 @@ import javax.servlet.ServletResponse;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
-import com.github.benmanes.caffeine.cache.Cache;
-import com.github.benmanes.caffeine.cache.Caffeine;
 import org.apache.knox.gateway.audit.api.Action;
 import org.apache.knox.gateway.audit.api.ActionOutcome;
 import org.apache.knox.gateway.audit.api.AuditContext;
@@ -84,10 +82,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 JWT_VERIFIED_CACHE_MAX = "jwt.verified.cache.max";
-  public static final int    JWT_VERIFIED_CACHE_MAX_DEFAULT = 250;
-
   static JWTMessages log = MessagesFactory.get( JWTMessages.class );
+
   private static AuditService auditService = 
AuditServiceFactory.getAuditService();
   private static Auditor auditor = auditService.getAuditor(
       AuditConstants.DEFAULT_AUDITOR_NAME, AuditConstants.KNOX_SERVICE_NAME,
@@ -96,7 +92,7 @@ public abstract class AbstractJWTFilter implements Filter {
   protected List<String> audiences;
   protected JWTokenAuthority authority;
   protected RSAPublicKey publicKey;
-  protected Cache<String, Boolean> verifiedTokens;
+  protected SignatureVerificationCache signatureVerificationCache;
   private String expectedIssuer;
   private String expectedSigAlg;
   protected String expectedPrincipalClaim;
@@ -129,27 +125,9 @@ public abstract class AbstractJWTFilter implements Filter {
     }
 
     // Setup the verified tokens cache
-    initializeVerifiedTokensCache(filterConfig);
-  }
-
-  /**
-   * Initialize the cache for token verifications records.
-   *
-   * @param config The filter configuration
-   */
-  private void initializeVerifiedTokensCache(final FilterConfig config) {
-    int maxCacheSize = JWT_VERIFIED_CACHE_MAX_DEFAULT;
-
-    String configValue = config.getInitParameter(JWT_VERIFIED_CACHE_MAX);
-    if (configValue != null && !configValue.isEmpty()) {
-      try {
-        maxCacheSize = Integer.parseInt(configValue);
-      } catch (NumberFormatException e) {
-        log.invalidVerificationCacheMaxConfiguration(configValue);
-      }
-    }
-
-    verifiedTokens = Caffeine.newBuilder().maximumSize(maxCacheSize).build();
+    String topologyName =
+              (context != null) ? (String) 
context.getAttribute(GatewayServices.GATEWAY_CLUSTER_ATTRIBUTE) : null;
+    signatureVerificationCache = 
SignatureVerificationCache.getInstance(topologyName, filterConfig);
   }
 
   protected void configureExpectedParameters(FilterConfig filterConfig) {
@@ -357,12 +335,10 @@ public abstract class AbstractJWTFilter implements Filter 
{
         } else {
           log.tokenHasExpired(displayableToken, displayableTokenId);
 
-          if (tokenId != null) {
-            // Explicitly evict the record of this token's signature 
verification (if present).
-            // There is no value in keeping this record for expired tokens, 
and explicitly removing them may prevent
-            // records for other valid tokens from being prematurely evicted 
from the cache.
-            removeSignatureVerificationRecord(tokenId);
-          }
+          // Explicitly evict the record of this token's signature 
verification (if present).
+          // There is no value in keeping this record for expired tokens, and 
explicitly removing them may prevent
+          // records for other valid tokens from being prematurely evicted 
from the cache.
+          removeSignatureVerificationRecord(token.toString());
 
           handleValidationError(request, response, 
HttpServletResponse.SC_BAD_REQUEST,
                                 "Bad request: token has expired");
@@ -409,13 +385,13 @@ public abstract class AbstractJWTFilter implements Filter 
{
     return false;
   }
 
-    protected boolean verifyTokenSignature(final JWT token) {
+  protected boolean verifyTokenSignature(final JWT token) {
     boolean verified;
 
-    String tokenId = TokenUtils.getTokenId(token);
+    final String serializedJWT = token.toString();
 
     // Check if the token has already been verified
-    verified = (tokenId != null) && hasSignatureBeenVerified(tokenId);
+    verified = hasSignatureBeenVerified(serializedJWT);
 
     // If it has not yet been verified, then perform the verification now
     if (!verified) {
@@ -444,8 +420,8 @@ public abstract class AbstractJWTFilter implements Filter {
         }
       }
 
-      if (verified && tokenId != null) { // If successful, record the 
verification for future reference
-        recordSignatureVerification(tokenId);
+      if (verified) { // If successful, record the verification for future 
reference
+        recordSignatureVerification(serializedJWT);
       }
     }
 
@@ -453,32 +429,32 @@ public abstract class AbstractJWTFilter implements Filter 
{
   }
 
   /**
-   * Determine if the specified token signature has previously been 
successfully verified.
+   * Determine if the specified JWT signature has previously been successfully 
verified.
    *
-   * @param tokenId The unique identifier for a token.
+   * @param jwt A serialized JWT String.
    *
    * @return true, if the specified token has been previously verified; 
Otherwise, false.
    */
-  protected boolean hasSignatureBeenVerified(final String tokenId) {
-    return (verifiedTokens.getIfPresent(tokenId) != null);
+  protected boolean hasSignatureBeenVerified(final String jwt) {
+    return signatureVerificationCache.hasSignatureBeenVerified(jwt);
   }
 
   /**
-   * Record a successful token signature verification.
+   * Record a successful JWT signature verification.
    *
-   * @param tokenId The unique identifier for the token which has been 
successfully verified.
+   * @param jwt The serialized String for a JWT which has been successfully 
verified.
    */
-  protected void recordSignatureVerification(final String tokenId) {
-    verifiedTokens.put(tokenId, true);
+  protected void recordSignatureVerification(final String jwt) {
+    signatureVerificationCache.recordSignatureVerification(jwt);
   }
 
   /**
-   * Explicitly evict the signature verification record from the cache if it 
exists.
+   * Explicitly evict the signature verification record for the specified JWT 
from the cache if it exists.
    *
-   * @param tokenId The token whose signature verification record should be 
evicted.
+   * @param jwt The serialized String for a JWT whose signature verification 
record should be evicted.
    */
-  protected void removeSignatureVerificationRecord(final String tokenId) {
-    verifiedTokens.asMap().remove(tokenId);
+  protected void removeSignatureVerificationRecord(final String jwt) {
+    signatureVerificationCache.removeSignatureVerificationRecord(jwt);
   }
 
   protected abstract void handleValidationError(HttpServletRequest request, 
HttpServletResponse response, int status,
diff --git 
a/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/SignatureVerificationCache.java
 
b/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/SignatureVerificationCache.java
new file mode 100644
index 0000000..2e62300
--- /dev/null
+++ 
b/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/SignatureVerificationCache.java
@@ -0,0 +1,137 @@
+/*
+ * 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.jwt.filter;
+
+import com.github.benmanes.caffeine.cache.Cache;
+import com.github.benmanes.caffeine.cache.Caffeine;
+import org.apache.knox.gateway.i18n.messages.MessagesFactory;
+import org.apache.knox.gateway.provider.federation.jwt.JWTMessages;
+
+import javax.servlet.FilterConfig;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * A shared record of tokens for which the signature has been verified.
+ */
+public class SignatureVerificationCache {
+
+    public static final String JWT_VERIFIED_CACHE_MAX = 
"jwt.verified.cache.max";
+    public static final int    JWT_VERIFIED_CACHE_MAX_DEFAULT = 250;
+
+    static final String DEFAULT_CACHE_ID = "default-cache";
+
+    static JWTMessages log = MessagesFactory.get( JWTMessages.class );
+
+    private static final ConcurrentHashMap<String, SignatureVerificationCache> 
instances = new ConcurrentHashMap<>();
+
+    private Cache<String, Boolean> verifiedTokens;
+
+    /**
+     * Caches are topology-specific because the configuration is defined at 
the provider level.
+     *
+     * @param topology The topology for which the cache is being requested, or 
null if the default is sufficient.
+     * @param config   The FilterConfig associated with the calling provider.
+     *
+     * @return A SignatureVerificationCache for the specified topology, or the 
default one if no topology is specified.
+     */
+    @SuppressWarnings("PMD.SingletonClassReturningNewInstance")
+    public static SignatureVerificationCache getInstance(final String 
topology, final FilterConfig config) {
+        String cacheId = topology != null ? topology : DEFAULT_CACHE_ID;
+        return instances.computeIfAbsent(cacheId, c -> 
initializeCacheForTopology(cacheId, config));
+    }
+
+    private static SignatureVerificationCache initializeCacheForTopology(final 
String topology, final FilterConfig config) {
+        SignatureVerificationCache cache = new 
SignatureVerificationCache(config);
+        log.initializedSignatureVerificationCache(topology);
+        return cache;
+    }
+
+    private SignatureVerificationCache(final FilterConfig config) {
+        initializeVerifiedTokensCache(config);
+    }
+
+    /**
+     * Initialize the cache for token verification records.
+     *
+     * @param config The configuration of the provider employing this cache.
+     */
+    private void initializeVerifiedTokensCache(final FilterConfig config) {
+        int maxCacheSize = JWT_VERIFIED_CACHE_MAX_DEFAULT;
+
+        String configValue = config.getInitParameter(JWT_VERIFIED_CACHE_MAX);
+        if (configValue != null && !configValue.isEmpty()) {
+            try {
+                maxCacheSize = Integer.parseInt(configValue);
+            } catch (NumberFormatException e) {
+                log.invalidVerificationCacheMaxConfiguration(configValue);
+            }
+        }
+
+        verifiedTokens = 
Caffeine.newBuilder().maximumSize(maxCacheSize).build();
+    }
+
+    /**
+     * Determine if the specified JWT's signature has previously been 
successfully verified.
+     *
+     * @param jwt A serialized JWT.
+     *
+     * @return true, if the specified token has been previously verified; 
Otherwise, false.
+     */
+    public boolean hasSignatureBeenVerified(final String jwt) {
+        return (verifiedTokens.getIfPresent(jwt) != null);
+    }
+
+    /**
+     * Record a successful token signature verification.
+     *
+     * @param jwt A serialized JWT for which the signature has been 
successfully verified.
+     */
+    public void recordSignatureVerification(final String jwt) {
+        verifiedTokens.put(jwt, true);
+    }
+
+    /**
+     * Explicitly evict the signature verification record from the cache if it 
exists.
+     *
+     * @param jwt The serialized JWT for which the associated signature 
verification record should be evicted.
+     */
+    public void removeSignatureVerificationRecord(final String jwt) {
+         verifiedTokens.asMap().remove(jwt);
+    }
+
+    /**
+     * @return The size of the cache.
+     */
+    public long getSize() {
+        return verifiedTokens.estimatedSize();
+    }
+
+    /**
+     * Remove any entries which should be evicted from the cache.
+     */
+    public void performMaintenance() {
+        verifiedTokens.cleanUp();
+    }
+
+    /**
+     * Clear the contents of the cache.
+     */
+    public void clear() {
+        verifiedTokens.asMap().clear();
+    }
+}
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 16b1811..ff01c25 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
@@ -17,7 +17,6 @@
  */
 package org.apache.knox.gateway.provider.federation;
 
-import com.github.benmanes.caffeine.cache.Cache;
 import com.nimbusds.jose.JWSAlgorithm;
 import com.nimbusds.jose.JWSHeader;
 import com.nimbusds.jose.JWSSigner;
@@ -29,6 +28,7 @@ import com.nimbusds.jwt.SignedJWT;
 import org.apache.commons.codec.binary.Base64;
 import 
org.apache.knox.gateway.provider.federation.jwt.filter.AbstractJWTFilter;
 import 
org.apache.knox.gateway.provider.federation.jwt.filter.SSOCookieFederationFilter;
+import 
org.apache.knox.gateway.provider.federation.jwt.filter.SignatureVerificationCache;
 import org.apache.knox.gateway.security.PrimaryPrincipal;
 import org.apache.knox.gateway.services.security.token.JWTokenAttributes;
 import org.apache.knox.gateway.services.security.token.JWTokenAuthority;
@@ -43,8 +43,6 @@ import org.junit.Test;
 
 import javax.security.auth.Subject;
 import javax.servlet.FilterChain;
-import javax.servlet.FilterConfig;
-import javax.servlet.ServletContext;
 import javax.servlet.ServletException;
 import javax.servlet.ServletOutputStream;
 import javax.servlet.ServletRequest;
@@ -66,7 +64,6 @@ import java.security.interfaces.RSAPublicKey;
 import java.text.MessageFormat;
 import java.time.Instant;
 import java.util.Date;
-import java.util.Enumeration;
 import java.util.Locale;
 import java.util.Properties;
 import java.util.Set;
@@ -113,6 +110,12 @@ public abstract class AbstractJWTFilterTest  {
 
   @After
   public void tearDown() {
+    try {
+      clearSignatureVerificationCache();
+    } catch (Exception e) {
+      //
+    }
+
     handler.destroy();
   }
 
@@ -725,30 +728,63 @@ public abstract class AbstractJWTFilterTest  {
     }
   }
 
+  /**
+   * KNOX-2544
+   * Verify the behavior of the token signature verification optimization, 
with the internal Knox token identifier
+   * included in the JWTs.
+   */
   @Test
   public void testVerificationOptimization() throws Exception {
+    doTestVerificationOptimization(true);
+  }
+
+  /**
+   * KNOX-2544
+   * Verify the behavior of the token signature verification optimization, 
with the internal Knox token identifier
+   * omitted from the JWTs (e.g., third-party JWTs).
+   */
+  @Test
+  public void testVerificationOptimization_NoTokenID() throws Exception {
+    doTestVerificationOptimization(false);
+  }
+
+  /**
+   * KNOX-2544
+   * Verify the behavior of the token signature verification optimization, 
with or without the internal Knox token
+   * identifier.
+   *
+   * @param includeTokenId Flag indicating whether the test JWTs should 
include the internal Knox token identifier.
+   */
+  public void doTestVerificationOptimization(boolean includeTokenId) throws 
Exception {
     try {
 
       final String principalAlice = "alice";
       final String principalBob   = "bob";
 
+      final String tokenId = (includeTokenId ? 
String.valueOf(UUID.randomUUID()) : null);
+
       final SignedJWT jwt_alice = getJWT(AbstractJWTFilter.JWT_DEFAULT_ISSUER,
                                          principalAlice,
+                                         "myAudience",
                                          new Date(new Date().getTime() + 
TimeUnit.MINUTES.toMillis(10)),
                                          new Date(),
                                          privateKey,
-                                         JWSAlgorithm.RS512.getName());
+                                         JWSAlgorithm.RS512.getName(),
+                                         tokenId);
 
       final SignedJWT jwt_bob = getJWT(AbstractJWTFilter.JWT_DEFAULT_ISSUER,
                                        principalBob,
+                                       "myAudience",
                                        new Date(new Date().getTime() + 
TimeUnit.MINUTES.toMillis(10)),
                                        new Date(),
                                        privateKey,
-                                       JWSAlgorithm.RS512.getName());
+                                       JWSAlgorithm.RS512.getName(),
+                                       tokenId);
 
       Properties props = getProperties();
       props.put(AbstractJWTFilter.JWT_EXPECTED_SIGALG, "RS512");
-      props.put(AbstractJWTFilter.JWT_VERIFIED_CACHE_MAX, "1");
+      props.put(SignatureVerificationCache.JWT_VERIFIED_CACHE_MAX, "1");
+      props.put(TestFilterConfig.TOPOLOGY_NAME_PROP, 
"jwt-verification-optimization-test");
       handler.init(new TestFilterConfig(props));
       Assert.assertEquals("Expected no token verification calls yet.",
                           0, ((TokenVerificationCounter) 
handler).getVerificationCount());
@@ -800,7 +836,8 @@ public abstract class AbstractJWTFilterTest  {
 
       Properties props = getProperties();
       props.put(AbstractJWTFilter.JWT_EXPECTED_SIGALG, "RS512");
-      props.put(AbstractJWTFilter.JWT_VERIFIED_CACHE_MAX, "1");
+      props.put(SignatureVerificationCache.JWT_VERIFIED_CACHE_MAX, "1");
+      props.put(TestFilterConfig.TOPOLOGY_NAME_PROP, "jwt-eviction-test");
       handler.init(new TestFilterConfig(props));
       Assert.assertEquals("Expected no token verification calls yet.",
                           0, ((TokenVerificationCounter) 
handler).getVerificationCount());
@@ -878,7 +915,6 @@ public abstract class AbstractJWTFilterTest  {
   private void doTestVerificationOptimization(final HttpServletRequest request,
                                               final HttpServletResponse 
response,
                                               final String expectedPrincipal) 
throws Exception {
-
     TestFilterChain chain = new TestFilterChain();
     handler.doFilter(request, response, chain);
     Assert.assertTrue("doFilterCalled should not be false.", 
chain.doFilterCalled );
@@ -891,17 +927,24 @@ public abstract class AbstractJWTFilterTest  {
    * Wait for the size limit enforcement, such that the Least-Recently-Used 
verified token record(s) will be evicted.
    */
   private void evictVerifiedTokenRecords() throws Exception {
-    Field f = 
handler.getClass().getSuperclass().getSuperclass().getDeclaredField("verifiedTokens");
-    f.setAccessible(true);
-    Cache<String, Boolean> cache = (Cache<String, Boolean>) f.get(handler);
-    cache.cleanUp();
+    SignatureVerificationCache cache = getSignatureVerificationCache(handler);
+    cache.performMaintenance();
   }
 
   private long getSignatureVerificationCacheSize() throws Exception {
-    Field f = 
handler.getClass().getSuperclass().getSuperclass().getDeclaredField("verifiedTokens");
+    SignatureVerificationCache cache = getSignatureVerificationCache(handler);
+    return cache.getSize();
+  }
+
+  private void clearSignatureVerificationCache() throws Exception {
+    SignatureVerificationCache cache = getSignatureVerificationCache(handler);
+    cache.clear();
+  }
+
+  private static SignatureVerificationCache 
getSignatureVerificationCache(final AbstractJWTFilter filter) throws Exception {
+    Field f = 
filter.getClass().getSuperclass().getSuperclass().getDeclaredField("signatureVerificationCache");
     f.setAccessible(true);
-    Cache<String, Boolean> cache = (Cache<String, Boolean>) f.get(handler);
-    return cache.estimatedSize();
+    return (SignatureVerificationCache) f.get(filter);
   }
 
   protected Properties getProperties() {
@@ -959,40 +1002,6 @@ public abstract class AbstractJWTFilterTest  {
     return signedJWT;
   }
 
-  protected static class TestFilterConfig implements FilterConfig {
-    Properties props;
-
-    public TestFilterConfig(Properties props) {
-      this.props = props;
-    }
-
-    @Override
-    public String getFilterName() {
-      return null;
-    }
-
-    @Override
-    public ServletContext getServletContext() {
-//      JWTokenAuthority authority = 
EasyMock.createNiceMock(JWTokenAuthority.class);
-//      GatewayServices services = 
EasyMock.createNiceMock(GatewayServices.class);
-//      
EasyMock.expect(services.getService("TokenService").andReturn(authority));
-//      ServletContext context = EasyMock.createNiceMock(ServletContext.class);
-//      
EasyMock.expect(context.getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE).andReturn(new
 DefaultGatewayServices()));
-      return null;
-    }
-
-    @Override
-    public String getInitParameter(String name) {
-      return props.getProperty(name, null);
-    }
-
-    @Override
-    public Enumeration<String> getInitParameterNames() {
-      return null;
-    }
-
-  }
-
   protected static class TestJWTokenAuthority implements JWTokenAuthority {
 
     private PublicKey verifyingKey;
diff --git 
a/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/TestFilterConfig.java
 
b/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/TestFilterConfig.java
new file mode 100644
index 0000000..a44c359
--- /dev/null
+++ 
b/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/TestFilterConfig.java
@@ -0,0 +1,68 @@
+/*
+ * 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.apache.knox.gateway.services.GatewayServices;
+import org.easymock.EasyMock;
+
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletContext;
+import java.util.Enumeration;
+import java.util.Properties;
+
+public class TestFilterConfig implements FilterConfig {
+    public static final String TOPOLOGY_NAME_PROP = "test-topology-name";
+
+    Properties props;
+
+    public TestFilterConfig() {
+        this.props = new Properties();
+    }
+
+    public TestFilterConfig(Properties props) {
+        this.props = props;
+    }
+
+    @Override
+    public String getFilterName() {
+        return null;
+    }
+
+    @Override
+    public ServletContext getServletContext() {
+        String topologyName = props.getProperty(TOPOLOGY_NAME_PROP);
+        if (topologyName == null) {
+            topologyName = "jwt-test-topology";
+        }
+        ServletContext context = EasyMock.createNiceMock(ServletContext.class);
+        
EasyMock.expect(context.getAttribute(GatewayServices.GATEWAY_CLUSTER_ATTRIBUTE)).andReturn(topologyName).anyTimes();
+        EasyMock.replay(context);
+        return context;
+    }
+
+    @Override
+    public String getInitParameter(String name) {
+        return props.getProperty(name, null);
+    }
+
+    @Override
+    public Enumeration<String> getInitParameterNames() {
+        return null;
+    }
+
+}
diff --git 
a/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/TokenIDAsHTTPBasicCredsFederationFilterTest.java
 
b/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/TokenIDAsHTTPBasicCredsFederationFilterTest.java
index 157aabe..12384c8 100644
--- 
a/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/TokenIDAsHTTPBasicCredsFederationFilterTest.java
+++ 
b/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/TokenIDAsHTTPBasicCredsFederationFilterTest.java
@@ -314,6 +314,11 @@ public class TokenIDAsHTTPBasicCredsFederationFilterTest 
extends JWTAsHTTPBasicC
         // Override to disable N/A test
     }
 
+    @Override
+    public void testVerificationOptimization_NoTokenID() throws Exception {
+        // Override to disable N/A test
+    }
+
     /**
      * Very basic TokenStateService implementation for these tests only
      */
diff --git 
a/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/jwt/filter/JWTTestUtils.java
 
b/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/jwt/filter/JWTTestUtils.java
new file mode 100644
index 0000000..77e5bb6
--- /dev/null
+++ 
b/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/jwt/filter/JWTTestUtils.java
@@ -0,0 +1,81 @@
+/*
+ * 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.jwt.filter;
+
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.JWSHeader;
+import com.nimbusds.jose.JWSSigner;
+import com.nimbusds.jose.crypto.RSASSASigner;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.SignedJWT;
+import org.apache.knox.gateway.services.security.token.impl.JWTToken;
+
+import java.security.interfaces.RSAPrivateKey;
+import java.util.Date;
+import java.util.UUID;
+
+public class JWTTestUtils {
+
+    public static SignedJWT getJWT(String issuer, String sub, Date expires, 
RSAPrivateKey privateKey)
+            throws Exception {
+        return getJWT(issuer, sub, expires, new Date(), privateKey, 
JWSAlgorithm.RS256.getName());
+    }
+
+    public static SignedJWT getJWT(String issuer, String sub, Date expires, 
Date nbf, RSAPrivateKey privateKey,
+                               String signatureAlgorithm)
+            throws Exception {
+        return getJWT(issuer, sub, "bar", expires, nbf, privateKey, 
signatureAlgorithm);
+    }
+
+    public static SignedJWT getJWT(String issuer, String sub, String aud, Date 
expires, Date nbf, RSAPrivateKey privateKey,
+                               String signatureAlgorithm) throws Exception {
+        return getJWT(issuer, sub, aud, expires, nbf, privateKey, 
signatureAlgorithm, String.valueOf(UUID.randomUUID()));
+    }
+
+    public static SignedJWT getJWT(final String issuer,
+                                   final String sub,
+                                   final String aud,
+                                   final Date expires,
+                                   final Date nbf,
+                                   final RSAPrivateKey privateKey,
+                                   final String signatureAlgorithm,
+                                   final String knoxId)
+            throws Exception {
+        JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder();
+        builder.issuer(issuer)
+                .subject(sub)
+                .audience(aud)
+                .expirationTime(expires)
+                .notBeforeTime(nbf)
+                .claim("scope", "openid");
+        if (knoxId != null) {
+            builder.claim(JWTToken.KNOX_ID_CLAIM, knoxId);
+        }
+        JWTClaimsSet claims = builder.build();
+
+        JWSHeader header = new 
JWSHeader.Builder(JWSAlgorithm.parse(signatureAlgorithm)).build();
+
+        SignedJWT signedJWT = new SignedJWT(header, claims);
+        JWSSigner signer = new RSASSASigner(privateKey);
+
+        signedJWT.sign(signer);
+
+        return signedJWT;
+    }
+
+}
diff --git 
a/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/jwt/filter/SignatureVerificationCacheTest.java
 
b/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/jwt/filter/SignatureVerificationCacheTest.java
new file mode 100644
index 0000000..2033c64
--- /dev/null
+++ 
b/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/jwt/filter/SignatureVerificationCacheTest.java
@@ -0,0 +1,161 @@
+/*
+ * 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.jwt.filter;
+
+import com.nimbusds.jwt.SignedJWT;
+import org.apache.knox.gateway.provider.federation.TestFilterConfig;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.interfaces.RSAPrivateKey;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Properties;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+public class SignatureVerificationCacheTest {
+
+    private static RSAPrivateKey privateKey;
+
+    @BeforeClass
+    public static void setUpBeforeClass() throws Exception {
+        KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
+        kpg.initialize(2048);
+        KeyPair KPair = kpg.generateKeyPair();
+        privateKey = (RSAPrivateKey) KPair.getPrivate();
+    }
+
+    /**
+     * Verify that the default SignatureVerificationCache instance is returned 
when no topology is specified.
+     */
+    @Test
+    public void testSignatureVerificationCacheWithoutTopology() throws 
Exception {
+        SignatureVerificationCache cache = 
SignatureVerificationCache.getInstance(null, new TestFilterConfig());
+        assertNotNull(cache);
+        SignatureVerificationCache defaultCache =
+            
SignatureVerificationCache.getInstance(SignatureVerificationCache.DEFAULT_CACHE_ID,
 new TestFilterConfig());
+        assertNotNull(defaultCache);
+        assertEquals("Expected the default cache when no topology is 
specified.", defaultCache, cache);
+    }
+
+    /**
+     * Verify that the topology-specific SignatureVerificationCache instance 
is returned when a topology is specified.
+     */
+    @Test
+    public void testSignatureVerificationCacheForTopology() throws Exception {
+        final String topologyName = "test-topology-explicit";
+        final Properties filterProps = new Properties();
+        filterProps.setProperty(TestFilterConfig.TOPOLOGY_NAME_PROP, 
topologyName);
+        final TestFilterConfig filterConfig = new 
TestFilterConfig(filterProps);
+
+        SignatureVerificationCache ref1 = 
SignatureVerificationCache.getInstance(topologyName, filterConfig);
+        assertNotNull(ref1);
+
+        SignatureVerificationCache ref2 = 
SignatureVerificationCache.getInstance(topologyName, filterConfig);
+        assertNotNull(ref2);
+        assertEquals("Expected the same cache when the same topology is 
explicitly specified.", ref1, ref2);
+
+        SignatureVerificationCache ref3 = 
SignatureVerificationCache.getInstance(topologyName + "-2", filterConfig);
+        assertNotNull(ref3);
+        assertNotEquals("Expected a different cache when a different topology 
is explicitly specified.", ref2, ref3);
+    }
+
+    @Test
+    public void testSignatureVerificationCacheLifecycle() throws Exception {
+        final String topologyName = "test-topology-lifecycle";
+        final Properties filterProps = new Properties();
+        filterProps.setProperty(TestFilterConfig.TOPOLOGY_NAME_PROP, 
topologyName);
+        final TestFilterConfig filterConfig = new 
TestFilterConfig(filterProps);
+
+        SignatureVerificationCache cache = 
SignatureVerificationCache.getInstance(topologyName, filterConfig);
+        assertNotNull(cache);
+
+        // Create a JWT
+        SignedJWT jwt = createTestJWT();
+        String serializedJWT = jwt.serialize();
+
+        // Verify that there is not yet any signature verification record for 
this JWT
+        assertFalse("JWT signature verification should NOT have been recored 
yet.",
+                    cache.hasSignatureBeenVerified(serializedJWT));
+
+        // Record the signature verification for this JWT
+        cache.recordSignatureVerification(serializedJWT);
+        assertTrue("JWT signature verification should have been recored yet.",
+                    cache.hasSignatureBeenVerified(serializedJWT));
+
+        // Explicitly remove the signature verification record for this JWT
+        cache.removeSignatureVerificationRecord(serializedJWT);
+        assertFalse("JWT signature verification record should no longer be in 
the cache.",
+                    cache.hasSignatureBeenVerified(serializedJWT));
+    }
+
+    @Test
+    public void testSignatureVerificationCacheClear() throws Exception {
+        final int jwtCount = 5;
+        final String topologyName = "test-topology-clear";
+        final Properties filterProps = new Properties();
+        filterProps.setProperty(TestFilterConfig.TOPOLOGY_NAME_PROP, 
topologyName);
+        final TestFilterConfig filterConfig = new 
TestFilterConfig(filterProps);
+
+        SignatureVerificationCache cache = 
SignatureVerificationCache.getInstance(topologyName, filterConfig);
+        assertNotNull(cache);
+
+        // Create test JWTs
+        List<SignedJWT> testJWTs = new ArrayList<>();
+        List<String> serializedJWTs = new ArrayList<>();
+        for (int i = 0 ; i < jwtCount ; i++) {
+            testJWTs.add(createTestJWT());
+            serializedJWTs.add(testJWTs.get(i).serialize());
+        }
+
+        // Verify that there is not yet any signature verification record for 
the test JWTs
+        assertEquals("There should not yet be any records in the cache.", 0, 
cache.getSize());
+
+        // Record the signature verification for the test JWTs
+        for (int i = 0 ; i < jwtCount ; i++) {
+            cache.recordSignatureVerification(serializedJWTs.get(i));
+        }
+        assertEquals("Unexpected cache size.", jwtCount, cache.getSize());
+
+        // Explicitly remove all signature verification records from the cache
+        cache.clear();
+        assertEquals("Cache should be empty after clear() is invoked.", 0, 
cache.getSize());
+
+        // Verify that there is no longer any signature verification record 
for the test JWTs
+        for (int i = 0 ; i < jwtCount ; i++) {
+            assertFalse("JWT signature verification record should no longer be 
in the cache.",
+                        cache.hasSignatureBeenVerified(serializedJWTs.get(i)));
+        }
+    }
+
+    private SignedJWT createTestJWT() throws Exception {
+        return JWTTestUtils.getJWT(AbstractJWTFilter.JWT_DEFAULT_ISSUER,
+                                   "alice",
+                                   new Date(System.currentTimeMillis() + 5000),
+                                   privateKey);
+    }
+
+}

Reply via email to