diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt index e7e31b686d5..5bbaa79bcf3 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 void doGet(HttpServletRequest _request, HttpServletResponse _response) th 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 void doGet(HttpServletRequest _request, HttpServletResponse _response) th } } } + + /** + * 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 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 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 headerNameCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor 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.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 void init(Map pluginConfig) { } 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 @@ private Optional parseIssuerFromTopLevelConfig(Map 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 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.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.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 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 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 void init() { 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 @@ protected void parseConfigMap(Map configMap) { 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 @@ protected void parseConfigMap(Map configMap) { 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 JWTIssuerConfig setAuthorizationEndpoint(String authorizationEndpoint) { 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 asConfig() { HashMap config = new HashMap<>(); putIfNotNull(config, PARAM_ISS_NAME, name); @@ -324,6 +381,8 @@ public Map asConfig() { 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 void testStaticJwtKeys() throws Exception { 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 void infoRequestValidateXSolrAuthHeadersBlockUnknownFalse() throws Except 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.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 void bothJwksUrlAndJwkFails() { 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 void xSolrAuthDataHeader() { 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 void parseCertsFromFile() throws IOException { .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 void setUp() throws Exception { .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 void parseConfigMapNoName() { new JWTIssuerConfig(testIssuerConfigMap).isValid(); } + @Test(expected = SolrException.class) + public void setInvalidAuthorizationFlow() { + new JWTIssuerConfig("name").setAuthorizationFlow("invalid_flow"); + } + @Test public void parseJwkSet() throws Exception { HashMap testJwks = new HashMap<>(); @@ -173,6 +182,7 @@ public void wellKnownConfigFromString() throws IOException { 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`.