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.