Skip to content

Commit ac55722

Browse files
committed
add support for Spring Authorization Server. Fixes #2094
1 parent db74b05 commit ac55722

File tree

14 files changed

+1474
-0
lines changed

14 files changed

+1474
-0
lines changed

pom.xml

+6
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
<jjwt.version>0.9.1</jjwt.version>
7272
<therapi-runtime-javadoc.version>0.15.0</therapi-runtime-javadoc.version>
7373
<spring-cloud-function.version>4.0.0</spring-cloud-function.version>
74+
<spring-security-oauth2-authorization-server.version>1.0.1</spring-security-oauth2-authorization-server.version>
7475
</properties>
7576

7677
<dependencyManagement>
@@ -140,6 +141,11 @@
140141
<artifactId>spring-cloud-starter-function-webflux</artifactId>
141142
<version>${spring-cloud-function.version}</version>
142143
</dependency>
144+
<dependency>
145+
<groupId>org.springframework.security</groupId>
146+
<artifactId>spring-security-oauth2-authorization-server</artifactId>
147+
<version>${spring-security-oauth2-authorization-server.version}</version>
148+
</dependency>
143149
<!-- SpringDoc -->
144150
<dependency>
145151
<groupId>org.springdoc</groupId>

springdoc-openapi-starter-common/pom.xml

+5
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@
5656
<artifactId>spring-security-config</artifactId>
5757
<optional>true</optional>
5858
</dependency>
59+
<dependency>
60+
<groupId>org.springframework.security</groupId>
61+
<artifactId>spring-security-oauth2-authorization-server</artifactId>
62+
<optional>true</optional>
63+
</dependency>
5964
<!-- Kotlin -->
6065
<dependency>
6166
<groupId>com.fasterxml.jackson.module</groupId>

springdoc-openapi-starter-common/src/main/java/org/springdoc/core/configuration/SpringDocSecurityConfiguration.java

+21
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
import org.apache.commons.lang3.reflect.FieldUtils;
4242
import org.slf4j.Logger;
4343
import org.slf4j.LoggerFactory;
44+
import org.springdoc.core.customizers.GlobalOpenApiCustomizer;
4445
import org.springdoc.core.customizers.OpenApiCustomizer;
4546

4647
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
@@ -55,6 +56,7 @@
5556
import org.springframework.http.HttpStatus;
5657
import org.springframework.security.core.Authentication;
5758
import org.springframework.security.core.annotation.AuthenticationPrincipal;
59+
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
5860
import org.springframework.security.web.FilterChainProxy;
5961
import org.springframework.security.web.SecurityFilterChain;
6062
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
@@ -64,6 +66,7 @@
6466
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
6567

6668
import static org.springdoc.core.utils.Constants.SPRINGDOC_SHOW_LOGIN_ENDPOINT;
69+
import static org.springdoc.core.utils.Constants.SPRINGDOC_SHOW_OAUTH2_ENDPOINTS;
6770
import static org.springdoc.core.utils.SpringDocUtils.getConfig;
6871

6972
/**
@@ -162,4 +165,22 @@ OpenApiCustomizer springSecurityLoginEndpointCustomiser(ApplicationContext appli
162165
};
163166
}
164167
}
168+
169+
@Lazy(false)
170+
@Configuration(proxyBeanMethods = false)
171+
@ConditionalOnClass(OAuth2AuthorizationService.class)
172+
class SpringDocSecurityOAuth2Configuration {
173+
174+
/**
175+
* Spring security OAuth2 endpoint OpenAPI customizer.
176+
*
177+
* @return the open api customizer
178+
*/
179+
@Bean
180+
@ConditionalOnProperty(SPRINGDOC_SHOW_OAUTH2_ENDPOINTS)
181+
@Lazy(false)
182+
GlobalOpenApiCustomizer springDocSecurityOAuth2Customizer() {
183+
return new SpringDocSecurityOAuth2Customizer();
184+
}
185+
}
165186
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,318 @@
1+
package org.springdoc.core.configuration;
2+
3+
import java.lang.reflect.Field;
4+
5+
import com.nimbusds.jose.jwk.JWKSet;
6+
import io.swagger.v3.oas.annotations.enums.ParameterIn;
7+
import io.swagger.v3.oas.models.OpenAPI;
8+
import io.swagger.v3.oas.models.Operation;
9+
import io.swagger.v3.oas.models.PathItem;
10+
import io.swagger.v3.oas.models.headers.Header;
11+
import io.swagger.v3.oas.models.media.Content;
12+
import io.swagger.v3.oas.models.media.MediaType;
13+
import io.swagger.v3.oas.models.media.ObjectSchema;
14+
import io.swagger.v3.oas.models.media.Schema;
15+
import io.swagger.v3.oas.models.media.StringSchema;
16+
import io.swagger.v3.oas.models.parameters.Parameter;
17+
import io.swagger.v3.oas.models.parameters.RequestBody;
18+
import io.swagger.v3.oas.models.responses.ApiResponse;
19+
import io.swagger.v3.oas.models.responses.ApiResponses;
20+
import org.apache.commons.lang3.reflect.FieldUtils;
21+
import org.slf4j.Logger;
22+
import org.slf4j.LoggerFactory;
23+
import org.springdoc.core.customizers.GlobalOpenApiCustomizer;
24+
import org.springdoc.core.utils.SpringDocAnnotationsUtils;
25+
26+
import org.springframework.beans.BeansException;
27+
import org.springframework.context.ApplicationContext;
28+
import org.springframework.context.ApplicationContextAware;
29+
import org.springframework.http.HttpMethod;
30+
import org.springframework.http.HttpStatus;
31+
import org.springframework.security.oauth2.core.OAuth2Error;
32+
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
33+
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
34+
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationServerMetadata;
35+
import org.springframework.security.oauth2.server.authorization.OAuth2TokenIntrospection;
36+
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationConsentAuthenticationToken;
37+
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenRevocationAuthenticationToken;
38+
import org.springframework.security.oauth2.server.authorization.web.NimbusJwkSetEndpointFilter;
39+
import org.springframework.security.oauth2.server.authorization.web.OAuth2AuthorizationEndpointFilter;
40+
import org.springframework.security.oauth2.server.authorization.web.OAuth2AuthorizationServerMetadataEndpointFilter;
41+
import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenEndpointFilter;
42+
import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenIntrospectionEndpointFilter;
43+
import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenRevocationEndpointFilter;
44+
import org.springframework.security.web.FilterChainProxy;
45+
import org.springframework.security.web.SecurityFilterChain;
46+
import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer;
47+
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
48+
import org.springframework.security.web.util.matcher.OrRequestMatcher;
49+
import org.springframework.security.web.util.matcher.RequestMatcher;
50+
51+
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
52+
53+
/**
54+
* The type Spring doc security o auth 2 customizer.
55+
*
56+
* @author bnasslahsen
57+
*/
58+
public class SpringDocSecurityOAuth2Customizer implements GlobalOpenApiCustomizer, ApplicationContextAware {
59+
60+
/**
61+
* The constant LOGGER.
62+
*/
63+
private static final Logger LOGGER = LoggerFactory.getLogger(SpringDocSecurityOAuth2Customizer.class);
64+
65+
/**
66+
* The constant OAUTH2_ENDPOINT_TAG.
67+
*/
68+
private static final String OAUTH2_ENDPOINT_TAG = "authorization-server-endpoints";
69+
70+
/**
71+
* The Context.
72+
*/
73+
private ApplicationContext applicationContext;
74+
75+
@Override
76+
public void customise(OpenAPI openAPI) {
77+
FilterChainProxy filterChainProxy = applicationContext.getBean(AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME, FilterChainProxy.class);
78+
for (SecurityFilterChain filterChain : filterChainProxy.getFilterChains()) {
79+
getNimbusJwkSetEndpoint(openAPI, filterChain);
80+
getOAuth2AuthorizationServerMetadataEndpoint(openAPI, filterChain);
81+
getOAuth2TokenEndpoint(openAPI, filterChain);
82+
getOAuth2AuthorizationEndpoint(openAPI, filterChain);
83+
getOAuth2TokenIntrospectionEndpointFilter(openAPI, filterChain);
84+
getOAuth2TokenRevocationEndpointFilter(openAPI, filterChain);
85+
}
86+
}
87+
88+
/**
89+
* Gets o auth 2 token revocation endpoint filter.
90+
*
91+
* @param openAPI the open api
92+
* @param securityFilterChain the security filter chain
93+
*/
94+
private void getOAuth2TokenRevocationEndpointFilter(OpenAPI openAPI, SecurityFilterChain securityFilterChain) {
95+
Object oAuth2EndpointFilter =
96+
new SpringDocSecurityOAuth2EndpointUtils(OAuth2TokenRevocationEndpointFilter.class).findEndpoint(securityFilterChain);
97+
if (oAuth2EndpointFilter != null) {
98+
ApiResponses apiResponses = buildApiResponsesWithBadRequest(SpringDocAnnotationsUtils.resolveSchemaFromType(OAuth2TokenRevocationAuthenticationToken.class, openAPI.getComponents(), null), openAPI);
99+
100+
Operation operation = buildOperation(apiResponses);
101+
Schema<?> schema = new ObjectSchema()
102+
.addProperty("token", new StringSchema())
103+
.addProperty(OAuth2ParameterNames.TOKEN_TYPE_HINT, new StringSchema());
104+
105+
String mediaType = org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED_VALUE;
106+
RequestBody requestBody = new RequestBody().content(new Content().addMediaType(mediaType, new MediaType().schema(schema)));
107+
operation.setRequestBody(requestBody);
108+
buildPath(oAuth2EndpointFilter, "tokenRevocationEndpointMatcher", openAPI, operation, HttpMethod.POST);
109+
}
110+
}
111+
112+
/**
113+
* Gets o auth 2 token introspection endpoint filter.
114+
*
115+
* @param openAPI the open api
116+
* @param securityFilterChain the security filter chain
117+
*/
118+
private void getOAuth2TokenIntrospectionEndpointFilter(OpenAPI openAPI, SecurityFilterChain securityFilterChain) {
119+
Object oAuth2EndpointFilter =
120+
new SpringDocSecurityOAuth2EndpointUtils(OAuth2TokenIntrospectionEndpointFilter.class).findEndpoint(securityFilterChain);
121+
if (oAuth2EndpointFilter != null) {
122+
ApiResponses apiResponses = buildApiResponsesWithBadRequest(SpringDocAnnotationsUtils.resolveSchemaFromType(OAuth2TokenIntrospection.class, openAPI.getComponents(), null), openAPI);
123+
Operation operation = buildOperation(apiResponses);
124+
Schema<?> schema = new ObjectSchema()
125+
.addProperty("token", new StringSchema())
126+
.addProperty(OAuth2ParameterNames.TOKEN_TYPE_HINT, new StringSchema())
127+
.addProperty("additionalParameters", new ObjectSchema().additionalProperties(new StringSchema()));
128+
129+
String mediaType = org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED_VALUE;
130+
RequestBody requestBody = new RequestBody().content(new Content().addMediaType(mediaType, new MediaType().schema(schema)));
131+
operation.setRequestBody(requestBody);
132+
buildPath(oAuth2EndpointFilter, "tokenIntrospectionEndpointMatcher", openAPI, operation, HttpMethod.POST);
133+
}
134+
}
135+
136+
/**
137+
* Gets o auth 2 authorization server metadata endpoint.
138+
*
139+
* @param openAPI the open api
140+
* @param securityFilterChain the security filter chain
141+
*/
142+
private void getOAuth2AuthorizationServerMetadataEndpoint(OpenAPI openAPI, SecurityFilterChain securityFilterChain) {
143+
Object oAuth2EndpointFilter =
144+
new SpringDocSecurityOAuth2EndpointUtils(OAuth2AuthorizationServerMetadataEndpointFilter.class).findEndpoint(securityFilterChain);
145+
if (oAuth2EndpointFilter != null) {
146+
ApiResponses apiResponses = buildApiResponses(SpringDocAnnotationsUtils.resolveSchemaFromType(OAuth2AuthorizationServerMetadata.class, openAPI.getComponents(), null), openAPI);
147+
Operation operation = buildOperation(apiResponses);
148+
buildPath(oAuth2EndpointFilter, "requestMatcher", openAPI, operation, HttpMethod.GET);
149+
}
150+
}
151+
152+
/**
153+
* Gets nimbus jwk set endpoint.
154+
*
155+
* @param openAPI the open api
156+
* @param securityFilterChain the security filter chain
157+
*/
158+
private void getNimbusJwkSetEndpoint(OpenAPI openAPI, SecurityFilterChain securityFilterChain) {
159+
Object oAuth2EndpointFilter =
160+
new SpringDocSecurityOAuth2EndpointUtils(NimbusJwkSetEndpointFilter.class).findEndpoint(securityFilterChain);
161+
if (oAuth2EndpointFilter != null) {
162+
ApiResponses apiResponses = buildApiResponses(SpringDocAnnotationsUtils.resolveSchemaFromType(JWKSet.class, openAPI.getComponents(), null), openAPI);
163+
Operation operation = buildOperation(apiResponses);
164+
operation.responses(apiResponses);
165+
buildPath(oAuth2EndpointFilter, "requestMatcher", openAPI, operation, HttpMethod.GET);
166+
}
167+
}
168+
169+
/**
170+
* Gets o auth 2 token endpoint.
171+
*
172+
* @param openAPI the open api
173+
* @param securityFilterChain the security filter chain
174+
*/
175+
private void getOAuth2TokenEndpoint(OpenAPI openAPI, SecurityFilterChain securityFilterChain) {
176+
Object oAuth2EndpointFilter =
177+
new SpringDocSecurityOAuth2EndpointUtils(OAuth2TokenEndpointFilter.class).findEndpoint(securityFilterChain);
178+
179+
if (oAuth2EndpointFilter != null) {
180+
ApiResponses apiResponses = buildApiResponsesWithBadRequest(SpringDocAnnotationsUtils.resolveSchemaFromType(OAuth2AccessTokenResponse.class, openAPI.getComponents(), null), openAPI);
181+
buildOAuth2Error(openAPI, apiResponses, HttpStatus.UNAUTHORIZED);
182+
Operation operation = buildOperation(apiResponses);
183+
Schema<?> schema = new ObjectSchema().additionalProperties(new StringSchema());
184+
operation.addParametersItem(new Parameter().name("parameters").in(ParameterIn.QUERY.toString()).schema(schema));
185+
buildPath(oAuth2EndpointFilter, "tokenEndpointMatcher", openAPI, operation, HttpMethod.POST);
186+
}
187+
}
188+
189+
/**
190+
* Gets o auth 2 authorization endpoint.
191+
*
192+
* @param openAPI the open api
193+
* @param securityFilterChain the security filter chain
194+
*/
195+
private void getOAuth2AuthorizationEndpoint(OpenAPI openAPI, SecurityFilterChain securityFilterChain) {
196+
Object oAuth2EndpointFilter =
197+
new SpringDocSecurityOAuth2EndpointUtils(OAuth2AuthorizationEndpointFilter.class).findEndpoint(securityFilterChain);
198+
if (oAuth2EndpointFilter != null) {
199+
ApiResponses apiResponses = buildApiResponsesWithBadRequest(SpringDocAnnotationsUtils.resolveSchemaFromType(OAuth2AuthorizationConsentAuthenticationToken.class, openAPI.getComponents(), null), openAPI);
200+
apiResponses.addApiResponse(String.valueOf(HttpStatus.MOVED_TEMPORARILY.value()),
201+
new ApiResponse().description(HttpStatus.MOVED_TEMPORARILY.getReasonPhrase())
202+
.addHeaderObject("Location", new Header().schema(new StringSchema())));
203+
Operation operation = buildOperation(apiResponses);
204+
Schema<?> schema = new ObjectSchema().additionalProperties(new StringSchema());
205+
operation.addParametersItem(new Parameter().name("parameters").in(ParameterIn.QUERY.toString()).schema(schema));
206+
buildPath(oAuth2EndpointFilter, "authorizationEndpointMatcher", openAPI, operation, HttpMethod.POST);
207+
}
208+
}
209+
210+
/**
211+
* Build operation operation.
212+
*
213+
* @param apiResponses the api responses
214+
* @return the operation
215+
*/
216+
private Operation buildOperation(ApiResponses apiResponses) {
217+
Operation operation = new Operation();
218+
operation.addTagsItem(OAUTH2_ENDPOINT_TAG);
219+
operation.responses(apiResponses);
220+
return operation;
221+
}
222+
223+
/**
224+
* Build api responses api responses.
225+
*
226+
* @param schema the schema
227+
* @param openAPI the open api
228+
* @return the api responses
229+
*/
230+
private ApiResponses buildApiResponses(Schema schema, OpenAPI openAPI) {
231+
ApiResponses apiResponses = new ApiResponses();
232+
ApiResponse response = new ApiResponse().description(HttpStatus.OK.getReasonPhrase()).content(new Content().addMediaType(
233+
APPLICATION_JSON_VALUE,
234+
new MediaType().schema(schema)));
235+
apiResponses.addApiResponse(String.valueOf(HttpStatus.OK.value()), response);
236+
apiResponses.addApiResponse(String.valueOf(HttpStatus.INTERNAL_SERVER_ERROR.value()), new ApiResponse().description(HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase()));
237+
return apiResponses;
238+
}
239+
240+
/**
241+
* Build api responses with bad request api responses.
242+
*
243+
* @param schema the schema
244+
* @param openAPI the open api
245+
* @return the api responses
246+
*/
247+
private ApiResponses buildApiResponsesWithBadRequest(Schema schema, OpenAPI openAPI) {
248+
ApiResponses apiResponses = buildApiResponses(schema, openAPI);
249+
buildOAuth2Error(openAPI, apiResponses, HttpStatus.BAD_REQUEST);
250+
return apiResponses;
251+
}
252+
253+
/**
254+
* Build o auth 2 error.
255+
*
256+
* @param openAPI the open api
257+
* @param apiResponses the api responses
258+
* @param httpStatus the http status
259+
*/
260+
private static void buildOAuth2Error(OpenAPI openAPI, ApiResponses apiResponses, HttpStatus httpStatus) {
261+
Schema oAuth2ErrorSchema = SpringDocAnnotationsUtils.resolveSchemaFromType(OAuth2Error.class, openAPI.getComponents(), null);
262+
apiResponses.addApiResponse(String.valueOf(httpStatus.value()), new ApiResponse().description(httpStatus.getReasonPhrase()).content(new Content().addMediaType(
263+
APPLICATION_JSON_VALUE,
264+
new MediaType().schema(oAuth2ErrorSchema))));
265+
}
266+
267+
/**
268+
* Build path.
269+
*
270+
* @param oAuth2EndpointFilter the o auth 2 endpoint filter
271+
* @param authorizationEndpointMatcher the authorization endpoint matcher
272+
* @param openAPI the open api
273+
* @param operation the operation
274+
* @param requestMethod the request method
275+
*/
276+
private void buildPath(Object oAuth2EndpointFilter, String authorizationEndpointMatcher, OpenAPI openAPI, Operation operation, HttpMethod requestMethod) {
277+
try {
278+
Field tokenEndpointMatcherField = FieldUtils.getDeclaredField(oAuth2EndpointFilter.getClass(), authorizationEndpointMatcher, true);
279+
RequestMatcher endpointMatcher = (RequestMatcher) tokenEndpointMatcherField.get(oAuth2EndpointFilter);
280+
String path = null;
281+
if (endpointMatcher instanceof AntPathRequestMatcher)
282+
path = ((AntPathRequestMatcher) endpointMatcher).getPattern();
283+
else if (endpointMatcher instanceof OrRequestMatcher) {
284+
OrRequestMatcher endpointMatchers = (OrRequestMatcher) endpointMatcher;
285+
Field requestMatchersField = FieldUtils.getDeclaredField(OrRequestMatcher.class, "requestMatchers", true);
286+
Iterable<RequestMatcher> requestMatchers = (Iterable<RequestMatcher>) requestMatchersField.get(endpointMatchers);
287+
for (RequestMatcher requestMatcher : requestMatchers) {
288+
if (requestMatcher instanceof OrRequestMatcher) {
289+
OrRequestMatcher orRequestMatcher = (OrRequestMatcher) requestMatcher;
290+
requestMatchersField = FieldUtils.getDeclaredField(OrRequestMatcher.class, "requestMatchers", true);
291+
requestMatchers = (Iterable<RequestMatcher>) requestMatchersField.get(orRequestMatcher);
292+
for (RequestMatcher matcher : requestMatchers) {
293+
if (matcher instanceof AntPathRequestMatcher)
294+
path = ((AntPathRequestMatcher) matcher).getPattern();
295+
}
296+
}
297+
}
298+
}
299+
300+
PathItem pathItem = new PathItem();
301+
if (HttpMethod.POST.equals(requestMethod)) {
302+
pathItem.post(operation);
303+
}
304+
else if (HttpMethod.GET.equals(requestMethod)) {
305+
pathItem.get(operation);
306+
}
307+
openAPI.getPaths().addPathItem(path, pathItem);
308+
}
309+
catch (IllegalAccessException | ClassCastException ignored) {
310+
LOGGER.trace(ignored.getMessage());
311+
}
312+
}
313+
314+
@Override
315+
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
316+
this.applicationContext = applicationContext;
317+
}
318+
}

0 commit comments

Comments
 (0)