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;

Reply via email to