This is an automated email from the ASF dual-hosted git repository.
janhoy pushed a commit to branch branch_9x
in repository https://gitbox.apache.org/repos/asf/solr.git
The following commit(s) were added to refs/heads/branch_9x by this push:
new 0555e7d1a1d Add test coverage for jwt-auth module (#4338)
0555e7d1a1d is described below
commit 0555e7d1a1d557ac295486aac9ecfbb482f7439e
Author: Jan Høydahl <[email protected]>
AuthorDate: Sun Apr 26 13:17:49 2026 +0200
Add test coverage for jwt-auth module (#4338)
(cherry picked from commit a7056c02e52355f0a03378a0988ccb101e101e47)
---
.../solr/security/jwt/JWTAuthPluginTest.java | 90 ++++++++++++++++++++
.../solr/security/jwt/JWTIssuerConfigTest.java | 8 ++
.../jwt/JWTVerificationkeyResolverTest.java | 95 ++++++++++++++++++++++
3 files changed, 193 insertions(+)
diff --git
a/solr/modules/jwt-auth/src/test/org/apache/solr/security/jwt/JWTAuthPluginTest.java
b/solr/modules/jwt-auth/src/test/org/apache/solr/security/jwt/JWTAuthPluginTest.java
index 7327008a24a..f871c779ed4 100644
---
a/solr/modules/jwt-auth/src/test/org/apache/solr/security/jwt/JWTAuthPluginTest.java
+++
b/solr/modules/jwt-auth/src/test/org/apache/solr/security/jwt/JWTAuthPluginTest.java
@@ -18,6 +18,7 @@ package org.apache.solr.security.jwt;
import static
org.apache.solr.security.jwt.JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.AUTZ_HEADER_PROBLEM;
import static
org.apache.solr.security.jwt.JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.CLAIM_MISMATCH;
+import static
org.apache.solr.security.jwt.JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.JWT_EXPIRED;
import static
org.apache.solr.security.jwt.JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.JWT_VALIDATION_EXCEPTION;
import static
org.apache.solr.security.jwt.JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.NO_AUTZ_HEADER;
import static
org.apache.solr.security.jwt.JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.PASS_THROUGH;
@@ -53,6 +54,7 @@ import org.jose4j.jwk.RsaJwkGenerator;
import org.jose4j.jws.AlgorithmIdentifiers;
import org.jose4j.jws.JsonWebSignature;
import org.jose4j.jwt.JwtClaims;
+import org.jose4j.jwt.NumericDate;
import org.jose4j.keys.BigEndianBigInteger;
import org.jose4j.lang.JoseException;
import org.junit.After;
@@ -747,4 +749,92 @@ public class JWTAuthPluginTest extends SolrTestCaseJ4 {
"http://acmepaymentscorp/oauth/oauth20/token",
EnvUtils.getProperty(LoadAdminUiServlet.SYSPROP_CSP_CONNECT_SRC_URLS));
}
+
+ @Test
+ public void requireIssuerFalseButIssPresentAndMismatches() {
+ // requireIssuer=false controls whether iss must be present, not whether a
mismatching value
+ // is silently accepted. A token with iss="IDServer" should fail when
iss="NA" is configured.
+ testConfig.put("iss", "NA");
+ testConfig.put("requireIss", false);
+ plugin.init(testConfig);
+ JWTAuthPlugin.JWTAuthenticationResponse resp =
plugin.authenticate(testHeader);
+ assertFalse(resp.isAuthenticated());
+ assertEquals(JWT_VALIDATION_EXCEPTION, resp.getAuthCode());
+ }
+
+ @Test
+ public void requireIssuerFalseNoIssInTokenOrConfig() {
+ // requireIssuer=false with no iss claim in token and no iss in config →
authenticated
+ testConfig.put("requireIss", false);
+ testConfig.put("requireExp", false);
+ plugin.init(testConfig);
+ JWTAuthPlugin.JWTAuthenticationResponse resp =
plugin.authenticate(slimHeader);
+ assertTrue(resp.getErrorMessage(), resp.isAuthenticated());
+ }
+
+ @Test
+ public void scopeClaimAsJsonArray() throws Exception {
+ // Verify that a scope claim expressed as a JSON array (not just a
whitespace-separated String)
+ // is correctly parsed: authentication succeeds and "openid" is filtered
out of the roles.
+ JwtClaims claims = generateClaims();
+ claims.setClaim("scope", Arrays.asList("solr:read", "openid"));
+ JsonWebSignature jws = new JsonWebSignature();
+ jws.setPayload(claims.toJson());
+ jws.setKey(rsaJsonWebKey.getPrivateKey());
+ jws.setKeyIdHeaderValue(rsaJsonWebKey.getKeyId());
+ jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.RSA_USING_SHA256);
+ String header = "Bearer " + jws.getCompactSerialization();
+
+ testConfig.put("scope", "solr:read");
+ plugin.init(testConfig);
+ JWTAuthPlugin.JWTAuthenticationResponse resp = plugin.authenticate(header);
+ assertTrue(resp.getErrorMessage(), resp.isAuthenticated());
+ Set<String> roles = ((VerifiedUserRoles)
resp.getPrincipal()).getVerifiedRoles();
+ assertTrue(roles.contains("solr:read"));
+ assertFalse("openid should be filtered from roles",
roles.contains("openid"));
+ }
+
+ @Test
+ public void tokenExpiredWithinClockSkewIsAuthenticated() throws Exception {
+ // Token expired 25 seconds ago — within the 30-second clock skew
tolerance.
+ // All timestamps must be consistent: iat < exp, so iat is set 90 seconds
in the past.
+ NumericDate now = NumericDate.now();
+ JwtClaims claims = new JwtClaims();
+ claims.setIssuer("IDServer");
+ claims.setClaim("customPrincipal", "custom");
+ claims.setIssuedAt(NumericDate.fromSeconds(now.getValue() - 90));
+ claims.setNotBefore(NumericDate.fromSeconds(now.getValue() - 90));
+ claims.setExpirationTime(NumericDate.fromSeconds(now.getValue() - 25));
+ JsonWebSignature jws = new JsonWebSignature();
+ jws.setPayload(claims.toJson());
+ jws.setKey(rsaJsonWebKey.getPrivateKey());
+ jws.setKeyIdHeaderValue(rsaJsonWebKey.getKeyId());
+ jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.RSA_USING_SHA256);
+ String header = "Bearer " + jws.getCompactSerialization();
+
+ JWTAuthPlugin.JWTAuthenticationResponse resp = plugin.authenticate(header);
+ assertTrue(resp.getErrorMessage(), resp.isAuthenticated());
+ }
+
+ @Test
+ public void tokenExpiredBeyondClockSkewIsRejected() throws Exception {
+ // Token expired 35 seconds ago — beyond the 30-second clock skew
tolerance.
+ NumericDate now = NumericDate.now();
+ JwtClaims claims = new JwtClaims();
+ claims.setIssuer("IDServer");
+ claims.setClaim("customPrincipal", "custom");
+ claims.setIssuedAt(NumericDate.fromSeconds(now.getValue() - 90));
+ claims.setNotBefore(NumericDate.fromSeconds(now.getValue() - 90));
+ claims.setExpirationTime(NumericDate.fromSeconds(now.getValue() - 35));
+ JsonWebSignature jws = new JsonWebSignature();
+ jws.setPayload(claims.toJson());
+ jws.setKey(rsaJsonWebKey.getPrivateKey());
+ jws.setKeyIdHeaderValue(rsaJsonWebKey.getKeyId());
+ jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.RSA_USING_SHA256);
+ String header = "Bearer " + jws.getCompactSerialization();
+
+ JWTAuthPlugin.JWTAuthenticationResponse resp = plugin.authenticate(header);
+ assertFalse(resp.isAuthenticated());
+ assertEquals(JWT_EXPIRED, resp.getAuthCode());
+ }
}
diff --git
a/solr/modules/jwt-auth/src/test/org/apache/solr/security/jwt/JWTIssuerConfigTest.java
b/solr/modules/jwt-auth/src/test/org/apache/solr/security/jwt/JWTIssuerConfigTest.java
index 6416b60c61c..2f25397de81 100644
---
a/solr/modules/jwt-auth/src/test/org/apache/solr/security/jwt/JWTIssuerConfigTest.java
+++
b/solr/modules/jwt-auth/src/test/org/apache/solr/security/jwt/JWTIssuerConfigTest.java
@@ -242,4 +242,12 @@ public class JWTIssuerConfigTest extends SolrTestCase {
"Well-known config could not be read from url
https://127.0.0.1:45678/.well-known/config",
e.getMessage());
}
+
+ @Test
+ public void parseJwkSetSingleBareJwk() throws Exception {
+ // testJwk is a bare JWK map (no "keys" wrapper) — exercises the
single-JWK branch
+ JsonWebKeySet result = JWTIssuerConfig.parseJwkSet(testJwk);
+ assertEquals(1, result.getJsonWebKeys().size());
+ assertEquals("k1", result.getJsonWebKeys().get(0).getKeyId());
+ }
}
diff --git
a/solr/modules/jwt-auth/src/test/org/apache/solr/security/jwt/JWTVerificationkeyResolverTest.java
b/solr/modules/jwt-auth/src/test/org/apache/solr/security/jwt/JWTVerificationkeyResolverTest.java
index 3406e439dbb..cc3d3ad253e 100644
---
a/solr/modules/jwt-auth/src/test/org/apache/solr/security/jwt/JWTVerificationkeyResolverTest.java
+++
b/solr/modules/jwt-auth/src/test/org/apache/solr/security/jwt/JWTVerificationkeyResolverTest.java
@@ -21,17 +21,25 @@ import static java.util.Arrays.asList;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.when;
+import java.security.Key;
+import java.security.interfaces.ECPublicKey;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.common.SolrException;
import org.apache.solr.security.jwt.JWTIssuerConfig.HttpsJwksFactory;
+import org.jose4j.jwk.EcJwkGenerator;
+import org.jose4j.jwk.EllipticCurveJsonWebKey;
import org.jose4j.jwk.HttpsJwks;
import org.jose4j.jwk.JsonWebKey;
+import org.jose4j.jwk.JsonWebKeySet;
import org.jose4j.jwk.RsaJsonWebKey;
import org.jose4j.jwk.RsaJwkGenerator;
import org.jose4j.jws.AlgorithmIdentifiers;
import org.jose4j.jws.JsonWebSignature;
+import org.jose4j.jwt.JwtClaims;
+import org.jose4j.keys.EllipticCurves;
import org.jose4j.lang.JoseException;
import org.jose4j.lang.UnresolvableKeyException;
import org.junit.Before;
@@ -118,6 +126,93 @@ public class JWTVerificationkeyResolverTest extends
SolrTestCaseJ4 {
resolver.resolveKey(k5.getJws(), null);
}
+ @Test
+ public void noIssRequireIssuerFalseSingleIssuerFallback() throws Exception {
+ // null iss, requireIssuer=false, single issuer → falls back to that issuer
+
when(httpsJwksFactory.createList(ArgumentMatchers.anyList())).thenReturn(asList(firstJwkList));
+ JWTIssuerConfig singleIssuerConfig = new
JWTIssuerConfig("single").setJwksUrl(asList("url1"));
+ resolver = new
JWTVerificationkeyResolver(Arrays.asList(singleIssuerConfig), false);
+
+ Key key = resolver.resolveKey(makeJws(k1, claimsWithNoIss()), null);
+ assertNotNull(key);
+ }
+
+ @Test(expected = SolrException.class)
+ public void noIssRequireIssuerFalseMultipleIssuersThrows() throws Exception {
+ // null iss, requireIssuer=false, multiple issuers → SolrException
(ambiguous)
+ JWTIssuerConfig iss1 = new
JWTIssuerConfig("iss1").setIss("A").setJwksUrl(asList("url1"));
+ JWTIssuerConfig iss2 = new
JWTIssuerConfig("iss2").setIss("B").setJwksUrl(asList("url2"));
+ resolver = new JWTVerificationkeyResolver(Arrays.asList(iss1, iss2),
false);
+ resolver.resolveKey(makeJws(k1, claimsWithNoIss()), null);
+ }
+
+ @Test
+ public void issMismatchSingleIssuerBackCompatFallback() throws Exception {
+ // iss present but unrecognised, single issuer → back-compat fallback to
that issuer
+
when(httpsJwksFactory.createList(ArgumentMatchers.anyList())).thenReturn(asList(firstJwkList));
+ JWTIssuerConfig singleIssuerConfig =
+ new JWTIssuerConfig("single").setIss("A").setJwksUrl(asList("url1"));
+ resolver = new
JWTVerificationkeyResolver(Arrays.asList(singleIssuerConfig), true);
+
+ Key key = resolver.resolveKey(makeJws(k1, claimsWithIss("UNKNOWN")), null);
+ assertNotNull(key);
+ }
+
+ @Test(expected = UnresolvableKeyException.class)
+ public void issMismatchMultipleIssuersThrows() throws Exception {
+ // iss present but unrecognised, multiple issuers →
UnresolvableKeyException
+ JWTIssuerConfig iss1 = new
JWTIssuerConfig("iss1").setIss("A").setJwksUrl(asList("url1"));
+ JWTIssuerConfig iss2 = new
JWTIssuerConfig("iss2").setIss("B").setJwksUrl(asList("url2"));
+ resolver = new JWTVerificationkeyResolver(Arrays.asList(iss1, iss2), true);
+ resolver.resolveKey(makeJws(k1, claimsWithIss("UNKNOWN")), null);
+ }
+
+ @Test
+ public void ecKeyTypeMaterialisedCorrectly() throws Exception {
+ // EC key type should be returned as ECPublicKey, not RSAPublicKey
+ EllipticCurveJsonWebKey ecKey =
EcJwkGenerator.generateJwk(EllipticCurves.P256);
+ ecKey.setKeyId("ec1");
+ JsonWebKey ecPublicKey = JsonWebKey.Factory.newJwk(ecKey.getECPublicKey());
+ ecPublicKey.setKeyId("ec1");
+ JWTIssuerConfig ecIssuerConfig =
+ new JWTIssuerConfig("ec-issuer")
+ .setIss("ec-iss")
+ .setJsonWebKeySet(new JsonWebKeySet(ecPublicKey));
+ resolver = new JWTVerificationkeyResolver(Arrays.asList(ecIssuerConfig),
false);
+
+ JsonWebSignature ecJws = new JsonWebSignature();
+ ecJws.setPayload(claimsWithIss("ec-iss").toJson());
+ ecJws.setKey(ecKey.getPrivateKey());
+ ecJws.setKeyIdHeaderValue("ec1");
+
ecJws.setAlgorithmHeaderValue(AlgorithmIdentifiers.ECDSA_USING_P256_CURVE_AND_SHA256);
+
+ Key key = resolver.resolveKey(ecJws, null);
+ assertNotNull(key);
+ assertTrue(key instanceof ECPublicKey);
+ }
+
+ private static JwtClaims claimsWithNoIss() {
+ JwtClaims claims = new JwtClaims();
+ claims.setExpirationTimeMinutesInTheFuture(10);
+ return claims;
+ }
+
+ private static JwtClaims claimsWithIss(String iss) {
+ JwtClaims claims = claimsWithNoIss();
+ claims.setIssuer(iss);
+ return claims;
+ }
+
+ private static JsonWebSignature makeJws(KeyHolder keyHolder, JwtClaims
claims)
+ throws JoseException {
+ JsonWebSignature jws = new JsonWebSignature();
+ jws.setPayload(claims.toJson());
+ jws.setKey(keyHolder.getRsaKey().getPrivateKey());
+ jws.setKeyIdHeaderValue(keyHolder.getRsaKey().getKeyId());
+ jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.RSA_USING_SHA256);
+ return jws;
+ }
+
@SuppressWarnings("NewClassNamingConvention")
public static class KeyHolder {
private final RsaJsonWebKey key;