You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@solr.apache.org by ja...@apache.org on 2023/09/03 15:50:04 UTC
[solr] branch main updated: SOLR-16897: Add support of OAuth 2.0/OIDC 'code with PKCE' flow (#1792)
This is an automated email from the ASF dual-hosted git repository.
janhoy 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 931de515722 SOLR-16897: Add support of OAuth 2.0/OIDC 'code with PKCE' flow (#1792)
931de515722 is described below
commit 931de5157226655ec18b2530b4ac27905f0b5818
Author: Lamine <10...@users.noreply.github.com>
AuthorDate: Sun Sep 3 10:49:58 2023 -0500
SOLR-16897: Add support of OAuth 2.0/OIDC 'code with PKCE' flow (#1792)
Co-authored-by: Lamine Idjeraoui <li...@apple.com>
Co-authored-by: Jan Høydahl <ja...@apache.org>
---
solr/CHANGES.txt | 2 +
.../apache/solr/servlet/LoadAdminUiServlet.java | 37 +++++++-
.../solr/servlet/LoadAdminUiServletTest.java | 102 +++++++++++++++++++++
.../apache/solr/security/jwt/JWTAuthPlugin.java | 23 ++++-
.../apache/solr/security/jwt/JWTIssuerConfig.java | 59 ++++++++++++
.../solr/security/jwt_plugin_jwk_security.json | 2 +
.../jwt_plugin_jwk_security_blockUnknownFalse.json | 2 +
.../security/jwt/JWTAuthPluginIntegrationTest.java | 4 +
.../solr/security/jwt/JWTAuthPluginTest.java | 14 +++
.../solr/security/jwt/JWTIssuerConfigTest.java | 12 ++-
.../pages/jwt-authentication-plugin.adoc | 2 +
11 files changed, 255 insertions(+), 4 deletions(-)
diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index 9bfc1191052..13cf7644f01 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -107,6 +107,8 @@ Improvements
* SOLR-16927: Allow SolrClientCache clients to use Jetty HTTP2 clients (Alex Deparvu, David Smiley)
+* SOLR-16896, SOLR-16897: Add support of OAuth 2.0/OIDC 'code with PKCE' flow (Lamine Idjeraoui, janhoy, Kevin Risden)
+
* SOLR-16879: Limit the number of concurrent expensive core admin operations by running them in a
dedicated thread pool. Backup, Restore and Split are expensive operations.
(Pierre Salagnac, David Smiley)
diff --git a/solr/core/src/java/org/apache/solr/servlet/LoadAdminUiServlet.java b/solr/core/src/java/org/apache/solr/servlet/LoadAdminUiServlet.java
index 5a7e797f297..9da4bc84775 100644
--- a/solr/core/src/java/org/apache/solr/servlet/LoadAdminUiServlet.java
+++ b/solr/core/src/java/org/apache/solr/servlet/LoadAdminUiServlet.java
@@ -16,11 +16,15 @@
*/
package org.apache.solr.servlet;
+import com.google.common.net.HttpHeaders;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.io.output.CloseShieldOutputStream;
@@ -37,6 +41,8 @@ public final class LoadAdminUiServlet extends BaseSolrServlet {
// check system properties for whether or not admin UI is disabled, default is false
private static final boolean disabled =
Boolean.parseBoolean(System.getProperty("disableAdminUI", "false"));
+ // list of comma separated URLs to inject into the CSP connect-src directive
+ public static final String SYSPROP_CSP_CONNECT_SRC_URLS = "solr.ui.headers.csp.connect-src.urls";
@Override
public void doGet(HttpServletRequest _request, HttpServletResponse _response) throws IOException {
@@ -60,15 +66,20 @@ public final class LoadAdminUiServlet extends BaseSolrServlet {
if (in != null && cores != null) {
response.setCharacterEncoding("UTF-8");
response.setContentType("text/html");
+ String connectSrc = generateCspConnectSrc();
+ response.setHeader(
+ HttpHeaders.CONTENT_SECURITY_POLICY,
+ "default-src 'none'; base-uri 'none'; connect-src "
+ + connectSrc
+ + "; form-action 'self'; font-src 'self'; frame-ancestors 'none'; img-src 'self' data:; media-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self'; worker-src 'self';");
// We have to close this to flush OutputStreamWriter buffer
try (Writer out =
new OutputStreamWriter(
CloseShieldOutputStream.wrap(response.getOutputStream()), StandardCharsets.UTF_8)) {
- Package pack = SolrCore.class.getPackage();
String html =
new String(in.readAllBytes(), StandardCharsets.UTF_8)
- .replace("${version}", pack.getSpecificationVersion());
+ .replace("${version}", getSolrCorePackageSpecVersion());
out.write(html);
}
} else {
@@ -76,4 +87,26 @@ public final class LoadAdminUiServlet extends BaseSolrServlet {
}
}
}
+
+ /**
+ * Retrieves the specification version of the SolrCore package.
+ *
+ * @return The specification version of the SolrCore class's package or Unknown if it's
+ * unavailable.
+ */
+ private String getSolrCorePackageSpecVersion() {
+ Package pack = SolrCore.class.getPackage();
+ return pack.getSpecificationVersion() != null ? pack.getSpecificationVersion() : "Unknown";
+ }
+
+ /**
+ * Fetch the value of {@link #SYSPROP_CSP_CONNECT_SRC_URLS} system property, split by comma, and
+ * concatenate them into a space-separated string that can be used in CSP connect-src directive
+ */
+ private String generateCspConnectSrc() {
+ String cspURLs = System.getProperty(SYSPROP_CSP_CONNECT_SRC_URLS, "");
+ List<String> props = new ArrayList<>(Arrays.asList(cspURLs.split(",")));
+ props.add("'self'");
+ return String.join(" ", props);
+ }
}
diff --git a/solr/core/src/test/org/apache/solr/servlet/LoadAdminUiServletTest.java b/solr/core/src/test/org/apache/solr/servlet/LoadAdminUiServletTest.java
new file mode 100644
index 00000000000..299029f86f7
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/servlet/LoadAdminUiServletTest.java
@@ -0,0 +1,102 @@
+/*
+ * 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.servlet;
+
+import static org.apache.solr.servlet.LoadAdminUiServlet.SYSPROP_CSP_CONNECT_SRC_URLS;
+import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.Set;
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletOutputStream;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.core.CoreContainer;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+public class LoadAdminUiServletTest extends SolrTestCaseJ4 {
+
+ @InjectMocks private LoadAdminUiServlet servlet;
+ @Mock private HttpServletRequest mockRequest;
+ @Mock private HttpServletResponse mockResponse;
+ @Mock private CoreContainer coreContainer;
+ @Mock private ServletConfig servletConfig;
+ @Mock private ServletContext mockServletContext;
+ @Mock private ServletOutputStream mockOutputStream;
+
+ private static final Set<String> CSP_URLS =
+ Set.of(
+ "http://example1.com/token",
+ "https://example2.com/path/uri1",
+ "http://example3.com/oauth2/uri2");
+
+ @Override
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ MockitoAnnotations.openMocks(this);
+ when(mockRequest.getRequestURI()).thenReturn("/path/URI");
+ when(mockRequest.getContextPath()).thenReturn("/path");
+ when(mockRequest.getAttribute("org.apache.solr.CoreContainer")).thenReturn(coreContainer);
+ when(servletConfig.getServletContext()).thenReturn(mockServletContext);
+ when(mockResponse.getOutputStream()).thenReturn(mockOutputStream);
+ InputStream mockInputStream =
+ new ByteArrayInputStream("mock content".getBytes(StandardCharsets.UTF_8));
+ when(mockServletContext.getResourceAsStream(anyString())).thenReturn(mockInputStream);
+ }
+
+ @BeforeClass
+ public static void ensureWorkingMockito() {
+ assumeWorkingMockito();
+ }
+
+ @Test
+ public void testDefaultCSPHeaderSet() throws IOException {
+ System.setProperty(SYSPROP_CSP_CONNECT_SRC_URLS, String.join(",", CSP_URLS));
+ ArgumentCaptor<String> headerNameCaptor = ArgumentCaptor.forClass(String.class);
+ ArgumentCaptor<String> headerValueCaptor = ArgumentCaptor.forClass(String.class);
+ servlet.doGet(mockRequest, mockResponse);
+
+ verify(mockResponse).setHeader(headerNameCaptor.capture(), headerValueCaptor.capture());
+ assertEquals("Content-Security-Policy", headerNameCaptor.getValue());
+ String cspValue = headerValueCaptor.getValue();
+ for (String endpoint : CSP_URLS) {
+ assertTrue("Expected CSP value to contain " + endpoint, cspValue.contains(endpoint));
+ }
+ }
+
+ @Override
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+}
diff --git a/solr/modules/jwt-auth/src/java/org/apache/solr/security/jwt/JWTAuthPlugin.java b/solr/modules/jwt-auth/src/java/org/apache/solr/security/jwt/JWTAuthPlugin.java
index ae20606f7e7..3e3f7578fc9 100644
--- a/solr/modules/jwt-auth/src/java/org/apache/solr/security/jwt/JWTAuthPlugin.java
+++ b/solr/modules/jwt-auth/src/java/org/apache/solr/security/jwt/JWTAuthPlugin.java
@@ -63,6 +63,7 @@ import org.apache.solr.security.AuthenticationPlugin;
import org.apache.solr.security.ConfigEditablePlugin;
import org.apache.solr.security.jwt.JWTAuthPlugin.JWTAuthenticationResponse.AuthCode;
import org.apache.solr.security.jwt.api.ModifyJWTAuthPluginConfigAPI;
+import org.apache.solr.servlet.LoadAdminUiServlet;
import org.apache.solr.util.CryptoKeys;
import org.eclipse.jetty.client.api.Request;
import org.jose4j.jwa.AlgorithmConstraints;
@@ -130,7 +131,9 @@ public class JWTAuthPlugin extends AuthenticationPlugin
JWTIssuerConfig.PARAM_CLIENT_ID,
JWTIssuerConfig.PARAM_WELL_KNOWN_URL,
JWTIssuerConfig.PARAM_AUDIENCE,
- JWTIssuerConfig.PARAM_AUTHORIZATION_ENDPOINT);
+ JWTIssuerConfig.PARAM_AUTHORIZATION_ENDPOINT,
+ JWTIssuerConfig.PARAM_TOKEN_ENDPOINT,
+ JWTIssuerConfig.PARAM_AUTHORIZATION_FLOW);
private JwtConsumer jwtConsumer;
private boolean requireExpirationTime;
@@ -280,10 +283,24 @@ public class JWTAuthPlugin extends AuthenticationPlugin
}
initConsumer();
+ registerTokenEndpointForCsp();
lastInitTime = Instant.now();
}
+ /**
+ * Record Issuer token URL as a system property so it can be picked up and sent to Admin UI as CSP
+ */
+ protected void registerTokenEndpointForCsp() {
+ final String syspropName = LoadAdminUiServlet.SYSPROP_CSP_CONNECT_SRC_URLS;
+ String url = !issuerConfigs.isEmpty() ? getPrimaryIssuer().getTokenEndpoint() : null;
+ if (url != null) {
+ System.setProperty(syspropName, url);
+ } else {
+ System.clearProperty(syspropName);
+ }
+ }
+
/**
* Given a configuration object of a file name or list of file names, read X509 certificates from
* each file
@@ -336,6 +353,8 @@ public class JWTAuthPlugin extends AuthenticationPlugin
.setJwksUrl(conf.get(JWTIssuerConfig.PARAM_JWKS_URL))
.setAuthorizationEndpoint(
(String) conf.get(JWTIssuerConfig.PARAM_AUTHORIZATION_ENDPOINT))
+ .setTokenEndpoint((String) conf.get(JWTIssuerConfig.PARAM_TOKEN_ENDPOINT))
+ .setAuthorizationFlow((String) conf.get(JWTIssuerConfig.PARAM_AUTHORIZATION_FLOW))
.setClientId((String) conf.get(JWTIssuerConfig.PARAM_CLIENT_ID))
.setWellKnownUrl((String) conf.get(JWTIssuerConfig.PARAM_WELL_KNOWN_URL));
if (conf.get(JWTIssuerConfig.PARAM_JWK) != null) {
@@ -847,9 +866,11 @@ public class JWTAuthPlugin extends AuthenticationPlugin
Map<String, Object> data = new HashMap<>();
data.put(
JWTIssuerConfig.PARAM_AUTHORIZATION_ENDPOINT, primaryIssuer.getAuthorizationEndpoint());
+ data.put(JWTIssuerConfig.PARAM_TOKEN_ENDPOINT, primaryIssuer.getTokenEndpoint());
data.put("client_id", primaryIssuer.getClientId());
data.put("scope", adminUiScope);
data.put("redirect_uris", redirectUris);
+ data.put("authorization_flow", primaryIssuer.getAuthorizationFlow());
String headerJson = Utils.toJSONString(data);
return Base64.getEncoder().encodeToString(headerJson.getBytes(StandardCharsets.UTF_8));
}
diff --git a/solr/modules/jwt-auth/src/java/org/apache/solr/security/jwt/JWTIssuerConfig.java b/solr/modules/jwt-auth/src/java/org/apache/solr/security/jwt/JWTIssuerConfig.java
index 55ccbcc956a..947d040da8b 100644
--- a/solr/modules/jwt-auth/src/java/org/apache/solr/security/jwt/JWTIssuerConfig.java
+++ b/solr/modules/jwt-auth/src/java/org/apache/solr/security/jwt/JWTIssuerConfig.java
@@ -21,6 +21,7 @@ import com.google.common.annotations.VisibleForTesting;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
+import java.lang.invoke.MethodHandles;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.Charset;
@@ -32,8 +33,10 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import java.util.stream.Collectors;
import org.apache.solr.common.SolrException;
+import org.apache.solr.common.util.StrUtils;
import org.apache.solr.common.util.Utils;
import org.jose4j.http.Get;
import org.jose4j.http.SimpleResponse;
@@ -41,9 +44,12 @@ import org.jose4j.jwk.HttpsJwks;
import org.jose4j.jwk.JsonWebKey;
import org.jose4j.jwk.JsonWebKeySet;
import org.jose4j.lang.JoseException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
/** Holds information about an IdP (issuer), such as issuer ID, JWK url(s), keys etc */
public class JWTIssuerConfig {
+ private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
static final String PARAM_ISS_NAME = "name";
static final String PARAM_JWKS_URL = "jwksUrl";
static final String PARAM_JWK = "jwk";
@@ -51,7 +57,9 @@ public class JWTIssuerConfig {
static final String PARAM_AUDIENCE = "aud";
static final String PARAM_WELL_KNOWN_URL = "wellKnownUrl";
static final String PARAM_AUTHORIZATION_ENDPOINT = "authorizationEndpoint";
+ static final String PARAM_TOKEN_ENDPOINT = "tokenEndpoint";
static final String PARAM_CLIENT_ID = "clientId";
+ static final String PARAM_AUTHORIZATION_FLOW = "authorizationFlow";
private static HttpsJwksFactory httpsJwksFactory = new HttpsJwksFactory(3600, 5000);
private String iss;
@@ -64,12 +72,18 @@ public class JWTIssuerConfig {
private WellKnownDiscoveryConfig wellKnownDiscoveryConfig;
private String clientId;
private String authorizationEndpoint;
+ private String tokenEndpoint;
+ private String authorizationFlow;
private Collection<X509Certificate> trustedCerts;
public static boolean ALLOW_OUTBOUND_HTTP =
Boolean.parseBoolean(System.getProperty("solr.auth.jwt.allowOutboundHttp", "false"));
public static final String ALLOW_OUTBOUND_HTTP_ERR_MSG =
"HTTPS required for IDP communication. Please use SSL or start your nodes with -Dsolr.auth.jwt.allowOutboundHttp=true to allow HTTP for test purposes.";
+ private static final String DEFAULT_AUTHORIZATION_FLOW =
+ "implicit"; // 'implicit' to be deprecated
+ private static final Set<String> VALID_AUTHORIZATION_FLOWS =
+ Set.of(DEFAULT_AUTHORIZATION_FLOW, "code_pkce");
/**
* Create config for further configuration with setters, builder style. Once all values are set,
@@ -117,6 +131,10 @@ public class JWTIssuerConfig {
if (authorizationEndpoint == null) {
authorizationEndpoint = wellKnownDiscoveryConfig.getAuthorizationEndpoint();
}
+
+ if (tokenEndpoint == null) {
+ tokenEndpoint = wellKnownDiscoveryConfig.getTokenEndpoint();
+ }
}
if (iss == null && usesHttpsJwk() && !JWTAuthPlugin.PRIMARY_ISSUER.equals(name)) {
throw new SolrException(
@@ -141,6 +159,8 @@ public class JWTIssuerConfig {
setJwksUrl(confJwksUrl);
setJsonWebKeySet(conf.get(PARAM_JWK));
setAuthorizationEndpoint((String) conf.get(PARAM_AUTHORIZATION_ENDPOINT));
+ setTokenEndpoint((String) conf.get(PARAM_TOKEN_ENDPOINT));
+ setAuthorizationFlow((String) conf.get(PARAM_AUTHORIZATION_FLOW));
conf.remove(PARAM_WELL_KNOWN_URL);
conf.remove(PARAM_ISSUER);
@@ -150,6 +170,8 @@ public class JWTIssuerConfig {
conf.remove(PARAM_JWKS_URL);
conf.remove(PARAM_JWK);
conf.remove(PARAM_AUTHORIZATION_ENDPOINT);
+ conf.remove(PARAM_TOKEN_ENDPOINT);
+ conf.remove(PARAM_AUTHORIZATION_FLOW);
if (!conf.isEmpty()) {
throw new SolrException(
@@ -315,6 +337,41 @@ public class JWTIssuerConfig {
return this;
}
+ public String getTokenEndpoint() {
+ return tokenEndpoint;
+ }
+
+ public JWTIssuerConfig setTokenEndpoint(String tokenEndpoint) {
+ this.tokenEndpoint = tokenEndpoint;
+ return this;
+ }
+
+ public String getAuthorizationFlow() {
+ return authorizationFlow;
+ }
+
+ public JWTIssuerConfig setAuthorizationFlow(String authorizationFlow) {
+ this.authorizationFlow =
+ StrUtils.isNullOrEmpty(authorizationFlow)
+ ? DEFAULT_AUTHORIZATION_FLOW
+ : authorizationFlow.trim();
+ if (!VALID_AUTHORIZATION_FLOWS.contains(this.authorizationFlow)) {
+ throw new SolrException(
+ SolrException.ErrorCode.SERVER_ERROR,
+ "Invalid value for "
+ + PARAM_AUTHORIZATION_FLOW
+ + ". Expected one of "
+ + VALID_AUTHORIZATION_FLOWS
+ + " but found "
+ + authorizationFlow);
+ }
+ if (this.authorizationFlow.equals("implicit")) {
+ log.warn(
+ "JWT authentication plugin is using 'implicit flow' which is deprecated and less secure. It's recommended to switch to 'code_pkce'");
+ }
+ return this;
+ }
+
public Map<String, Object> asConfig() {
HashMap<String, Object> config = new HashMap<>();
putIfNotNull(config, PARAM_ISS_NAME, name);
@@ -324,6 +381,8 @@ public class JWTIssuerConfig {
putIfNotNull(config, PARAM_WELL_KNOWN_URL, wellKnownUrl);
putIfNotNull(config, PARAM_CLIENT_ID, clientId);
putIfNotNull(config, PARAM_AUTHORIZATION_ENDPOINT, authorizationEndpoint);
+ putIfNotNull(config, PARAM_TOKEN_ENDPOINT, tokenEndpoint);
+ putIfNotNull(config, PARAM_AUTHORIZATION_FLOW, authorizationFlow);
if (jsonWebKeySet != null) {
putIfNotNull(config, PARAM_JWK, jsonWebKeySet.getJsonWebKeys());
}
diff --git a/solr/modules/jwt-auth/src/test-files/solr/security/jwt_plugin_jwk_security.json b/solr/modules/jwt-auth/src/test-files/solr/security/jwt_plugin_jwk_security.json
index 772089e3819..313b409a230 100644
--- a/solr/modules/jwt-auth/src/test-files/solr/security/jwt_plugin_jwk_security.json
+++ b/solr/modules/jwt-auth/src/test-files/solr/security/jwt_plugin_jwk_security.json
@@ -13,6 +13,8 @@
"realm": "my-solr-jwt",
"adminUiScope": "solr:admin",
"authorizationEndpoint": "http://acmepaymentscorp/oauth/auz/authorize",
+ "tokenEndpoint": "http://acmepaymentscorp/oauth/oauth20/token",
+ "authorizationFlow": "code_pkce",
"clientId": "solr-cluster"
}
}
\ No newline at end of file
diff --git a/solr/modules/jwt-auth/src/test-files/solr/security/jwt_plugin_jwk_security_blockUnknownFalse.json b/solr/modules/jwt-auth/src/test-files/solr/security/jwt_plugin_jwk_security_blockUnknownFalse.json
index ef5bcdeea4a..e9dafff1c2f 100644
--- a/solr/modules/jwt-auth/src/test-files/solr/security/jwt_plugin_jwk_security_blockUnknownFalse.json
+++ b/solr/modules/jwt-auth/src/test-files/solr/security/jwt_plugin_jwk_security_blockUnknownFalse.json
@@ -13,6 +13,8 @@
"realm": "my-solr-jwt-blockunknown-false",
"adminUiScope": "solr:admin",
"authorizationEndpoint": "http://acmepaymentscorp/oauth/auz/authorize",
+ "tokenEndpoint": "http://acmepaymentscorp/oauth/oauth20/token",
+ "authorizationFlow": "code_pkce",
"clientId": "solr-cluster"
},
"authorization": {
diff --git a/solr/modules/jwt-auth/src/test/org/apache/solr/security/jwt/JWTAuthPluginIntegrationTest.java b/solr/modules/jwt-auth/src/test/org/apache/solr/security/jwt/JWTAuthPluginIntegrationTest.java
index a7dad973d86..23cefdbca51 100644
--- a/solr/modules/jwt-auth/src/test/org/apache/solr/security/jwt/JWTAuthPluginIntegrationTest.java
+++ b/solr/modules/jwt-auth/src/test/org/apache/solr/security/jwt/JWTAuthPluginIntegrationTest.java
@@ -162,6 +162,8 @@ public class JWTAuthPluginIntegrationTest extends SolrCloudAuthTestCase {
String authData = new String(Base64.getDecoder().decode(headers.get("X-Solr-AuthData")), UTF_8);
assertEquals(
"{\n"
+ + " \"tokenEndpoint\":\"http://acmepaymentscorp/oauth/oauth20/token\",\n"
+ + " \"authorization_flow\":\"code_pkce\",\n"
+ " \"scope\":\"solr:admin\",\n"
+ " \"redirect_uris\":[],\n"
+ " \"authorizationEndpoint\":\"http://acmepaymentscorp/oauth/auz/authorize\",\n"
@@ -184,6 +186,8 @@ public class JWTAuthPluginIntegrationTest extends SolrCloudAuthTestCase {
String authData = new String(Base64.getDecoder().decode(headers.get("X-Solr-AuthData")), UTF_8);
assertEquals(
"{\n"
+ + " \"tokenEndpoint\":\"http://acmepaymentscorp/oauth/oauth20/token\",\n"
+ + " \"authorization_flow\":\"code_pkce\",\n"
+ " \"scope\":\"solr:admin\",\n"
+ " \"redirect_uris\":[],\n"
+ " \"authorizationEndpoint\":\"http://acmepaymentscorp/oauth/auz/authorize\",\n"
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 f40ffee9be9..9e04865c6c3 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
@@ -45,6 +45,7 @@ import org.apache.solr.SolrTestCaseJ4;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.util.Utils;
import org.apache.solr.security.VerifiedUserRoles;
+import org.apache.solr.servlet.LoadAdminUiServlet;
import org.apache.solr.util.CryptoKeys;
import org.jose4j.jwk.RsaJsonWebKey;
import org.jose4j.jwk.RsaJwkGenerator;
@@ -519,6 +520,8 @@ public class JWTAuthPluginTest extends SolrTestCaseJ4 {
public void xSolrAuthDataHeader() {
testConfig.put("adminUiScope", "solr:admin");
testConfig.put("authorizationEndpoint", "http://acmepaymentscorp/oauth/auz/authorize");
+ testConfig.put("tokenEndpoint", "http://acmepaymentscorp/oauth/oauth20/token");
+ testConfig.put("authorizationFlow", "code_pkce");
testConfig.put("clientId", "solr-cluster");
plugin.init(testConfig);
String headerBase64 = plugin.generateAuthDataHeader();
@@ -528,6 +531,8 @@ public class JWTAuthPluginTest extends SolrTestCaseJ4 {
assertEquals("solr:admin", parsed.get("scope"));
assertEquals(
"http://acmepaymentscorp/oauth/auz/authorize", parsed.get("authorizationEndpoint"));
+ assertEquals("http://acmepaymentscorp/oauth/oauth20/token", parsed.get("tokenEndpoint"));
+ assertEquals("code_pkce", parsed.get("authorization_flow"));
assertEquals("solr-cluster", parsed.get("client_id"));
}
@@ -703,4 +708,13 @@ public class JWTAuthPluginTest extends SolrTestCaseJ4 {
.toString();
assertEquals(2, plugin.parseCertsFromFile(pemFilePath).size());
}
+
+ @Test
+ public void testRegisterTokenEndpointForCsp() {
+ testConfig.put("tokenEndpoint", "http://acmepaymentscorp/oauth/oauth20/token");
+ plugin.init(testConfig);
+ assertEquals(
+ "http://acmepaymentscorp/oauth/oauth20/token",
+ System.getProperty(LoadAdminUiServlet.SYSPROP_CSP_CONNECT_SRC_URLS));
+ }
}
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 3355486001a..57c0261b897 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
@@ -53,15 +53,19 @@ public class JWTIssuerConfigTest extends SolrTestCase {
.setAud("audience")
.setClientId("clientid")
.setWellKnownUrl("wellknown")
- .setAuthorizationEndpoint("https://issuer/authz");
+ .setAuthorizationEndpoint("https://issuer/authz")
+ .setTokenEndpoint("https://issuer/token")
+ .setAuthorizationFlow("code_pkce");
testIssuerConfigMap = testIssuer.asConfig();
testIssuerJson =
"{\n"
+ " \"aud\":\"audience\",\n"
+ + " \"tokenEndpoint\":\"https://issuer/token\",\n"
+ " \"wellKnownUrl\":\"wellknown\",\n"
+ " \"clientId\":\"clientid\",\n"
+ + " \"authorizationFlow\":\"code_pkce\",\n"
+ " \"jwksUrl\":[\"https://issuer/path\"],\n"
+ " \"name\":\"name\",\n"
+ " \"iss\":\"issuer\",\n"
@@ -89,6 +93,11 @@ public class JWTIssuerConfigTest extends SolrTestCase {
new JWTIssuerConfig(testIssuerConfigMap).isValid();
}
+ @Test(expected = SolrException.class)
+ public void setInvalidAuthorizationFlow() {
+ new JWTIssuerConfig("name").setAuthorizationFlow("invalid_flow");
+ }
+
@Test
public void parseJwkSet() throws Exception {
HashMap<String, Object> testJwks = new HashMap<>();
@@ -173,6 +182,7 @@ public class JWTIssuerConfigTest extends SolrTestCase {
assertEquals("https://acmepaymentscorp/oauth/jwks", config.getJwksUrl());
assertEquals("http://acmepaymentscorp", config.getIssuer());
assertEquals("http://acmepaymentscorp/oauth/auz/authorize", config.getAuthorizationEndpoint());
+ assertEquals("http://acmepaymentscorp/oauth/oauth20/token", config.getTokenEndpoint());
assertEquals(
Arrays.asList(
"READ", "WRITE", "DELETE", "openid", "scope", "profile", "email", "address", "phone"),
diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/jwt-authentication-plugin.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/jwt-authentication-plugin.adoc
index ec79558548e..e4de26a87ad 100644
--- a/solr/solr-ref-guide/modules/deployment-guide/pages/jwt-authentication-plugin.adoc
+++ b/solr/solr-ref-guide/modules/deployment-guide/pages/jwt-authentication-plugin.adoc
@@ -85,6 +85,8 @@ jwk ; As an alternative to `jwksUrl` you may provide a static J
iss ; Unique issuer id as configured on the IdP. Incoming tokens must have a matching `iss` claim. Also used to resolve issuer when multiple issuers configured. ; Auto configured if `wellKnownUrl` is provided
aud ; Validates that the `aud` (audience) claim equals this string ; Uses `clientId` if configured
authorizationEndpoint; The URL for the Id Provider's authorization endpoint ; Auto configured if `wellKnownUrl` is provided
+tokenEndpoint; The URL for the Id Provider's token endpoint ; Auto configured if `wellKnownUrl` is provided
+authorizationFlow; Specifies the OAuth 2.0 flow to be used. Supported flows are 'implicit' and 'code_pkce' (for authorization code with 'Proof Key for Code Exchange'). Note: 'implicit' is deprecated and it is highly recommended to use 'code_pkce' instead. ; implicit
|===
TIP: For backwards compatibility, all the configuration keys for the primary issuer may be configured as top-level keys, except `name`.