This is an automated email from the ASF dual-hosted git repository.
reta pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/cxf.git
The following commit(s) were added to refs/heads/main by this push:
new 579fcc6657 [CXF-9175] add support for OAuth2 config in swagger UI
(#2724)
579fcc6657 is described below
commit 579fcc6657235f680712261b06ce92e6ee0902af
Author: Gaƫtan Pitteloud <[email protected]>
AuthorDate: Sat Nov 15 19:45:11 2025 +0100
[CXF-9175] add support for OAuth2 config in swagger UI (#2724)
* [CXF-9175] add support for OAuth2 config in swagger UI
* [CXF-9175] fixed formatting
* [CXF-9175] fixed formatting
* [CXF-9175] added test, renamed attribute, fixed PMDM issues
---
.../cxf/jaxrs/swagger/ui/SwaggerUiConfig.java | 26 ++-
.../jaxrs/swagger/ui/SwaggerUiOAuth2Config.java | 205 +++++++++++++++++++++
.../cxf/jaxrs/swagger/ui/SwaggerUiService.java | 58 ++++--
.../openapi/SwaggerUiConfigurationTest.java | 43 ++++-
4 files changed, 308 insertions(+), 24 deletions(-)
diff --git
a/rt/rs/description-swagger-ui/src/main/java/org/apache/cxf/jaxrs/swagger/ui/SwaggerUiConfig.java
b/rt/rs/description-swagger-ui/src/main/java/org/apache/cxf/jaxrs/swagger/ui/SwaggerUiConfig.java
index 38db5328c7..49cf78e6e5 100644
---
a/rt/rs/description-swagger-ui/src/main/java/org/apache/cxf/jaxrs/swagger/ui/SwaggerUiConfig.java
+++
b/rt/rs/description-swagger-ui/src/main/java/org/apache/cxf/jaxrs/swagger/ui/SwaggerUiConfig.java
@@ -31,16 +31,16 @@ import org.apache.cxf.common.util.StringUtils;
public class SwaggerUiConfig {
// URL to fetch external configuration document from.
private String configUrl;
- // The url pointing to API definition (normally
+ // The url pointing to API definition (normally
// swagger.json/swagger.yaml/openapi.json/openapi.yaml).
private String url;
- // If set, enables filtering. The top bar will show an edit box that
+ // If set, enables filtering. The top bar will show an edit box that
// could be used to filter the tagged operations that are shown.
private String filter;
// Enables or disables deep linking for tags and operations.
private Boolean deepLinking;
- // Controls the display of operationId in operations list.
+ // Controls the display of operationId in operations list.
private Boolean displayOperationId;
// The default expansion depth for models (set to -1 completely hide the
models).
private Integer defaultModelsExpandDepth;
@@ -51,9 +51,9 @@ public class SwaggerUiConfig {
private String defaultModelRendering;
// Controls the display of the request duration (in milliseconds) for
Try-It-Out requests.
private Boolean displayRequestDuration;
- // Controls the default expansion setting for the operations and tags.
+ // Controls the default expansion setting for the operations and tags.
private String docExpansion;
- // If set, limits the number of tagged operations displayed to at most
this many.
+ // If set, limits the number of tagged operations displayed to at most
this many.
private Integer maxDisplayedTags;
// Controls the display of vendor extension (x-) fields and values.
private Boolean showExtensions;
@@ -66,6 +66,9 @@ public class SwaggerUiConfig {
// Enables overriding configuration parameters via URL search params. If
not explicitly set, it
// will be automatically set to true when setter for any other field is
called.
private Boolean queryConfigEnabled;
+ // Controls the OAuth config. If present, the SwaggerUIBundle
initialization will contain a call to initOAuth
+ // with the parameters contained here
+ private SwaggerUiOAuth2Config oAuth2Config;
public String getConfigUrl() {
return configUrl;
@@ -336,4 +339,17 @@ public class SwaggerUiConfig {
this.tryItOutEnabled = tryItOutEnabled;
setQueryConfigEnabledIfNeeded();
}
+
+ public SwaggerUiOAuth2Config getOAuth2Config() {
+ return oAuth2Config;
+ }
+
+ public void setOAuth2Config(SwaggerUiOAuth2Config oauth) {
+ this.oAuth2Config = oauth;
+ }
+
+ public SwaggerUiConfig oAuth2Config(SwaggerUiOAuth2Config oauth) {
+ setOAuth2Config(oauth);
+ return this;
+ }
}
diff --git
a/rt/rs/description-swagger-ui/src/main/java/org/apache/cxf/jaxrs/swagger/ui/SwaggerUiOAuth2Config.java
b/rt/rs/description-swagger-ui/src/main/java/org/apache/cxf/jaxrs/swagger/ui/SwaggerUiOAuth2Config.java
new file mode 100644
index 0000000000..cf553dd595
--- /dev/null
+++
b/rt/rs/description-swagger-ui/src/main/java/org/apache/cxf/jaxrs/swagger/ui/SwaggerUiOAuth2Config.java
@@ -0,0 +1,205 @@
+/**
+ * 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.cxf.jaxrs.swagger.ui;
+
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * Swagger UI OAuth2 configuration parameters, to be injected into swagger
initialization JS code (call to initOAuth()).
+ * @author Gaetan Pitteloud
+ * @see org.apache.cxf.jaxrs.swagger.ui.SwaggerUiService
+ */
+public class SwaggerUiOAuth2Config {
+
+ private String clientId;
+ private String clientSecret;
+ private String realm;
+ private String appName;
+ private List<String> scopes;
+ private Map<String, String> additionalQueryStringParams;
+ private Boolean useBasicAuthenticationWithAccessCodeGrant;
+ private Boolean usePkceWithAuthorizationCodeGrant;
+
+ public String getClientId() {
+ return clientId;
+ }
+
+ public void setClientId(String clientId) {
+ this.clientId = clientId;
+ }
+
+ public SwaggerUiOAuth2Config clientId(String cid) {
+ setClientId(cid);
+ return this;
+ }
+
+ public String getClientSecret() {
+ return clientSecret;
+ }
+
+ public void setClientSecret(String clientSecret) {
+ this.clientSecret = clientSecret;
+ }
+
+ public SwaggerUiOAuth2Config clientSecret(String secret) {
+ setClientSecret(secret);
+ return this;
+ }
+
+ public String getRealm() {
+ return realm;
+ }
+
+ public void setRealm(String realm) {
+ this.realm = realm;
+ }
+
+ public SwaggerUiOAuth2Config realm(String re) {
+ setRealm(re);
+ return this;
+ }
+
+ public String getAppName() {
+ return appName;
+ }
+
+ public void setAppName(String appName) {
+ this.appName = appName;
+ }
+
+ public SwaggerUiOAuth2Config appName(String name) {
+ setAppName(name);
+ return this;
+ }
+
+ public List<String> getScopes() {
+ return scopes;
+ }
+
+ public void setScopes(List<String> scopes) {
+ this.scopes = scopes;
+ }
+
+ public SwaggerUiOAuth2Config scopes(List<String> scopesList) {
+ setScopes(scopesList);
+ return this;
+ }
+
+ public Map<String, String> getAdditionalQueryStringParams() {
+ return additionalQueryStringParams;
+ }
+
+ public void setAdditionalQueryStringParams(Map<String, String>
additionalQueryStringParams) {
+ this.additionalQueryStringParams = additionalQueryStringParams;
+ }
+
+ public SwaggerUiOAuth2Config additionalQueryStringParams(Map<String,
String> additionalParams) {
+ setAdditionalQueryStringParams(additionalParams);
+ return this;
+ }
+
+ public Boolean getUseBasicAuthenticationWithAccessCodeGrant() {
+ return useBasicAuthenticationWithAccessCodeGrant;
+ }
+
+ public void setUseBasicAuthenticationWithAccessCodeGrant(Boolean
useBasicAuthenticationWithAccessCodeGrant) {
+ this.useBasicAuthenticationWithAccessCodeGrant =
useBasicAuthenticationWithAccessCodeGrant;
+ }
+
+ public SwaggerUiOAuth2Config
useBasicAuthenticationWithAccessCodeGrant(Boolean basicAuth) {
+ setUseBasicAuthenticationWithAccessCodeGrant(basicAuth);
+ return this;
+ }
+
+ public Boolean getUsePkceWithAuthorizationCodeGrant() {
+ return usePkceWithAuthorizationCodeGrant;
+ }
+
+ public void setUsePkceWithAuthorizationCodeGrant(Boolean
usePkceWithAuthorizationCodeGrant) {
+ this.usePkceWithAuthorizationCodeGrant =
usePkceWithAuthorizationCodeGrant;
+ }
+
+ public SwaggerUiOAuth2Config usePkceWithAuthorizationCodeGrant(Boolean
pkce) {
+ setUsePkceWithAuthorizationCodeGrant(pkce);
+ return this;
+ }
+
+ /**
+ * Print this object as json, so that it can be injected into JavaScript
code
+ * @return JSON for this object
+ */
+ public String toJsonString() {
+ // don't add a json dependency
+ final StringBuilder json = new StringBuilder("{");
+ addStringField(json, "clientId", clientId);
+ addStringField(json, "clientSecret", clientSecret);
+ addStringField(json, "realm", realm);
+ addStringField(json, "appName", appName);
+ addListField(json, "scopes", scopes);
+ addMapField(json, "additionalQueryStringParams",
additionalQueryStringParams);
+ addBooleanField(json, "useBasicAuthenticationWithAccessCodeGrant",
useBasicAuthenticationWithAccessCodeGrant);
+ addBooleanField(json, "usePkceWithAuthorizationCodeGrant",
usePkceWithAuthorizationCodeGrant);
+ // remove last printed ","
+ if (json.toString().endsWith(",")) {
+ json.delete(json.length() - 1, json.length());
+ }
+ return json.append('}').toString();
+ }
+
+ private void addStringField(StringBuilder json, String name, String value)
{
+ if (value != null) {
+
json.append(quote(name)).append(':').append(quote(value)).append(',');
+ }
+ }
+
+ private void addBooleanField(StringBuilder json, String name, Boolean
value) {
+ if (value != null) {
+ json.append(quote(name)).append(':').append(value).append(',');
+ }
+ }
+
+ private void addListField(StringBuilder json, String name, List<String>
value) {
+ if (value != null) {
+ json.append(quote(name)).append(':').append(
+ value.stream().map(this::quote)
+ .collect(Collectors.joining(",", "[", "]"))
+ ).append(',');
+ }
+ }
+
+ private void addMapField(StringBuilder json, String name, Map<String,
String> value) {
+ if (value != null) {
+ json.append(quote(name)).append(':').append(
+ value.entrySet().stream().map(this::entryToString)
+ .collect(Collectors.joining(",", "{", "}"))
+ ).append(',');
+ }
+ }
+
+ private String quote(String s) {
+ return '"' + s + '"';
+ }
+
+ private String entryToString(Map.Entry<String, String> entry) {
+ return quote(entry.getKey()) + ':' + quote(entry.getValue());
+ }
+}
diff --git
a/rt/rs/description-swagger-ui/src/main/java/org/apache/cxf/jaxrs/swagger/ui/SwaggerUiService.java
b/rt/rs/description-swagger-ui/src/main/java/org/apache/cxf/jaxrs/swagger/ui/SwaggerUiService.java
index ba31682e90..ea40c1200e 100644
---
a/rt/rs/description-swagger-ui/src/main/java/org/apache/cxf/jaxrs/swagger/ui/SwaggerUiService.java
+++
b/rt/rs/description-swagger-ui/src/main/java/org/apache/cxf/jaxrs/swagger/ui/SwaggerUiService.java
@@ -116,7 +116,7 @@ public class SwaggerUiService {
.entrySet()
.stream()
.reduce(
- uriInfo.getRequestUriBuilder(),
+ uriInfo.getRequestUriBuilder(),
(b, e) -> b.queryParam(e.getKey(),
e.getValue()),
(left, right) -> left
);
@@ -124,17 +124,24 @@ public class SwaggerUiService {
}
}
- // Since Swagger UI 4.1.3, passing the default URL as query
parameter,
- // e.g. `?url=swagger.json` is disabled by default due to
security concerns.
- final boolean hasUrlPlaceholder = path.endsWith("/index.html")
- || path.endsWith("/swagger-initializer.js");
- if (hasUrlPlaceholder &&
!Boolean.TRUE.equals(config.isQueryConfigEnabled())) {
+ // customize the following swagger resources
+ if (path.endsWith("/index.html") ||
path.endsWith("/swagger-initializer.js")) {
+ // Since Swagger UI 4.1.3, passing the default URL as
query parameter,
+ // e.g. `?url=swagger.json` is disabled by default due to
security concerns.
final String url = config.getUrl();
- if (!StringUtils.isEmpty(url)) {
+ final SwaggerUiOAuth2Config oAuthConfig =
config.getOAuth2Config();
+ if (!StringUtils.isEmpty(url) || oAuthConfig != null) {
try (InputStream in = resourceURL.openStream()) {
- final String index =
replaceUrl(IOUtils.readStringFromStream(in), url);
- final ResponseBuilder rb = Response.ok(index);
-
+ String resource = IOUtils.readStringFromStream(in);
+ if (!StringUtils.isEmpty(url) &&
!Boolean.TRUE.equals(config.isQueryConfigEnabled())) {
+ resource = replaceUrl(resource, url);
+ }
+ // don't try to replace in index.html
+ if (path.endsWith("/swagger-initializer.js") &&
oAuthConfig != null) {
+ resource = addOAuth2Init(resource,
oAuthConfig);
+ }
+ // render the modified file
+ final ResponseBuilder rb = Response.ok(resource);
if (mediaType != null) {
rb.type(mediaType);
}
@@ -156,21 +163,44 @@ public class SwaggerUiService {
}
/**
- * Replaces the URL inside Swagger UI index.html file. The implementation
does not attempt to
- * read the file and parse it as valid HTML but uses straightforward
approach by looking for
+ * Replaces the URL inside Swagger UI index.html file. The implementation
does not attempt to
+ * read the file and parse it as valid HTML but uses straightforward
approach by looking for
* the URL pattern of the SwaggerUIBundle initialization and replacing it.
* @param index index.html file content
- * @param replacement replacement URL
+ * @param replacement replacement URL
* @return index.html file content with replaced URL
*/
protected String replaceUrl(final String index, final String replacement) {
final Matcher matcher = URL_PATTERN.matcher(index);
if (matcher.find()) {
- return index.substring(0, matcher.start(1)) + replacement +
index.substring(matcher.end(1));
+ return index.substring(0, matcher.start(1)) + replacement +
index.substring(matcher.end(1));
}
return index;
}
+
+ /**
+ * Add a JavaScript block in swagger-initializer.js to initialize OAuth2
with parameters read from the provided
+ * OAuth2 config. The implementation jumps after the initialization of
SwaggerUIBundle (variable is expected to
+ * be 'ui') and adds a call to <code>iniOAuth()</code> with the provided
parameters.
+ * @param original original contents of swagger-initializer.js
+ * @param oAuth2Config OAuth config
+ * @return modified swagger-initializer.js
+ */
+ protected String addOAuth2Init(String original, SwaggerUiOAuth2Config
oAuth2Config) {
+ final int startSwaggerConstructIndex =
original.indexOf("SwaggerUIBundle({");
+ if (startSwaggerConstructIndex < 0) {
+ return original;
+ }
+ int endSwaggerConstructIndex = original.indexOf("});",
startSwaggerConstructIndex);
+ if (endSwaggerConstructIndex < 0) {
+ return original;
+ } else {
+ endSwaggerConstructIndex += 3;
+ }
+ String initJs = "\n ui.initOAuth(" + oAuth2Config.toJsonString() +
")\n";
+ return original.substring(0, endSwaggerConstructIndex) + initJs +
original.substring(endSwaggerConstructIndex);
+ }
}
diff --git
a/systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/description/openapi/SwaggerUiConfigurationTest.java
b/systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/description/openapi/SwaggerUiConfigurationTest.java
index 4ac2082afb..ec68a4af2e 100644
---
a/systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/description/openapi/SwaggerUiConfigurationTest.java
+++
b/systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/description/openapi/SwaggerUiConfigurationTest.java
@@ -19,7 +19,11 @@
package org.apache.cxf.systest.jaxrs.description.openapi;
import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.jakarta.rs.json.JacksonJsonProvider;
import jakarta.ws.rs.core.MediaType;
@@ -31,12 +35,14 @@ import
org.apache.cxf.jaxrs.lifecycle.SingletonResourceProvider;
import org.apache.cxf.jaxrs.model.AbstractResourceInfo;
import org.apache.cxf.jaxrs.openapi.OpenApiFeature;
import org.apache.cxf.jaxrs.swagger.ui.SwaggerUiConfig;
+import org.apache.cxf.jaxrs.swagger.ui.SwaggerUiOAuth2Config;
import org.apache.cxf.testutil.common.AbstractClientServerTestBase;
import org.apache.cxf.testutil.common.AbstractServerTestServerBase;
import org.junit.BeforeClass;
import org.junit.Test;
+import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_EMPTY;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.MatcherAssert.assertThat;
@@ -45,7 +51,12 @@ import static org.junit.Assert.assertTrue;
public class SwaggerUiConfigurationTest extends AbstractClientServerTestBase {
private static final String PORT =
allocatePort(SwaggerUiConfigurationTest.class);
-
+ private static final SwaggerUiOAuth2Config OAUTH2_CONFIG = new
SwaggerUiOAuth2Config()
+ .usePkceWithAuthorizationCodeGrant(true)
+ .appName("CXF Test App")
+ .additionalQueryStringParams(Map.of("key1", "value1", "key2",
"value2"))
+ .scopes(List.of("scope1", "scope2"));
+
public static class Server extends AbstractServerTestServerBase {
@Override
@@ -57,7 +68,10 @@ public class SwaggerUiConfigurationTest extends
AbstractClientServerTestBase {
sf.setProvider(new JacksonJsonProvider());
final OpenApiFeature feature = new OpenApiFeature();
feature.setRunAsFilter(false);
- feature.setSwaggerUiConfig(new
SwaggerUiConfig().url("/openapi.json").queryConfigEnabled(false));
+ feature.setSwaggerUiConfig(new SwaggerUiConfig()
+ .url("/openapi.json")
+ .queryConfigEnabled(false)
+ .oAuth2Config(OAUTH2_CONFIG));
sf.setFeatures(Arrays.asList(feature));
sf.setAddress("http://localhost:" + PORT + "/");
return sf.create();
@@ -109,7 +123,7 @@ public class SwaggerUiConfigurationTest extends
AbstractClientServerTestBase {
@Test
public void testUiRootResourcePicksUrlFromConfigurationOnly() {
- // Test that Swagger UI URL is picked from configuration only and
+ // Test that Swagger UI URL is picked from configuration only and
// never from the query string (when query config is disabled).
WebClient uiClient = WebClient
.create("http://localhost:" + getPort() + "/api-docs")
@@ -130,7 +144,7 @@ public class SwaggerUiConfigurationTest extends
AbstractClientServerTestBase {
WebClient uiClient = WebClient
.create("http://localhost:" + getPort() + "/api-docs")
.path("/swagger-initializer.js")
- .query("another-openapi.json")
+ .query("url", "another-openapi.json")
.accept("*/*");
try (Response response = uiClient.get()) {
@@ -139,7 +153,26 @@ public class SwaggerUiConfigurationTest extends
AbstractClientServerTestBase {
assertFalse(jsCode.contains("another-openapi.json"));
}
}
-
+
+ @Test
+ public void testUiRootResourceAddsOAuth2ConfigAsConfigured() throws
Exception {
+ // With query config disabled or unset, we replace the url value in
the Swagger resource with the one
+ // configured in SwaggerUiConfig, and ignore the one in query
parameter.
+ WebClient uiClient = WebClient
+ .create("http://localhost:" + getPort() + "/api-docs")
+ .path("/swagger-initializer.js")
+ .accept("*/*");
+
+ try (Response response = uiClient.get()) {
+ String jsCode = response.readEntity(String.class);
+ final JsonMapper mapper = JsonMapper.builder()
+
.defaultPropertyInclusion(JsonInclude.Value.construct(NON_EMPTY, NON_EMPTY))
+ .build();
+ final String expectedConfigAsJson =
mapper.writeValueAsString(OAUTH2_CONFIG);
+ assertThat(jsCode, containsString("ui.initOAuth(" +
expectedConfigAsJson + ")"));
+ }
+ }
+
public static String getPort() {
return PORT;
}