Copilot commented on code in PR #4334:
URL: https://github.com/apache/solr/pull/4334#discussion_r3229737693


##########
solr/solr-ref-guide/modules/deployment-guide/pages/jwt-authentication-plugin.adoc:
##########
@@ -42,17 +42,15 @@ The simplest possible `security.json` for registering the 
plugin without configu
 }
 ----
 
-The plugin will by default require a valid JWT token for all traffic.
-
-If the `blockUnknown` property is set to `false` as in the above example, it 
is possible to start configuring the plugin using unauthenticated REST API 
calls, which is further described in section <<Editing JWT Authentication 
Plugin Configuration>>.
+By default (`blockUnknown=false`), requests without a JWT are allowed through, 
which makes it possible to configure the plugin using unauthenticated REST API 
calls as described in <<Editing JWT Authentication Plugin Configuration>>. Set 
`blockUnknown=true` to require a valid JWT for all requests.

Review Comment:
   The new text claims the default is `blockUnknown=false`, but the plugin code 
defaults `blockUnknown` to `true` (see `JWTAuthPlugin` initialization using 
`getOrDefault(PARAM_BLOCK_UNKNOWN, true)`) and this page later also states 
blocking unauthenticated requests is the default. Please update this paragraph 
to reflect the actual default and keep it consistent with the rest of the doc 
and runtime behavior.
   
   This issue also appears on line 53 of the same file.



##########
solr/modules/jwt-auth/src/java/org/apache/solr/security/jwt/JWTIssuerConfig.java:
##########
@@ -441,6 +441,110 @@ public Collection<X509Certificate> getTrustedCerts() {
     return this.trustedCerts;
   }
 
+  /** Builds an SSL socket factory trusting the given certificates. */
+  static SSLSocketFactory buildSSLSocketFactory(Collection<X509Certificate> 
trustedCerts) {
+    try {
+      KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
+      ks.load(null, null);
+      int i = 0;
+      for (X509Certificate cert : trustedCerts) {
+        ks.setCertificateEntry("trusted-cert-" + i++, cert);
+      }
+      TrustManagerFactory tmf =
+          
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+      tmf.init(ks);
+      SSLContext sslContext = SSLContext.getInstance("TLS");
+      sslContext.init(null, tmf.getTrustManagers(), null);
+      return sslContext.getSocketFactory();
+    } catch (GeneralSecurityException | IOException e) {
+      throw new SolrException(
+          SolrException.ErrorCode.SERVER_ERROR, "Failed to build custom SSL 
context", e);
+    }
+  }
+
+  /**
+   * Builds a ResourceRetriever with an optional custom SSL trust store. Uses 
a custom
+   * implementation when trusted certs are configured; in that case hostname 
verification is also
+   * bypassed for loopback hosts. Otherwise uses the default 
DefaultResourceRetriever.
+   */
+  static ResourceRetriever buildResourceRetriever(
+      Collection<X509Certificate> trustedCerts, URL url) {
+    if (trustedCerts == null) {
+      return new DefaultResourceRetriever();
+    }
+    SSLSocketFactory ssf = buildSSLSocketFactory(trustedCerts);
+    InetAddress loopback = InetAddress.getLoopbackAddress();
+    boolean disableHostnameVerification =
+        loopback.getCanonicalHostName().equals(url.getHost())
+            || loopback.getHostName().equals(url.getHost());
+    return resourceUrl -> {
+      URLConnection conn = resourceUrl.openConnection();
+      if (conn instanceof HttpsURLConnection httpsConn) {
+        httpsConn.setSSLSocketFactory(ssf);
+        if (disableHostnameVerification) {
+          httpsConn.setHostnameVerifier((hostname, session) -> true);
+        }
+      }
+      try (InputStream in = conn.getInputStream()) {
+        String content = new String(in.readAllBytes(), StandardCharsets.UTF_8);
+        return new Resource(content, conn.getContentType());
+      }
+    };

Review Comment:
   `buildResourceRetriever`'s custom `ResourceRetriever` uses `URLConnection` 
with no explicit connect/read timeouts (and no response size limit), unlike 
`DefaultResourceRetriever`. This can cause request threads to hang indefinitely 
if an IdP endpoint is slow/unresponsive. Please set reasonable timeouts on the 
connection (and ideally enforce a max response size), or rework this to 
leverage Nimbus's `DefaultResourceRetriever` behavior while still applying the 
custom SSL socket factory / hostname verifier.



##########
solr/modules/jwt-auth/src/java/org/apache/solr/security/jwt/IssuerAwareJWSKeySelector.java:
##########
@@ -0,0 +1,218 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.solr.security.jwt;
+
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JWSHeader;
+import com.nimbusds.jose.KeySourceException;
+import com.nimbusds.jose.jwk.ECKey;
+import com.nimbusds.jose.jwk.JWK;
+import com.nimbusds.jose.jwk.JWKMatcher;
+import com.nimbusds.jose.jwk.JWKSelector;
+import com.nimbusds.jose.jwk.JWKSet;
+import com.nimbusds.jose.jwk.OctetSequenceKey;
+import com.nimbusds.jose.jwk.RSAKey;
+import com.nimbusds.jose.proc.JWSKeySelector;
+import com.nimbusds.jose.proc.SecurityContext;
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+import java.security.Key;
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import javax.net.ssl.SSLHandshakeException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Resolves JWS signature verification keys from a set of {@link 
JWTIssuerConfig} objects, which may
+ * represent any valid configuration in Solr's security.json, i.e. static list 
of JWKs or keys
+ * retrieved from HTTPS JWK endpoints.
+ *
+ * <p>This implementation maintains a map of issuers, each with its own list 
of {@link JWK}, and
+ * resolves the correct key from the correct issuer. The issuer is passed in 
via {@link
+ * IssuerContext}.
+ *
+ * <p>If a key is not found and the issuer is backed by HTTPS JWKs, one cache 
refresh is attempted
+ * before failing.
+ */
+public class IssuerAwareJWSKeySelector implements 
JWSKeySelector<SecurityContext> {
+  private static final Logger log = 
LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+  private final Map<String, JWTIssuerConfig> issuerConfigs = new HashMap<>();
+  private final boolean requireIssuer;
+
+  /**
+   * SecurityContext subclass that carries the (unverified) issuer claim from 
the JWT payload,
+   * allowing the key selector to look up the correct issuer configuration.
+   */
+  public static class IssuerContext implements SecurityContext {
+    private final String issuer;
+
+    public IssuerContext(String issuer) {
+      this.issuer = issuer;
+    }
+
+    public String getIssuer() {
+      return issuer;
+    }
+  }
+
+  /**
+   * Resolves key from a JWKs from one or more IssuerConfigs
+   *
+   * @param issuerConfigs Collection of configuration objects for the issuer(s)
+   * @param requireIssuer if true, will require 'iss' claim on jws
+   */
+  public IssuerAwareJWSKeySelector(
+      Collection<JWTIssuerConfig> issuerConfigs, boolean requireIssuer) {
+    this.requireIssuer = requireIssuer;
+    issuerConfigs.forEach(ic -> this.issuerConfigs.put(ic.getIss(), ic));
+  }
+
+  @Override
+  public List<? extends Key> selectJWSKeys(JWSHeader header, SecurityContext 
context)
+      throws KeySourceException {
+    String tokenIssuer =
+        (context instanceof IssuerContext) ? ((IssuerContext) 
context).getIssuer() : null;
+
+    JWTIssuerConfig issuerConfig = resolveIssuerConfig(tokenIssuer);
+
+    List<JWK> allJwks = new ArrayList<>();
+    String keysSource = "N/A";
+    try {
+      if (issuerConfig.usesHttpsJwk()) {
+        keysSource = "[" + String.join(", ", issuerConfig.getJwksUrls()) + "]";
+        for (JWTIssuerConfig.JwkSetFetcher fetcher : 
issuerConfig.getHttpsJwks()) {
+          try {
+            allJwks.addAll(fetcher.getKeys());
+          } catch (SSLHandshakeException e) {
+            throw new KeySourceException(
+                "Failed to connect with "
+                    + fetcher.getLocation()
+                    + ", do you have the correct SSL certificate configured?",
+                e);
+          }
+        }
+      } else {
+        keysSource = "static list of keys in security.json";
+        allJwks.addAll(issuerConfig.getJsonWebKeySet().getKeys());
+      }
+    } catch (IOException | ParseException e) {
+      throw new KeySourceException(
+          String.format(
+              Locale.ROOT, "Unable to fetch JWKs from source %s: %s", 
keysSource, e.getMessage()),
+          e);
+    }
+
+    JWKSelector selector = new JWKSelector(JWKMatcher.forJWSHeader(header));
+    List<JWK> matchingJwks = selector.select(new JWKSet(allJwks));
+
+    if (matchingJwks.isEmpty() && issuerConfig.usesHttpsJwk()) {
+      if (log.isDebugEnabled()) {
+        log.debug(
+            "No matching key found for JWS header {} in {} keys from {}; 
refreshing",
+            header,
+            allJwks.size(),
+            keysSource);
+      }
+      allJwks.clear();
+      try {
+        for (JWTIssuerConfig.JwkSetFetcher fetcher : 
issuerConfig.getHttpsJwks()) {
+          fetcher.refresh();
+          allJwks.addAll(fetcher.getKeys());
+        }

Review Comment:
   In the refresh path, `SSLHandshakeException` from `fetcher.refresh()` / 
`fetcher.getKeys()` is not given the same helpful hint as the initial fetch 
path (which explicitly suggests checking configured SSL certs). Consider 
catching `SSLHandshakeException` here as well and rethrowing a 
`KeySourceException` with the clearer message to keep troubleshooting 
consistent.
   



##########
solr/modules/jwt-auth/src/java/org/apache/solr/security/jwt/JWTIssuerConfig.java:
##########
@@ -475,19 +574,12 @@ private HttpsJwks create(String url) {
             SolrException.ErrorCode.SERVER_ERROR,
             "Url " + url + " configured in " + PARAM_JWKS_URL + " is not a 
valid URL");

Review Comment:
   `HttpsJwksFactory.create()` only catches `MalformedURLException`, but 
`URI.create(url)` can also throw `IllegalArgumentException` for invalid input. 
That would currently bubble up as an unchecked exception instead of a clear 
`SolrException` pointing to `jwksUrl`. Please catch/wrap 
`IllegalArgumentException` (or validate earlier) so invalid JWKS URLs 
consistently produce a helpful configuration error.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to