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 281f3a589 KNOX-3052: Allow Multiple Issuers and JWTs with no Audience 
in same Topology as Others (#1006)
281f3a589 is described below

commit 281f3a589bd22b5d012e10e38c5016936b9fa8f9
Author: Phil Zampino <[email protected]>
AuthorDate: Wed Mar 19 21:44:47 2025 -0400

    KNOX-3052: Allow Multiple Issuers and JWTs with no Audience in same 
Topology as Others (#1006)
---
 .../federation/jwt/filter/AbstractJWTFilter.java   |  27 +++-
 .../provider/federation/AbstractJWTFilterTest.java | 154 +++++++++++++++++++++
 ...okenIDAsHTTPBasicCredsFederationFilterTest.java |  10 ++
 3 files changed, 186 insertions(+), 5 deletions(-)

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 5668066a4..60ebd8a9c 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
@@ -35,6 +35,7 @@ import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Locale;
 import java.util.Set;
+import java.util.stream.Collectors;
 
 import javax.security.auth.Subject;
 import javax.servlet.Filter;
@@ -117,7 +118,7 @@ public abstract class AbstractJWTFilter implements Filter {
   protected JWTokenAuthority authority;
   protected RSAPublicKey publicKey;
   protected SignatureVerificationCache signatureVerificationCache;
-  private String expectedIssuer;
+  private List<String> expectedIssuers;
   private String expectedSigAlg;
   protected String expectedPrincipalClaim;
   protected Set<URI> expectedJWKSUrls = new LinkedHashSet();
@@ -169,10 +170,11 @@ public abstract class AbstractJWTFilter implements Filter 
{
   }
 
   protected void configureExpectedParameters(FilterConfig filterConfig) {
-    expectedIssuer = filterConfig.getInitParameter(JWT_EXPECTED_ISSUER);
-    if (expectedIssuer == null) {
-      expectedIssuer = JWT_DEFAULT_ISSUER;
+    String expectedIssuersParam = 
filterConfig.getInitParameter(JWT_EXPECTED_ISSUER);
+    if (expectedIssuersParam == null) {
+      expectedIssuersParam = JWT_DEFAULT_ISSUER;
     }
+    expectedIssuers = parseToListOfStrings(expectedIssuersParam);
 
     expectedSigAlg = filterConfig.getInitParameter(JWT_EXPECTED_SIGALG);
     if(StringUtils.isBlank(expectedSigAlg)) {
@@ -180,6 +182,18 @@ public abstract class AbstractJWTFilter implements Filter {
     }
   }
 
+  /**
+   * Helper function to extract a List<String> from a comma separated
+   * String param.
+   * @param commaSeparatedList string to parse into a List<String>
+   */
+  protected List<String> parseToListOfStrings(final String commaSeparatedList) 
{
+    return Arrays.stream(
+                    commaSeparatedList.split(","))
+            .map(String::trim)
+            .collect(Collectors.toList());
+  }
+
   protected List<String> parseExpectedAudiences(String expectedAudiences) {
     List<String> audList = null;
     // setup the list of valid audiences for token validation
@@ -249,6 +263,9 @@ public abstract class AbstractJWTFilter implements Filter {
             break;
           }
         }
+      } else if (audiences.contains("NONE")) {
+        log.jwtAudienceValidated();
+        valid = true;
       }
     }
     return valid;
@@ -359,7 +376,7 @@ public abstract class AbstractJWTFilter implements Filter {
     final String displayableTokenId = Tokens.getTokenIDDisplayText(tokenId);
     final String displayableToken = 
Tokens.getTokenDisplayText(token.toString());
     // confirm that issuer matches the intended target
-    if (expectedIssuer.equals(token.getIssuer())) {
+    if (expectedIssuers.contains(token.getIssuer())) {
       // if there is no expiration data then the lifecycle is tied entirely to
       // the cookie validity - otherwise ensure that the current time is before
       // the designated expiration time
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 0b2dcbc7a..6ad8b7d54 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
@@ -125,6 +125,7 @@ public abstract class AbstractJWTFilterTest  {
     }
 
     handler.destroy();
+    handler = null;
   }
 
   /**
@@ -234,6 +235,96 @@ public abstract class AbstractJWTFilterTest  {
     }
   }
 
+  @Test
+  public void testValidAudienceJWTWithNONEAllowed() throws Exception {
+    try {
+      Properties props = getProperties();
+      props.put(getAudienceProperty(), "bar, NONE");
+      handler.init(new TestFilterConfig(props));
+
+      SignedJWT jwt = getJWT(AbstractJWTFilter.JWT_DEFAULT_ISSUER, "alice",
+              null, new Date(new Date().getTime() + 5000));
+      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 not be false.", 
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 testValidAudienceJWTWithNONEAllowedButWithBar() throws Exception 
{
+    try {
+      Properties props = getProperties();
+      props.put(getAudienceProperty(), "bar, NONE");
+      handler.init(new TestFilterConfig(props));
+
+      SignedJWT jwt = getJWT(AbstractJWTFilter.JWT_DEFAULT_ISSUER, "alice",
+              "bar", new Date(new Date().getTime() + 5000));
+      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 not be false.", 
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 testInvalidAudienceJWTWithNONENotAllowed() throws Exception {
+    try {
+      Properties props = getProperties();
+      props.put(getAudienceProperty(), "bar");
+      handler.init(new TestFilterConfig(props));
+
+      SignedJWT jwt = getJWT(AbstractJWTFilter.JWT_DEFAULT_ISSUER, "alice",
+              null, new Date(new Date().getTime() + 5000));
+      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.assertFalse("doFilterCalled should not be true.", 
chain.doFilterCalled );
+    } catch (ServletException se) {
+      fail("Should NOT have thrown a ServletException.");
+    }
+  }
+
   @Test
   public void testInvalidAudienceJWT() throws Exception {
     try {
@@ -896,6 +987,65 @@ public abstract class AbstractJWTFilterTest  {
     }
   }
 
+  @Test
+  public void testValidIssuerViaConfigWithTwoIssuers() throws Exception {
+    try {
+      Properties props = getProperties();
+      props.setProperty(AbstractJWTFilter.JWT_EXPECTED_ISSUER, "new-issuer, 
KNOXSSO");
+      handler.init(new TestFilterConfig(props));
+
+      SignedJWT jwt = getJWT("KNOXSSO", "alice", new Date(new Date().getTime() 
+ 5000), 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 not be false.", 
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 testInvalidIssuerViaConfigWithTwoIssuers() throws Exception {
+    try {
+      Properties props = getProperties();
+      props.setProperty(AbstractJWTFilter.JWT_EXPECTED_ISSUER, "new-issuer, 
KNOXSSO");
+      handler.init(new TestFilterConfig(props));
+
+      SignedJWT jwt = getJWT("fake-issuer", "alice", new Date(new 
Date().getTime() + 5000), 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.assertFalse("doFilterCalled should not be true.", 
chain.doFilterCalled);
+    } catch (ServletException se) {
+      fail("Should NOT have thrown a ServletException.");
+    }
+  }
+
   @Test
   public void testRS512SignatureAlgorithm() throws Exception {
     try {
@@ -1233,6 +1383,10 @@ public abstract class AbstractJWTFilterTest  {
     return getJWT(issuer, sub, expires, privateKey);
   }
 
+  protected SignedJWT getJWT(String issuer, String sub, String aud, Date 
expires) throws Exception {
+    return getJWT(issuer, sub, aud, expires, null, privateKey, 
JWSAlgorithm.RS256.getName());
+  }
+
   protected SignedJWT getJWT(String issuer, String sub, Date expires, 
RSAPrivateKey privateKey)
       throws Exception {
     return getJWT(issuer, sub, expires, new Date(), privateKey, 
JWSAlgorithm.RS256.getName());
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 65a48cf53..17f6fd53b 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
@@ -351,6 +351,16 @@ public class TokenIDAsHTTPBasicCredsFederationFilterTest 
extends JWTAsHTTPBasicC
         // Override to disable N/A test
     }
 
+    @Override
+    public void testInvalidAudienceJWTWithNONENotAllowed() throws Exception {
+        // Override to disable N/A test
+    }
+
+    @Override
+    public void testInvalidIssuerViaConfigWithTwoIssuers() throws Exception {
+        // Override to disable N/A test
+    }
+
     /**
      * Very basic TokenStateService implementation for these tests only
      */

Reply via email to