diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/utils/Constants.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/utils/Constants.java index 117f61227..7e03d8c85 100644 --- a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/utils/Constants.java +++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/utils/Constants.java @@ -414,6 +414,14 @@ public final class Constants { */ public static final String SPRINGDOC_NULLABLE_REQUEST_PARAMETER_ENABLED = "springdoc.nullable-request-parameter-enabled"; + /** + * The constant SPRINGDOC_SWAGGER_UI_OAUTH_PROXY_PREFIX. + */ + public static final String SPRINGDOC_SWAGGER_UI_OAUTH_PROXY_PREFIX = "springdoc.swagger-ui.oauth-proxy"; + /** + * The constant SPRINGDOC_SWAGGER_UI_OAUTH_PROXY_ENABLED. + */ + public static final String SPRINGDOC_SWAGGER_UI_OAUTH_PROXY_ENABLED = SPRINGDOC_SWAGGER_UI_OAUTH_PROXY_PREFIX + ".enabled"; /** * Instantiates a new Constants. */ diff --git a/springdoc-openapi-starter-webmvc-ui/src/main/java/org/springdoc/webmvc/ui/SwaggerConfig.java b/springdoc-openapi-starter-webmvc-ui/src/main/java/org/springdoc/webmvc/ui/SwaggerConfig.java index 00985a5ff..13f589286 100644 --- a/springdoc-openapi-starter-webmvc-ui/src/main/java/org/springdoc/webmvc/ui/SwaggerConfig.java +++ b/springdoc-openapi-starter-webmvc-ui/src/main/java/org/springdoc/webmvc/ui/SwaggerConfig.java @@ -37,6 +37,8 @@ import org.springdoc.core.providers.SpringWebProvider; import org.springdoc.webmvc.core.providers.SpringWebMvcProvider; +import org.springdoc.webmvc.ui.oauth.proxy.SwaggerOauthProxyController; +import org.springdoc.webmvc.ui.oauth.proxy.SwaggerOauthProxyProperties; import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties; import org.springframework.boot.actuate.autoconfigure.web.server.ConditionalOnManagementPort; import org.springframework.boot.actuate.autoconfigure.web.server.ManagementPortType; @@ -47,11 +49,13 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Lazy; import static org.springdoc.core.utils.Constants.SPRINGDOC_SWAGGER_UI_ENABLED; +import static org.springdoc.core.utils.Constants.SPRINGDOC_SWAGGER_UI_OAUTH_PROXY_ENABLED; import static org.springdoc.core.utils.Constants.SPRINGDOC_USE_MANAGEMENT_PORT; import static org.springdoc.core.utils.Constants.SPRINGDOC_USE_ROOT_PATH; @@ -63,6 +67,7 @@ */ @Lazy(false) @Configuration(proxyBeanMethods = false) +@EnableConfigurationProperties(SwaggerOauthProxyProperties.class) @ConditionalOnProperty(name = SPRINGDOC_SWAGGER_UI_ENABLED, matchIfMissing = true) @ConditionalOnWebApplication(type = Type.SERVLET) @ConditionalOnBean(SpringDocConfiguration.class) @@ -96,6 +101,20 @@ SpringWebProvider springWebProvider() { return new SpringWebMvcProvider(); } + /** + * To delegate Oauth2 authentication + * + * @param swaggerOauthProxyProperties to configure the authorization header + * @return the controller to redirect swagger authentication through application + */ + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(name = SPRINGDOC_SWAGGER_UI_OAUTH_PROXY_ENABLED, havingValue = "true") + @Lazy(false) + SwaggerOauthProxyController swaggerOauthProxyController(SwaggerOauthProxyProperties swaggerOauthProxyProperties) { + return new SwaggerOauthProxyController(swaggerOauthProxyProperties); + } + /** * Swagger config resource swagger config resource. * diff --git a/springdoc-openapi-starter-webmvc-ui/src/main/java/org/springdoc/webmvc/ui/oauth/proxy/SwaggerOauthProxyController.java b/springdoc-openapi-starter-webmvc-ui/src/main/java/org/springdoc/webmvc/ui/oauth/proxy/SwaggerOauthProxyController.java new file mode 100644 index 000000000..5561f7533 --- /dev/null +++ b/springdoc-openapi-starter-webmvc-ui/src/main/java/org/springdoc/webmvc/ui/oauth/proxy/SwaggerOauthProxyController.java @@ -0,0 +1,86 @@ +/* + * + * * + * * * + * * * * + * * * * * Copyright 2019-2024 the original author or authors. + * * * * * + * * * * * Licensed 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 + * * * * * + * * * * * https://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.springdoc.webmvc.ui.oauth.proxy; + +import io.swagger.v3.oas.annotations.Hidden; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.client.RestClient; +import org.springframework.web.server.ResponseStatusException; +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +@RestController +public class SwaggerOauthProxyController { + + private final String GRANT_TYPE_KEY = "grant_type"; + private final String CLIENT_CREDENTIALS_VALUE = "client_credentials"; + private final String CLIENT_ID_KEY = "client_id"; + private final String CLIENT_SECRET_KEY = "client_secret"; + private final String AUTHENTICATION_SCHEME_BASIC = "Basic"; + + private final RestClient restClient = RestClient.builder().build(); + private final SwaggerOauthProxyProperties swaggerOauthProxyProperties; + + @Autowired + public SwaggerOauthProxyController(SwaggerOauthProxyProperties swaggerOauthProxyProperties) { + this.swaggerOauthProxyProperties = swaggerOauthProxyProperties; + } + + @Hidden + @PostMapping(path = "${springdoc.swagger-ui.oauth-proxy.path}", produces = MediaType.APPLICATION_JSON_VALUE) + public String redirectSwaggerOauth(@RequestHeader(HttpHeaders.AUTHORIZATION) String authorizationHeader) { + + if (authorizationHeader != null && authorizationHeader.startsWith(AUTHENTICATION_SCHEME_BASIC)) { + + String base64Credentials = authorizationHeader.substring(AUTHENTICATION_SCHEME_BASIC.length()).trim(); + String credentials = new String(Base64.getDecoder().decode(base64Credentials), StandardCharsets.UTF_8); + String[] clientDetails = credentials.split(":", 2); + + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add(GRANT_TYPE_KEY, CLIENT_CREDENTIALS_VALUE); + body.add(CLIENT_ID_KEY, clientDetails[0]); + body.add(CLIENT_SECRET_KEY, clientDetails[1]); + + ResponseEntity response = restClient.post() + .uri(swaggerOauthProxyProperties.getOauthTokenUri()) + .body(body) + .retrieve() + .toEntity(String.class); + + return response.getBody(); + + } else { + throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Authorization header missing or not using Basic Auth"); + } + } +} diff --git a/springdoc-openapi-starter-webmvc-ui/src/main/java/org/springdoc/webmvc/ui/oauth/proxy/SwaggerOauthProxyProperties.java b/springdoc-openapi-starter-webmvc-ui/src/main/java/org/springdoc/webmvc/ui/oauth/proxy/SwaggerOauthProxyProperties.java new file mode 100644 index 000000000..25be5c0d8 --- /dev/null +++ b/springdoc-openapi-starter-webmvc-ui/src/main/java/org/springdoc/webmvc/ui/oauth/proxy/SwaggerOauthProxyProperties.java @@ -0,0 +1,85 @@ +/* + * + * * + * * * + * * * * + * * * * * Copyright 2019-2024 the original author or authors. + * * * * * + * * * * * Licensed 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 + * * * * * + * * * * * https://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.springdoc.webmvc.ui.oauth.proxy; + +import org.springdoc.core.utils.Constants; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.Errors; +import org.springframework.validation.ValidationUtils; +import org.springframework.validation.Validator; +import org.springframework.validation.annotation.Validated; +import java.io.Serializable; +import java.net.URI; +import static org.springdoc.core.utils.Constants.SPRINGDOC_ENABLED; + +@Validated +@ConfigurationProperties(prefix = Constants.SPRINGDOC_SWAGGER_UI_OAUTH_PROXY_PREFIX) +public class SwaggerOauthProxyProperties implements Validator { + + private boolean enabled; + private URI path; + private URI oauthTokenUri; + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public URI getPath() { + return path; + } + + public void setPath(URI path) { + this.path = path; + } + + public URI getOauthTokenUri() { + return oauthTokenUri; + } + + public void setOauthTokenUri(URI oauthTokenUri) { + this.oauthTokenUri = oauthTokenUri; + } + + @Override + public boolean supports(Class clazz) { + return SwaggerOauthProxyProperties.class.isAssignableFrom(clazz); + } + + @Override + public void validate(Object target, Errors errors) { + if (enabled) { + if (path == null) { + errors.rejectValue("path", "field.required"); + } + if (oauthTokenUri == null) { + errors.rejectValue("oauthTokenUri", "field.required"); + } + } + } +}