Skip to content

Commit ec591f9

Browse files
author
bnasslahsen
committed
Ãenable CSRF support for swagger-ui. Fixes #776
1 parent b57116f commit ec591f9

File tree

8 files changed

+237
-4
lines changed

8 files changed

+237
-4
lines changed

springdoc-openapi-common/src/main/java/org/springdoc/core/Constants.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,6 @@ public final class Constants {
7070
*/
7171
public static final String SPRINGDOC_ENABLED = "springdoc.api-docs.enabled";
7272

73-
7473
/**
7574
* The constant SPRINGDOC_DEPRECATING_CONVERTER_ENABLED.
7675
*/
@@ -256,6 +255,16 @@ public final class Constants {
256255
*/
257256
public static final String SWAGGER_UI_DEFAULT_URL = "https://petstore.swagger.io/v2/swagger.json";
258257

258+
/**
259+
* The constant CSRF_DEFAULT_COOKIE_NAME.
260+
*/
261+
public static final String CSRF_DEFAULT_COOKIE_NAME= "XSRF-TOKEN";
262+
263+
/**
264+
* The constant CSRF_DEFAULT_HEADER_NAME.
265+
*/
266+
public static final String CSRF_DEFAULT_HEADER_NAME= "X-XSRF-TOKEN";
267+
259268
/**
260269
* Instantiates a new Constants.
261270
*/

springdoc-openapi-common/src/main/java/org/springdoc/core/SwaggerUiConfigProperties.java

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,87 @@ public class SwaggerUiConfigProperties extends AbstractSwaggerUiConfigProperties
5353
*/
5454
private boolean displayQueryParamsWithoutOauth2;
5555

56+
/**
57+
* The Csrf configuration.
58+
*/
59+
private Csrf csrf = new Csrf();
60+
61+
62+
/**
63+
* The type Csrf.
64+
*/
65+
public static class Csrf {
66+
67+
/**
68+
* The Enabled.
69+
*/
70+
private boolean enabled;
71+
72+
/**
73+
* The Cookie name.
74+
*/
75+
private String cookieName = Constants.CSRF_DEFAULT_COOKIE_NAME;
76+
77+
/**
78+
* The Header name.
79+
*/
80+
private String headerName = Constants.CSRF_DEFAULT_HEADER_NAME;
81+
82+
/**
83+
* Is enabled boolean.
84+
*
85+
* @return the boolean
86+
*/
87+
public boolean isEnabled() {
88+
return enabled;
89+
}
90+
91+
/**
92+
* Sets enabled.
93+
*
94+
* @param enabled the enabled
95+
*/
96+
public void setEnabled(boolean enabled) {
97+
this.enabled = enabled;
98+
}
99+
100+
/**
101+
* Gets cookie name.
102+
*
103+
* @return the cookie name
104+
*/
105+
public String getCookieName() {
106+
return cookieName;
107+
}
108+
109+
/**
110+
* Sets cookie name.
111+
*
112+
* @param cookieName the cookie name
113+
*/
114+
public void setCookieName(String cookieName) {
115+
this.cookieName = cookieName;
116+
}
117+
118+
/**
119+
* Gets header name.
120+
*
121+
* @return the header name
122+
*/
123+
public String getHeaderName() {
124+
return headerName;
125+
}
126+
127+
/**
128+
* Sets header name.
129+
*
130+
* @param headerName the header name
131+
*/
132+
public void setHeaderName(String headerName) {
133+
this.headerName = headerName;
134+
}
135+
}
136+
56137
/**
57138
* Is disable swagger default url boolean.
58139
*
@@ -107,4 +188,31 @@ public void setDisplayQueryParamsWithoutOauth2(boolean displayQueryParamsWithout
107188
this.displayQueryParamsWithoutOauth2 = displayQueryParamsWithoutOauth2;
108189
}
109190

191+
/**
192+
* Gets csrf.
193+
*
194+
* @return the csrf
195+
*/
196+
public Csrf getCsrf() {
197+
return csrf;
198+
}
199+
200+
/**
201+
* Sets csrf.
202+
*
203+
* @param csrf the csrf
204+
*/
205+
public void setCsrf(Csrf csrf) {
206+
this.csrf = csrf;
207+
}
208+
209+
/**
210+
* Is csrf enabled boolean.
211+
*
212+
* @return the boolean
213+
*/
214+
public boolean isCsrfEnabled(){
215+
return csrf.isEnabled();
216+
};
217+
110218
}

springdoc-openapi-common/src/main/java/org/springdoc/ui/AbstractSwaggerIndexTransformer.java

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ protected String overwriteSwaggerDefaultUrl(String html) {
119119
*/
120120
protected boolean hasDefaultTransformations() {
121121
boolean oauth2Configured = !CollectionUtils.isEmpty(swaggerUiOAuthProperties.getConfigParameters());
122-
return oauth2Configured || swaggerUiConfig.isDisableSwaggerDefaultUrl();
122+
return oauth2Configured || swaggerUiConfig.isDisableSwaggerDefaultUrl() || swaggerUiConfig.isCsrfEnabled();
123123
}
124124

125125
/**
@@ -137,7 +137,27 @@ protected String defaultTransformations(InputStream inputStream) throws IOExcept
137137
if (swaggerUiConfig.isDisableSwaggerDefaultUrl()) {
138138
html = overwriteSwaggerDefaultUrl(html);
139139
}
140-
140+
if (swaggerUiConfig.isCsrfEnabled()) {
141+
html = addCSRF(html);
142+
}
141143
return html;
142144
}
145+
146+
protected String addCSRF(String html) throws JsonProcessingException {
147+
StringBuilder stringBuilder = new StringBuilder();
148+
stringBuilder.append("requestInterceptor: function() {\n");
149+
stringBuilder.append("const value = `; ${document.cookie}`;\n");
150+
stringBuilder.append("const parts = value.split(`; ");
151+
stringBuilder.append(swaggerUiConfig.getCsrf().getCookieName());
152+
stringBuilder.append("=`);\n");
153+
stringBuilder.append("console.log(parts);\n");
154+
stringBuilder.append("if (parts.length === 2)\n");
155+
stringBuilder.append("this.headers['");
156+
stringBuilder.append(swaggerUiConfig.getCsrf().getHeaderName());
157+
stringBuilder.append("'] = parts.pop().split(';').shift();\n");
158+
stringBuilder.append("return this;\n");
159+
stringBuilder.append("},\n");
160+
stringBuilder.append("presets: [");
161+
return html.replace("presets: [", stringBuilder.toString());
162+
}
143163
}

springdoc-openapi-ui/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ dependencies {
66
api 'org.webjars:webjars-locator-core'
77
compileOnly 'jakarta.servlet:jakarta.servlet-api'
88
testRuntimeOnly 'jakarta.servlet:jakarta.servlet-api'
9+
testImplementation 'org.springframework.boot:spring-boot-starter-security'
910
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package test.org.springdoc.ui.app11;
2+
3+
import org.springframework.http.ResponseEntity;
4+
import org.springframework.web.bind.annotation.GetMapping;
5+
import org.springframework.web.bind.annotation.PostMapping;
6+
import org.springframework.web.bind.annotation.RestController;
7+
8+
@RestController
9+
public class ExampleController {
10+
11+
@PostMapping("/post")
12+
public ResponseEntity<String> postExample() {
13+
return ResponseEntity.ok("Successful post");
14+
}
15+
16+
@GetMapping("/get")
17+
public ResponseEntity<String> getExample() {
18+
return ResponseEntity.ok("Successful get");
19+
}
20+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
*
3+
* * Copyright 2019-2020 the original author or authors.
4+
* *
5+
* * Licensed under the Apache License, Version 2.0 (the "License");
6+
* * you may not use this file except in compliance with the License.
7+
* * You may obtain a copy of the License at
8+
* *
9+
* * https://www.apache.org/licenses/LICENSE-2.0
10+
* *
11+
* * Unless required by applicable law or agreed to in writing, software
12+
* * distributed under the License is distributed on an "AS IS" BASIS,
13+
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* * See the License for the specific language governing permissions and
15+
* * limitations under the License.
16+
*
17+
*/
18+
19+
package test.org.springdoc.ui.app11;
20+
21+
import javax.servlet.http.Cookie;
22+
23+
import org.junit.jupiter.api.Test;
24+
import test.org.springdoc.ui.AbstractSpringDocTest;
25+
26+
import org.springframework.boot.autoconfigure.SpringBootApplication;
27+
import org.springframework.test.context.TestPropertySource;
28+
import org.springframework.test.web.servlet.MvcResult;
29+
30+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
31+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
32+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
33+
34+
@TestPropertySource(properties = {
35+
"springdoc.swagger-ui.csrf.enabled=true",
36+
"springdoc.swagger-ui.csrf.cookie-name=XSRF-TOKEN",
37+
"springdoc.swagger-ui.csrf.header-name=X-XSRF-TOKEN"
38+
})
39+
public class SpringDocCSRFTest extends AbstractSpringDocTest {
40+
41+
@Test
42+
public void testApp() throws Exception {
43+
MvcResult mvcResult = mockMvc.perform(get("/swagger-ui.html"))
44+
.andExpect(status().isFound()).andReturn();
45+
Cookie cookie = mvcResult.getResponse().getCookie("XSRF-TOKEN");
46+
mockMvc.perform(post("/post").header("X-XSRF-TOKEN", cookie.getValue()).cookie(cookie))
47+
.andExpect(status().isOk());
48+
}
49+
50+
@SpringBootApplication
51+
static class SpringDocTestApp {}
52+
53+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package test.org.springdoc.ui.app11;
2+
3+
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
4+
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
5+
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
6+
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
7+
8+
@EnableWebSecurity
9+
public class WebSecurityConf extends WebSecurityConfigurerAdapter {
10+
11+
@Override
12+
protected void configure(HttpSecurity http) throws Exception {
13+
http
14+
.csrf()
15+
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
16+
}
17+
18+
}
Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
1-
spring.main.banner-mode: "off"
1+
spring:
2+
main:
3+
banner-mode: "off"
4+
autoconfigure:
5+
exclude: org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration

0 commit comments

Comments
 (0)