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;
     }

Reply via email to