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
*/