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);
+ }
+
+}