This is an automated email from the ASF dual-hosted git repository.
more 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 7980cda85 KNOX-3040 - Support multiple ways to verify JWT tokens (#915)
7980cda85 is described below
commit 7980cda85d9db922490016e995577c2f5774654b
Author: Sandeep Moré <[email protected]>
AuthorDate: Fri Jun 7 19:47:41 2024 -0400
KNOX-3040 - Support multiple ways to verify JWT tokens (#915)
* KNOX-3040 - Support multiple ways to verify JWT tokens
* KNOX-3040 - 1) update default algorithm for JWKS 2) Fix unittests
---
.../provider/federation/jwt/JWTMessages.java | 8 +++
.../federation/jwt/filter/AbstractJWTFilter.java | 15 ++++-
.../provider/federation/AbstractJWTFilterTest.java | 68 +++++++++++++++++++++-
.../knox/gateway/hbase/HBaseDispatchTest.java | 4 +-
4 files changed, 89 insertions(+), 6 deletions(-)
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 fb72d9bcb..38604aa6e 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
@@ -115,4 +115,12 @@ public interface JWTMessages {
@Message(level = MessageLevel.ERROR, text = "Error while fetching grant type
and client secret from the request: {0}")
void errorFetchingClientSecret(String errorMessage, @StackTrace(level =
MessageLevel.DEBUG) Exception e);
+ @Message( level = MessageLevel.INFO, text = "Token verification using
provided PEM, verified: {0}" )
+ void publicKeyVerification(boolean verified);
+
+ @Message( level = MessageLevel.INFO, text = "Token verification using
provided JWKS Url, verified: {0}" )
+ void jwksVerification(boolean verified);
+
+ @Message( level = MessageLevel.INFO, text = "Token verification using knox
signing cert, verified: {0}" )
+ void signingKeyVerification(boolean verified);
}
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 e372e0d42..81d6ae5e4 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
@@ -45,6 +45,7 @@ import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
+import org.apache.commons.lang3.StringUtils;
import org.apache.knox.gateway.audit.api.Action;
import org.apache.knox.gateway.audit.api.ActionOutcome;
import org.apache.knox.gateway.audit.api.AuditContext;
@@ -165,6 +166,9 @@ public abstract class AbstractJWTFilter implements Filter {
}
expectedSigAlg = filterConfig.getInitParameter(JWT_EXPECTED_SIGALG);
+ if(StringUtils.isBlank(expectedSigAlg)) {
+ expectedSigAlg = JWT_DEFAULT_SIGALG;
+ }
}
protected List<String> parseExpectedAudiences(String expectedAudiences) {
@@ -509,10 +513,17 @@ public abstract class AbstractJWTFilter implements Filter
{
try {
if (publicKey != null) {
verified = authority.verifyToken(token, publicKey);
- } else if (expectedJWKSUrl != null) {
+ log.publicKeyVerification(verified);
+ }
+
+ if (!verified && expectedJWKSUrl != null) {
verified = authority.verifyToken(token, expectedJWKSUrl,
expectedSigAlg, allowedJwsTypes);
- } else {
+ log.jwksVerification(verified);
+ }
+
+ if(!verified) {
verified = authority.verifyToken(token);
+ log.signingKeyVerification(verified);
}
} catch (TokenServiceException e) {
log.unableToVerifyToken(e);
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 ffd99444b..73ec4c35b 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
@@ -551,6 +551,8 @@ public abstract class AbstractJWTFilterTest {
@Test
public void testInvalidVerificationPEM() throws Exception {
try {
+
+
Properties props = getProperties();
KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
@@ -565,7 +567,12 @@ public abstract class AbstractJWTFilterTest {
props.put(getAudienceProperty(), "bar");
props.put(getVerificationPemProperty(), failingPem);
- handler.init(new TestFilterConfig(props));
+
+ /* Create a new handler that has different public key then the token
signing key */
+ final AbstractJWTFilter handler_new = new TestJWTFederationFilter();
+ ((TestJWTFederationFilter) handler_new).setTokenService(new
TestJWTokenAuthority(KPair.getPublic()));
+
+ handler_new.init(new TestFilterConfig(props));
SignedJWT jwt = getJWT(AbstractJWTFilter.JWT_DEFAULT_ISSUER, "alice",
new Date(new Date().getTime() +
TimeUnit.MINUTES.toMillis(10)), privateKey);
@@ -582,7 +589,7 @@ public abstract class AbstractJWTFilterTest {
EasyMock.replay(request, response);
TestFilterChain chain = new TestFilterChain();
- handler.doFilter(request, response, chain);
+ handler_new.doFilter(request, response, chain);
Assert.assertFalse("doFilterCalled should not be true.",
chain.doFilterCalled);
Assert.assertNull("No Subject should be returned.", chain.subject);
} catch (ServletException se) {
@@ -590,6 +597,63 @@ public abstract class AbstractJWTFilterTest {
}
}
+ /**
+ * 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
+ */
+ @Test
+ public void testSignatureVerificationChain() throws Exception {
+ try {
+
+
+ Properties props = getProperties();
+
+ 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 ) );
+ String failingPem = new String(encoder.encodeToString( data ).getBytes(
StandardCharsets.US_ASCII ), StandardCharsets.US_ASCII).trim();
+
+ props.put(getAudienceProperty(), "bar");
+ /* Add a failing PEN */
+ props.put(getVerificationPemProperty(), failingPem);
+
+ /* This handler is setup with a publicKey, corresponding privateKey is
used to sign tje 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);
+ Assert.assertTrue("doFilterCalled should be true.",
chain.doFilterCalled);
+
+ Set<PrimaryPrincipal> principals =
chain.subject.getPrincipals(PrimaryPrincipal.class);
+ Assert.assertFalse("No PrimaryPrincipal", principals.isEmpty());
+ Assert.assertEquals("Not the expected principal", "alice",
((Principal)principals.toArray()[0]).getName());
+ } catch (ServletException se) {
+ fail("Should NOT have thrown a ServletException.");
+ }
+ }
+
@Test
public void testInvalidIssuer() throws Exception {
try {
diff --git
a/gateway-service-hbase/src/test/java/org/apache/knox/gateway/hbase/HBaseDispatchTest.java
b/gateway-service-hbase/src/test/java/org/apache/knox/gateway/hbase/HBaseDispatchTest.java
index 0345fc9cd..04cd5187e 100644
---
a/gateway-service-hbase/src/test/java/org/apache/knox/gateway/hbase/HBaseDispatchTest.java
+++
b/gateway-service-hbase/src/test/java/org/apache/knox/gateway/hbase/HBaseDispatchTest.java
@@ -35,7 +35,7 @@ import static org.hamcrest.MatcherAssert.assertThat;
public class HBaseDispatchTest {
@SuppressWarnings("deprecation")
- @Test( timeout = TestUtils.SHORT_TIMEOUT )
+ @Test( timeout = TestUtils.LONG_TIMEOUT )
public void testGetDispatchUrl() throws Exception {
HttpServletRequest request;
Dispatch dispatch;
@@ -84,4 +84,4 @@ public class HBaseDispatchTest {
uri = dispatch.getDispatchUrl( request );
assertThat( uri.toASCIIString(), is(
"http://test-host:42/test,path?test%26name=test%3Dvalue" ) );
}
-}
\ No newline at end of file
+}