This is an automated email from the ASF dual-hosted git repository.

malliaridis pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/solr.git


The following commit(s) were added to refs/heads/main by this push:
     new c8bd2eeb408 SOLR-17930: Allow xbasic for BasicAuthPlugin in 
MultiAuthPlugin (#3703)
c8bd2eeb408 is described below

commit c8bd2eeb4082177a45078352f2c28a7f5386cf3d
Author: Christos Malliaridis <[email protected]>
AuthorDate: Wed Oct 1 00:24:37 2025 +0300

    SOLR-17930: Allow xbasic for BasicAuthPlugin in MultiAuthPlugin (#3703)
    
    * Allow MultiAuthPlugin to lookup xBasic scheme for Basic auth requests
    
    * Update CHANGES.txt
    
    * Allow MultiAuthPlugin to be configured with single plugin
    
    * Add documentation notes for special treatment of Basic scheme
    
    * Add additional MultiAuthPlugin tests
---
 solr/CHANGES.txt                                   |   5 +
 .../org/apache/solr/security/MultiAuthPlugin.java  |  14 +-
 ...auth_plugin_with_basic_and_xbasic_security.json |  26 +++
 ...multi_auth_plugin_with_basic_only_security.json |  17 ++
 ...i_auth_plugin_with_mock_and_basic_security.json |  22 ++
 .../multi_auth_plugin_with_xbasic_security.json    |  22 ++
 .../apache/solr/security/MultiAuthPluginTest.java  | 235 +++++++++++++++++++++
 .../pages/basic-authentication-plugin.adoc         |  31 ++-
 8 files changed, 364 insertions(+), 8 deletions(-)

diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index 74bcc09b0ab..88d63a52e02 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -71,6 +71,11 @@ Improvements
 
 * SOLR-17926: Improve tracking of time already spent to discount the limit for 
sub-requests when `timeAllowed` is used. (Andrzej Bialecki, hossman)
 
+* SOLR-17930: MultiAuthPlugin now looks up for auth plugins configured with 
"xBasic" as scheme if
+  "Basic" authentication used and no plugin with "Basic" scheme found. This 
allows the new UI to
+  authenticate in browser without a credentials prompt being displayed. The 
MultiAuthPlugin can now
+  also be configured with a single plugin.
+
 Optimizations
 ---------------------
 * SOLR-17568: The CLI bin/solr export tool now contacts the appropriate nodes 
directly for data instead of proxying through one.
diff --git a/solr/core/src/java/org/apache/solr/security/MultiAuthPlugin.java 
b/solr/core/src/java/org/apache/solr/security/MultiAuthPlugin.java
index f8a557c9e8d..1a3cf09533f 100644
--- a/solr/core/src/java/org/apache/solr/security/MultiAuthPlugin.java
+++ b/solr/core/src/java/org/apache/solr/security/MultiAuthPlugin.java
@@ -130,11 +130,6 @@ public class MultiAuthPlugin extends AuthenticationPlugin
     }
 
     List<Object> schemeList = (List<Object>) o;
-    // if you only have one scheme, then you don't need to use this class
-    if (schemeList.size() < 2) {
-      throw new SolrException(
-          ErrorCode.SERVER_ERROR, "Invalid config: MultiAuthPlugin requires at 
least two schemes!");
-    }
 
     for (Object s : schemeList) {
       if (!(s instanceof Map)) {
@@ -231,7 +226,14 @@ public class MultiAuthPlugin extends AuthenticationPlugin
     }
 
     final String scheme = getSchemeFromAuthHeader(authHeader);
-    final AuthenticationPlugin plugin = pluginMap.get(scheme);
+    AuthenticationPlugin plugin = pluginMap.get(scheme);
+
+    if (plugin == null && scheme.equalsIgnoreCase("basic")) {
+      // In case no plugin found try looking up custom scheme xBasic when 
scheme is Basic, so that
+      // clients that use "Basic ..." are resolved with plugin "xBasic ..." if 
configured
+      plugin = pluginMap.get("x" + scheme);
+    }
+
     if (plugin == null) {
       addWWWAuthenticateHeaders(response);
       response.sendError(
diff --git 
a/solr/core/src/test-files/solr/security/multi_auth_plugin_with_basic_and_xbasic_security.json
 
b/solr/core/src/test-files/solr/security/multi_auth_plugin_with_basic_and_xbasic_security.json
new file mode 100644
index 00000000000..1ff7df5a859
--- /dev/null
+++ 
b/solr/core/src/test-files/solr/security/multi_auth_plugin_with_basic_and_xbasic_security.json
@@ -0,0 +1,26 @@
+{
+  "authentication": {
+    "class": "solr.MultiAuthPlugin",
+    "schemes": [
+      {
+        "scheme": "xbasic",
+        "realm": "xBasicRealm",
+        "blockUnknown": true,
+        "class": "solr.BasicAuthPlugin",
+        "credentials": {
+          "xadmin": "orwp2Ghgj39lmnrZOTm7Qtre1VqHFDfwAEzr0ApbN3Y= 
Ju5osoAqOX8iafhWpPP01E5P+sg8tK8tHON7rCYZRRw="
+        },
+        "forwardCredentials": false
+      },{
+        "scheme": "basic",
+        "realm": "basicRealm",
+        "blockUnknown": true,
+        "class": "solr.BasicAuthPlugin",
+        "credentials": {
+          "admin": "orwp2Ghgj39lmnrZOTm7Qtre1VqHFDfwAEzr0ApbN3Y= 
Ju5osoAqOX8iafhWpPP01E5P+sg8tK8tHON7rCYZRRw="
+        },
+        "forwardCredentials": false
+      }
+    ]
+  }
+}
diff --git 
a/solr/core/src/test-files/solr/security/multi_auth_plugin_with_basic_only_security.json
 
b/solr/core/src/test-files/solr/security/multi_auth_plugin_with_basic_only_security.json
new file mode 100644
index 00000000000..4aedfd1cda8
--- /dev/null
+++ 
b/solr/core/src/test-files/solr/security/multi_auth_plugin_with_basic_only_security.json
@@ -0,0 +1,17 @@
+{
+  "authentication": {
+    "class": "solr.MultiAuthPlugin",
+    "schemes": [
+      {
+        "scheme": "basic",
+        "realm": "BasicRealm",
+        "blockUnknown": true,
+        "class": "solr.BasicAuthPlugin",
+        "credentials": {
+          "admin": "orwp2Ghgj39lmnrZOTm7Qtre1VqHFDfwAEzr0ApbN3Y= 
Ju5osoAqOX8iafhWpPP01E5P+sg8tK8tHON7rCYZRRw="
+        },
+        "forwardCredentials": false
+      }
+    ]
+  }
+}
diff --git 
a/solr/core/src/test-files/solr/security/multi_auth_plugin_with_mock_and_basic_security.json
 
b/solr/core/src/test-files/solr/security/multi_auth_plugin_with_mock_and_basic_security.json
new file mode 100644
index 00000000000..da3e725092e
--- /dev/null
+++ 
b/solr/core/src/test-files/solr/security/multi_auth_plugin_with_mock_and_basic_security.json
@@ -0,0 +1,22 @@
+{
+  "authentication": {
+    "class": "solr.MultiAuthPlugin",
+    "schemes": [
+      {
+        "scheme": "mock",
+        "realm": "mockRealm",
+        "class": 
"org.apache.solr.security.MultiAuthPluginTest$MockAuthPluginForTesting",
+        "blockUnknown": true
+      },{
+        "scheme": "basic",
+        "realm": "basicRealm",
+        "blockUnknown": true,
+        "class": "solr.BasicAuthPlugin",
+        "credentials": {
+          "admin": "orwp2Ghgj39lmnrZOTm7Qtre1VqHFDfwAEzr0ApbN3Y= 
Ju5osoAqOX8iafhWpPP01E5P+sg8tK8tHON7rCYZRRw="
+        },
+        "forwardCredentials": false
+      }
+    ]
+  }
+}
diff --git 
a/solr/core/src/test-files/solr/security/multi_auth_plugin_with_xbasic_security.json
 
b/solr/core/src/test-files/solr/security/multi_auth_plugin_with_xbasic_security.json
new file mode 100644
index 00000000000..da85bf90b6f
--- /dev/null
+++ 
b/solr/core/src/test-files/solr/security/multi_auth_plugin_with_xbasic_security.json
@@ -0,0 +1,22 @@
+{
+  "authentication": {
+    "class": "solr.MultiAuthPlugin",
+    "schemes": [
+      {
+        "scheme": "xbasic",
+        "realm": "xBasicRealm",
+        "blockUnknown": true,
+        "class": "solr.BasicAuthPlugin",
+        "credentials": {
+          "admin": "orwp2Ghgj39lmnrZOTm7Qtre1VqHFDfwAEzr0ApbN3Y= 
Ju5osoAqOX8iafhWpPP01E5P+sg8tK8tHON7rCYZRRw="
+        },
+        "forwardCredentials": false
+      },{
+        "scheme": "mock",
+        "realm": "mockRealm",
+        "class": 
"org.apache.solr.security.MultiAuthPluginTest$MockAuthPluginForTesting",
+        "blockUnknown": true
+      }
+    ]
+  }
+}
diff --git 
a/solr/core/src/test/org/apache/solr/security/MultiAuthPluginTest.java 
b/solr/core/src/test/org/apache/solr/security/MultiAuthPluginTest.java
index d952569ee40..abe6cbbfd55 100644
--- a/solr/core/src/test/org/apache/solr/security/MultiAuthPluginTest.java
+++ b/solr/core/src/test/org/apache/solr/security/MultiAuthPluginTest.java
@@ -270,6 +270,241 @@ public class MultiAuthPluginTest extends SolrTestCaseJ4 {
     }
   }
 
+  @Test
+  public void testMultiAuthXBasicLookup() throws Exception {
+    final String user = "admin";
+    final String pass = "SolrRocks";
+
+    HttpClient httpClient = null;
+    SolrClient solrClient = null;
+    try {
+      httpClient = HttpClientUtil.createClient(null);
+      String baseUrl = buildUrl(jetty.getLocalPort());
+      solrClient = getHttpSolrClient(baseUrl);
+
+      verifySecurityStatus(httpClient, baseUrl + authcPrefix, 
"/errorMessages", null, 5);
+
+      // Initialize security.json with multiple xbasic auth and other 
configured
+      String multiAuthPluginSecurityJson =
+          Files.readString(
+              TEST_PATH()
+                  .resolve("security")
+                  .resolve("multi_auth_plugin_with_xbasic_security.json"),
+              StandardCharsets.UTF_8);
+      securityConfHandler.persistConf(
+          new SecurityConfHandler.SecurityConfig()
+              .setData(Utils.fromJSONString(multiAuthPluginSecurityJson)));
+      securityConfHandler.securityConfEdited();
+
+      // verify "WWW-Authenticate" headers are returned
+      verifyWWWAuthenticateHeaders(httpClient, baseUrl);
+
+      // Command that does not update anything in the current config
+      String command = "{ 'set-property': { 'xbasic': { 'blockUnknown': true } 
} }";
+
+      // verify that clients can still use "Basic" scheme with xBasic scheme 
configured in MultiAuth
+      doHttpPost(httpClient, baseUrl + authcPrefix, command, user, pass, 200);
+    } finally {
+      if (httpClient != null) {
+        HttpClientUtil.close(httpClient);
+      }
+      if (solrClient != null) {
+        solrClient.close();
+      }
+    }
+  }
+
+  @Test
+  public void testMultiAuthWithBasicAndXBasic() throws Exception {
+    final String user = "admin";
+    final String xUser = "xadmin";
+    final String pass = "SolrRocks";
+
+    HttpClient httpClient = null;
+    SolrClient solrClient = null;
+    try {
+      httpClient = HttpClientUtil.createClient(null);
+      String baseUrl = buildUrl(jetty.getLocalPort());
+      solrClient = getHttpSolrClient(baseUrl);
+
+      verifySecurityStatus(httpClient, baseUrl + authcPrefix, 
"/errorMessages", null, 5);
+
+      // Initialize security.json with basic and xbasic scheme
+      String multiAuthPluginSecurityJson =
+          Files.readString(
+              TEST_PATH()
+                  .resolve("security")
+                  
.resolve("multi_auth_plugin_with_basic_and_xbasic_security.json"),
+              StandardCharsets.UTF_8);
+      securityConfHandler.persistConf(
+          new SecurityConfHandler.SecurityConfig()
+              .setData(Utils.fromJSONString(multiAuthPluginSecurityJson)));
+      securityConfHandler.securityConfEdited();
+
+      // verify "WWW-Authenticate" headers are returned
+      verifyWWWAuthenticateHeaders(httpClient, baseUrl);
+
+      // Command that does not update anything in the current config
+      String command = "{ 'set-property': { 'basic': { 'blockUnknown': true } 
} }";
+
+      // verify that basic takes precedence over xbasic when both present
+      doHttpPost(httpClient, baseUrl + authcPrefix, command, user, pass, 200);
+
+      // Since both are present, xbasic will never be looked up if client does 
not send XBasic
+      // as auth scheme, and using xBasic won't work with BasicAuthPlugin, so 
this security
+      // configuration should return 401 as it resolves with the plugin that 
uses "basic" as scheme
+      doHttpPost(httpClient, baseUrl + authcPrefix, command, xUser, pass, 401);
+    } finally {
+      if (httpClient != null) {
+        HttpClientUtil.close(httpClient);
+      }
+      if (solrClient != null) {
+        solrClient.close();
+      }
+    }
+  }
+
+  @Test
+  public void testMultiAuthWithSinglePlugin() throws Exception {
+    final String user = "admin";
+    final String pass = "SolrRocks";
+
+    HttpClient httpClient = null;
+    SolrClient solrClient = null;
+    try {
+      httpClient = HttpClientUtil.createClient(null);
+      String baseUrl = buildUrl(jetty.getLocalPort());
+      solrClient = getHttpSolrClient(baseUrl);
+
+      verifySecurityStatus(httpClient, baseUrl + authcPrefix, 
"/errorMessages", null, 5);
+
+      // Initialize security.json with a single plugin configured
+      String multiAuthPluginSecurityJson =
+          Files.readString(
+              TEST_PATH()
+                  .resolve("security")
+                  .resolve("multi_auth_plugin_with_basic_only_security.json"),
+              StandardCharsets.UTF_8);
+      securityConfHandler.persistConf(
+          new SecurityConfHandler.SecurityConfig()
+              .setData(Utils.fromJSONString(multiAuthPluginSecurityJson)));
+      securityConfHandler.securityConfEdited();
+
+      // verify "WWW-Authenticate" headers are returned
+      verifyWWWAuthenticateHeaders(httpClient, baseUrl);
+
+      // Command that does not update anything in the current config
+      String command = "{ 'set-property': { 'basic': { 'blockUnknown': true } 
} }";
+
+      // verify that a single plugin configuration is allowed and works
+      doHttpPost(httpClient, baseUrl + authcPrefix, command, user, pass, 200);
+    } finally {
+      if (httpClient != null) {
+        HttpClientUtil.close(httpClient);
+      }
+      if (solrClient != null) {
+        solrClient.close();
+      }
+    }
+  }
+
+  @Test
+  public void testMultiAuthWithBasicAndMockPlugin() throws Exception {
+    final String user = "admin";
+    final String pass = "SolrRocks";
+
+    HttpClient httpClient = null;
+    SolrClient solrClient = null;
+    try {
+      httpClient = HttpClientUtil.createClient(null);
+      String baseUrl = buildUrl(jetty.getLocalPort());
+      solrClient = getHttpSolrClient(baseUrl);
+
+      verifySecurityStatus(httpClient, baseUrl + authcPrefix, 
"/errorMessages", null, 5);
+
+      // Initialize security.json with a single plugin configured
+      String multiAuthPluginSecurityJson =
+          Files.readString(
+              TEST_PATH()
+                  .resolve("security")
+                  
.resolve("multi_auth_plugin_with_mock_and_basic_security.json"),
+              StandardCharsets.UTF_8);
+      securityConfHandler.persistConf(
+          new SecurityConfHandler.SecurityConfig()
+              .setData(Utils.fromJSONString(multiAuthPluginSecurityJson)));
+      securityConfHandler.securityConfEdited();
+
+      // verify "WWW-Authenticate" headers are returned
+      verifyWWWAuthenticateHeaders(httpClient, baseUrl);
+
+      // Command that does not update anything in the current config
+      String command = "{ 'set-property': { 'basic': { 'blockUnknown': true } 
} }";
+
+      // verify that the basic auth plugin works and is looked up as expected
+      doHttpPost(httpClient, baseUrl + authcPrefix, command, user, pass, 200);
+    } finally {
+      if (httpClient != null) {
+        HttpClientUtil.close(httpClient);
+      }
+      if (solrClient != null) {
+        solrClient.close();
+      }
+    }
+  }
+
+  @Test
+  public void testMultiAuthWithBasicPluginAndAjax() throws Exception {
+    HttpClient httpClient = null;
+    SolrClient solrClient = null;
+    try {
+      httpClient = HttpClientUtil.createClient(null);
+      String baseUrl = buildUrl(jetty.getLocalPort());
+      solrClient = getHttpSolrClient(baseUrl);
+
+      verifySecurityStatus(httpClient, baseUrl + authcPrefix, 
"/errorMessages", null, 5);
+
+      // Initialize security.json with a single plugin configured
+      String multiAuthPluginSecurityJson =
+          Files.readString(
+              
TEST_PATH().resolve("security").resolve("multi_auth_plugin_security.json"),
+              StandardCharsets.UTF_8);
+      securityConfHandler.persistConf(
+          new SecurityConfHandler.SecurityConfig()
+              .setData(Utils.fromJSONString(multiAuthPluginSecurityJson)));
+      securityConfHandler.securityConfEdited();
+
+      // Pretend to send unauthorized AJAX request
+      HttpGet httpGet = new HttpGet(baseUrl + "/admin/info/system");
+      httpGet.addHeader(new BasicHeader("X-Requested-With", "XMLHttpRequest"));
+
+      HttpResponse response = httpClient.execute(httpGet);
+      assertEquals(
+          "Unauthorized response was expected", 401, 
response.getStatusLine().getStatusCode());
+
+      // Only first plugin is expected as response, which is also xBasic if 
BasicAuthPlugin
+      Header[] headers = response.getHeaders(HttpHeaders.WWW_AUTHENTICATE);
+      List<String> actualSchemes = 
Arrays.stream(headers).map(Header::getValue).toList();
+
+      // Only the first scheme is expected for AJAX-Requests
+      assertEquals("Only one scheme was expected", 1, actualSchemes.size());
+
+      // In case of BasicAuthPlugin, xBasic should be returned if AJAX request 
sent and handled by
+      // BasicAuthPlugin
+      String expectedScheme = "xBasic realm=\"solr\"";
+      assertEquals(
+          "Mapped xBasic challenge expected from first plugin which is 
BasicAuthPlugin",
+          expectedScheme,
+          actualSchemes.getFirst());
+    } finally {
+      if (httpClient != null) {
+        HttpClientUtil.close(httpClient);
+      }
+      if (solrClient != null) {
+        solrClient.close();
+      }
+    }
+  }
+
   private int doHttpGetAnonymous(HttpClient cl, String url) throws IOException 
{
     HttpGet httpPost = new HttpGet(url);
     HttpResponse r = cl.execute(httpPost);
diff --git 
a/solr/solr-ref-guide/modules/deployment-guide/pages/basic-authentication-plugin.adoc
 
b/solr/solr-ref-guide/modules/deployment-guide/pages/basic-authentication-plugin.adoc
index 8979e1da24a..4c0aa1821f0 100644
--- 
a/solr/solr-ref-guide/modules/deployment-guide/pages/basic-authentication-plugin.adoc
+++ 
b/solr/solr-ref-guide/modules/deployment-guide/pages/basic-authentication-plugin.adoc
@@ -83,9 +83,9 @@ An example command and more information about securing your 
setup can be found a
 
 === Password Encoding
 
-Solr stores the passwords in the format: 
`base64(sha256(sha256(salt+password))) base64(salt)`.  
+Solr stores the passwords in the format: 
`base64(sha256(sha256(salt+password))) base64(salt)`.
 
-If you edit `security.json` directly then you need to encode the password 
yourself.  
+If you edit `security.json` directly then you need to encode the password 
yourself.
 You can visit https://clemente-biondo.github.io/ to use a simple web utility 
that does the encoding for you.
 
 
@@ -142,6 +142,33 @@ For un-authenticated AJAX requests from the Solr Admin UI 
(i.e. requests without
 the `MultiAuthPlugin` forwards the request to the first plugin listed in the 
`schemes` list. In the example above,
 users will need to authenticate to the OIDC provider to login to the Admin UI.
 
+For un-authenticated, non-AJAX requests the `MultiAuthPlugin` returns all 
available plugins via
+`WWW-Authenticate` headers, if no plugin is configured to use `"blockUnknown": 
false`. Otherwise,
+the request is delegated to the first plugin that has set `blockUnknown` to 
`false`.
+
+=== Special Case for Basic Scheme
+
+A special case exists for the authentication scheme `Basic`. Some browser 
applications like the new UI
+may support multiple authentication options, but the `Basic` scheme in the 
`WWW-Authenticate` header
+triggers automatically a credentials prompt if received in the browser. For 
the scenario where the
+`Basic` scheme is used in combination with the `solr.BasicAuthPlugin`, the 
plugin maps the scheme
+to `xBasic` for AJAX requests, suppressing this way the prompt. For non-AJAX 
requests, this is not
+the case. The `BasicAuthPlugin` expects clients to continue using the `Basic` 
scheme in the
+`Authorization`header.
+
+In case you are using a web client that provides its own sign-in mask for the 
basic authentication
+and don't want to show the browser prompt you have the following options:
+- Configure `MultiAuthPlugin` with `BasicAuthPlugin` and scheme `xBasic`, so 
that it does not send
+  `WWW-Authenticate` header with `Basic` scheme (which triggers the browser 
prompt). This
+  configuration is supported starting with Solr 10.
+- Send the `X-Requested-With` header with `XMLHttpRequest` as value to let the 
`BasicAuthPlugin`
+  think it is an AJAX request (if it isn't already). This disables multiple 
authentication challenges
+  for unauthorized requests, but it is supported for all Solr versions.
+- Write a custom `AuthenticationPlugin` and use a custom scheme that is 
supported by your clients.
+
+If using basic authentication in combination with the new UI, it is 
recommended to use
+`MultiAuthPlugin`, even if you only have `BasicAuthPlugin` enabled.
+
 == Editing Basic Authentication Plugin Configuration
 
 An Authentication API allows modifying user IDs and passwords.

Reply via email to