Skip to content

Commit a7056c0

Browse files
authored
Add test coverage for jwt-auth module (#4338)
1 parent e2a1819 commit a7056c0

3 files changed

Lines changed: 193 additions & 0 deletions

File tree

solr/modules/jwt-auth/src/test/org/apache/solr/security/jwt/JWTAuthPluginTest.java

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import static org.apache.solr.security.jwt.JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.AUTZ_HEADER_PROBLEM;
2020
import static org.apache.solr.security.jwt.JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.CLAIM_MISMATCH;
21+
import static org.apache.solr.security.jwt.JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.JWT_EXPIRED;
2122
import static org.apache.solr.security.jwt.JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.JWT_VALIDATION_EXCEPTION;
2223
import static org.apache.solr.security.jwt.JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.NO_AUTZ_HEADER;
2324
import static org.apache.solr.security.jwt.JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.PASS_THROUGH;
@@ -52,6 +53,7 @@
5253
import org.jose4j.jws.AlgorithmIdentifiers;
5354
import org.jose4j.jws.JsonWebSignature;
5455
import org.jose4j.jwt.JwtClaims;
56+
import org.jose4j.jwt.NumericDate;
5557
import org.jose4j.keys.BigEndianBigInteger;
5658
import org.jose4j.lang.JoseException;
5759
import org.junit.After;
@@ -746,4 +748,92 @@ public void testRegisterTokenEndpointForCsp() {
746748
"http://acmepaymentscorp/oauth/oauth20/token",
747749
EnvUtils.getProperty(LoadAdminUiServlet.SYSPROP_CSP_CONNECT_SRC_URLS));
748750
}
751+
752+
@Test
753+
public void requireIssuerFalseButIssPresentAndMismatches() {
754+
// requireIssuer=false controls whether iss must be present, not whether a mismatching value
755+
// is silently accepted. A token with iss="IDServer" should fail when iss="NA" is configured.
756+
testConfig.put("iss", "NA");
757+
testConfig.put("requireIss", false);
758+
plugin.init(testConfig);
759+
JWTAuthPlugin.JWTAuthenticationResponse resp = plugin.authenticate(testHeader);
760+
assertFalse(resp.isAuthenticated());
761+
assertEquals(JWT_VALIDATION_EXCEPTION, resp.getAuthCode());
762+
}
763+
764+
@Test
765+
public void requireIssuerFalseNoIssInTokenOrConfig() {
766+
// requireIssuer=false with no iss claim in token and no iss in config → authenticated
767+
testConfig.put("requireIss", false);
768+
testConfig.put("requireExp", false);
769+
plugin.init(testConfig);
770+
JWTAuthPlugin.JWTAuthenticationResponse resp = plugin.authenticate(slimHeader);
771+
assertTrue(resp.getErrorMessage(), resp.isAuthenticated());
772+
}
773+
774+
@Test
775+
public void scopeClaimAsJsonArray() throws Exception {
776+
// Verify that a scope claim expressed as a JSON array (not just a whitespace-separated String)
777+
// is correctly parsed: authentication succeeds and "openid" is filtered out of the roles.
778+
JwtClaims claims = generateClaims();
779+
claims.setClaim("scope", Arrays.asList("solr:read", "openid"));
780+
JsonWebSignature jws = new JsonWebSignature();
781+
jws.setPayload(claims.toJson());
782+
jws.setKey(rsaJsonWebKey.getPrivateKey());
783+
jws.setKeyIdHeaderValue(rsaJsonWebKey.getKeyId());
784+
jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.RSA_USING_SHA256);
785+
String header = "Bearer " + jws.getCompactSerialization();
786+
787+
testConfig.put("scope", "solr:read");
788+
plugin.init(testConfig);
789+
JWTAuthPlugin.JWTAuthenticationResponse resp = plugin.authenticate(header);
790+
assertTrue(resp.getErrorMessage(), resp.isAuthenticated());
791+
Set<String> roles = ((VerifiedUserRoles) resp.getPrincipal()).getVerifiedRoles();
792+
assertTrue(roles.contains("solr:read"));
793+
assertFalse("openid should be filtered from roles", roles.contains("openid"));
794+
}
795+
796+
@Test
797+
public void tokenExpiredWithinClockSkewIsAuthenticated() throws Exception {
798+
// Token expired 25 seconds ago — within the 30-second clock skew tolerance.
799+
// All timestamps must be consistent: iat < exp, so iat is set 90 seconds in the past.
800+
NumericDate now = NumericDate.now();
801+
JwtClaims claims = new JwtClaims();
802+
claims.setIssuer("IDServer");
803+
claims.setClaim("customPrincipal", "custom");
804+
claims.setIssuedAt(NumericDate.fromSeconds(now.getValue() - 90));
805+
claims.setNotBefore(NumericDate.fromSeconds(now.getValue() - 90));
806+
claims.setExpirationTime(NumericDate.fromSeconds(now.getValue() - 25));
807+
JsonWebSignature jws = new JsonWebSignature();
808+
jws.setPayload(claims.toJson());
809+
jws.setKey(rsaJsonWebKey.getPrivateKey());
810+
jws.setKeyIdHeaderValue(rsaJsonWebKey.getKeyId());
811+
jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.RSA_USING_SHA256);
812+
String header = "Bearer " + jws.getCompactSerialization();
813+
814+
JWTAuthPlugin.JWTAuthenticationResponse resp = plugin.authenticate(header);
815+
assertTrue(resp.getErrorMessage(), resp.isAuthenticated());
816+
}
817+
818+
@Test
819+
public void tokenExpiredBeyondClockSkewIsRejected() throws Exception {
820+
// Token expired 35 seconds ago — beyond the 30-second clock skew tolerance.
821+
NumericDate now = NumericDate.now();
822+
JwtClaims claims = new JwtClaims();
823+
claims.setIssuer("IDServer");
824+
claims.setClaim("customPrincipal", "custom");
825+
claims.setIssuedAt(NumericDate.fromSeconds(now.getValue() - 90));
826+
claims.setNotBefore(NumericDate.fromSeconds(now.getValue() - 90));
827+
claims.setExpirationTime(NumericDate.fromSeconds(now.getValue() - 35));
828+
JsonWebSignature jws = new JsonWebSignature();
829+
jws.setPayload(claims.toJson());
830+
jws.setKey(rsaJsonWebKey.getPrivateKey());
831+
jws.setKeyIdHeaderValue(rsaJsonWebKey.getKeyId());
832+
jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.RSA_USING_SHA256);
833+
String header = "Bearer " + jws.getCompactSerialization();
834+
835+
JWTAuthPlugin.JWTAuthenticationResponse resp = plugin.authenticate(header);
836+
assertFalse(resp.isAuthenticated());
837+
assertEquals(JWT_EXPIRED, resp.getAuthCode());
838+
}
749839
}

solr/modules/jwt-auth/src/test/org/apache/solr/security/jwt/JWTIssuerConfigTest.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,4 +242,12 @@ public void wellKnownConfigNotReachable() {
242242
"Well-known config could not be read from url https://127.0.0.1:45678/.well-known/config",
243243
e.getMessage());
244244
}
245+
246+
@Test
247+
public void parseJwkSetSingleBareJwk() throws Exception {
248+
// testJwk is a bare JWK map (no "keys" wrapper) — exercises the single-JWK branch
249+
JsonWebKeySet result = JWTIssuerConfig.parseJwkSet(testJwk);
250+
assertEquals(1, result.getJsonWebKeys().size());
251+
assertEquals("k1", result.getJsonWebKeys().get(0).getKeyId());
252+
}
245253
}

solr/modules/jwt-auth/src/test/org/apache/solr/security/jwt/JWTVerificationkeyResolverTest.java

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,25 @@
2121
import static org.mockito.Mockito.doAnswer;
2222
import static org.mockito.Mockito.when;
2323

24+
import java.security.Key;
25+
import java.security.interfaces.ECPublicKey;
2426
import java.util.Arrays;
2527
import java.util.Iterator;
2628
import java.util.List;
2729
import org.apache.solr.SolrTestCaseJ4;
30+
import org.apache.solr.common.SolrException;
2831
import org.apache.solr.security.jwt.JWTIssuerConfig.HttpsJwksFactory;
32+
import org.jose4j.jwk.EcJwkGenerator;
33+
import org.jose4j.jwk.EllipticCurveJsonWebKey;
2934
import org.jose4j.jwk.HttpsJwks;
3035
import org.jose4j.jwk.JsonWebKey;
36+
import org.jose4j.jwk.JsonWebKeySet;
3137
import org.jose4j.jwk.RsaJsonWebKey;
3238
import org.jose4j.jwk.RsaJwkGenerator;
3339
import org.jose4j.jws.AlgorithmIdentifiers;
3440
import org.jose4j.jws.JsonWebSignature;
41+
import org.jose4j.jwt.JwtClaims;
42+
import org.jose4j.keys.EllipticCurves;
3543
import org.jose4j.lang.JoseException;
3644
import org.jose4j.lang.UnresolvableKeyException;
3745
import org.junit.Before;
@@ -118,6 +126,93 @@ public void notFoundKey() throws JoseException {
118126
resolver.resolveKey(k5.getJws(), null);
119127
}
120128

129+
@Test
130+
public void noIssRequireIssuerFalseSingleIssuerFallback() throws Exception {
131+
// null iss, requireIssuer=false, single issuer → falls back to that issuer
132+
when(httpsJwksFactory.createList(ArgumentMatchers.anyList())).thenReturn(asList(firstJwkList));
133+
JWTIssuerConfig singleIssuerConfig = new JWTIssuerConfig("single").setJwksUrl(asList("url1"));
134+
resolver = new JWTVerificationkeyResolver(Arrays.asList(singleIssuerConfig), false);
135+
136+
Key key = resolver.resolveKey(makeJws(k1, claimsWithNoIss()), null);
137+
assertNotNull(key);
138+
}
139+
140+
@Test(expected = SolrException.class)
141+
public void noIssRequireIssuerFalseMultipleIssuersThrows() throws Exception {
142+
// null iss, requireIssuer=false, multiple issuers → SolrException (ambiguous)
143+
JWTIssuerConfig iss1 = new JWTIssuerConfig("iss1").setIss("A").setJwksUrl(asList("url1"));
144+
JWTIssuerConfig iss2 = new JWTIssuerConfig("iss2").setIss("B").setJwksUrl(asList("url2"));
145+
resolver = new JWTVerificationkeyResolver(Arrays.asList(iss1, iss2), false);
146+
resolver.resolveKey(makeJws(k1, claimsWithNoIss()), null);
147+
}
148+
149+
@Test
150+
public void issMismatchSingleIssuerBackCompatFallback() throws Exception {
151+
// iss present but unrecognised, single issuer → back-compat fallback to that issuer
152+
when(httpsJwksFactory.createList(ArgumentMatchers.anyList())).thenReturn(asList(firstJwkList));
153+
JWTIssuerConfig singleIssuerConfig =
154+
new JWTIssuerConfig("single").setIss("A").setJwksUrl(asList("url1"));
155+
resolver = new JWTVerificationkeyResolver(Arrays.asList(singleIssuerConfig), true);
156+
157+
Key key = resolver.resolveKey(makeJws(k1, claimsWithIss("UNKNOWN")), null);
158+
assertNotNull(key);
159+
}
160+
161+
@Test(expected = UnresolvableKeyException.class)
162+
public void issMismatchMultipleIssuersThrows() throws Exception {
163+
// iss present but unrecognised, multiple issuers → UnresolvableKeyException
164+
JWTIssuerConfig iss1 = new JWTIssuerConfig("iss1").setIss("A").setJwksUrl(asList("url1"));
165+
JWTIssuerConfig iss2 = new JWTIssuerConfig("iss2").setIss("B").setJwksUrl(asList("url2"));
166+
resolver = new JWTVerificationkeyResolver(Arrays.asList(iss1, iss2), true);
167+
resolver.resolveKey(makeJws(k1, claimsWithIss("UNKNOWN")), null);
168+
}
169+
170+
@Test
171+
public void ecKeyTypeMaterialisedCorrectly() throws Exception {
172+
// EC key type should be returned as ECPublicKey, not RSAPublicKey
173+
EllipticCurveJsonWebKey ecKey = EcJwkGenerator.generateJwk(EllipticCurves.P256);
174+
ecKey.setKeyId("ec1");
175+
JsonWebKey ecPublicKey = JsonWebKey.Factory.newJwk(ecKey.getECPublicKey());
176+
ecPublicKey.setKeyId("ec1");
177+
JWTIssuerConfig ecIssuerConfig =
178+
new JWTIssuerConfig("ec-issuer")
179+
.setIss("ec-iss")
180+
.setJsonWebKeySet(new JsonWebKeySet(ecPublicKey));
181+
resolver = new JWTVerificationkeyResolver(Arrays.asList(ecIssuerConfig), false);
182+
183+
JsonWebSignature ecJws = new JsonWebSignature();
184+
ecJws.setPayload(claimsWithIss("ec-iss").toJson());
185+
ecJws.setKey(ecKey.getPrivateKey());
186+
ecJws.setKeyIdHeaderValue("ec1");
187+
ecJws.setAlgorithmHeaderValue(AlgorithmIdentifiers.ECDSA_USING_P256_CURVE_AND_SHA256);
188+
189+
Key key = resolver.resolveKey(ecJws, null);
190+
assertNotNull(key);
191+
assertTrue(key instanceof ECPublicKey);
192+
}
193+
194+
private static JwtClaims claimsWithNoIss() {
195+
JwtClaims claims = new JwtClaims();
196+
claims.setExpirationTimeMinutesInTheFuture(10);
197+
return claims;
198+
}
199+
200+
private static JwtClaims claimsWithIss(String iss) {
201+
JwtClaims claims = claimsWithNoIss();
202+
claims.setIssuer(iss);
203+
return claims;
204+
}
205+
206+
private static JsonWebSignature makeJws(KeyHolder keyHolder, JwtClaims claims)
207+
throws JoseException {
208+
JsonWebSignature jws = new JsonWebSignature();
209+
jws.setPayload(claims.toJson());
210+
jws.setKey(keyHolder.getRsaKey().getPrivateKey());
211+
jws.setKeyIdHeaderValue(keyHolder.getRsaKey().getKeyId());
212+
jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.RSA_USING_SHA256);
213+
return jws;
214+
}
215+
121216
@SuppressWarnings("NewClassNamingConvention")
122217
public static class KeyHolder {
123218
private final RsaJsonWebKey key;

0 commit comments

Comments
 (0)