This is an automated email from the ASF dual-hosted git repository. reta pushed a commit to branch 4.0.x-fixes in repository https://gitbox.apache.org/repos/asf/cxf.git
commit e6023527ea86b0c229446843d2cf56593ca4bf01 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 (cherry picked from commit 579fcc6657235f680712261b06ce92e6ee0902af) --- .../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; }
