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