spring-boot: Broken CORS Support with Spring Security
Using Spring Boot in combination with Spring Security and enabling Cors Support with Annotation @CrossOrigin leads to a broken cors response if the authentication with spring security fails.
Considering java script code for cors request:
url = 'http://localhost:5000/api/token'
xmlhttp = new XMLHttpRequest
xmlhttp.onreadystatechange = ->
if xmlhttp.readyState is 4
console.log xmlhttp.status
xmlhttp.open "GET", url, true
# xmlhttp.setRequestHeader "X-Requested-With", "XMLHttpRequest"
xmlhttp.setRequestHeader 'Authorization', 'Basic ' + btoa 'a:a'
do xmlhttp.send
The output needs to be 200. If the credentials are valid, the request will be 200.
Considering the usecase the credentials are wrong, you would expect output 401 (the standard code for failed authentication). but with spring boot and spring security the output will be 0 with the browser notification:
XMLHttpRequest cannot load http://localhost:5000/api/token. No ‘Access-Control-Allow-Origin’ header is present on the requested resource. Origin ‘http://rudolfschmidt:3000’ is therefore not allowed access. The response had HTTP status code 401.
The OPTION request goes through
Accept:*/*
Accept-Encoding:gzip, deflate, sdch
Accept-Language:de-DE,de;q=0.8,en-US;q=0.6,en;q=0.4
Access-Control-Request-Headers:authorization
Access-Control-Request-Method:GET
Cache-Control:no-cache
Connection:keep-alive
Host:localhost:5000
Origin:http://localhost:3000
Pragma:no-cache
Referer:http://localhost:3000/
and the reserver response
Access-Control-Allow-Credentials:true
Access-Control-Allow-Headers:authorization
Access-Control-Allow-Methods:GET
Access-Control-Allow-Origin:http://localhost:3000
Access-Control-Max-Age:1800
Allow:GET, HEAD, POST, PUT, DELETE, TRACE, OPTIONS, PATCH
Cache-Control:no-cache, no-store, max-age=0, must-revalidate
Content-Length:0
Date:Mon, 02 May 2016 06:52:03 GMT
Expires:0
Pragma:no-cache
Server:Apache-Coyote/1.1
Vary:Origin
X-Content-Type-Options:nosniff
X-Frame-Options:DENY
X-XSS-Protection:1; mode=block
and the result is
Request URL:http://localhost:5000/api/token
Request Method:OPTIONS
Status Code:200 OK
Remote Address:[::1]:5000
everthing is fine.
but now the real request starts:
Accept:*/*
Accept-Encoding:gzip, deflate, sdch
Accept-Language:de-DE,de;q=0.8,en-US;q=0.6,en;q=0.4
Authorization:Basic YTphYQ==
Cache-Control:no-cache
Connection:keep-alive
Host:localhost:5000
Origin:http://localhost:3000
Pragma:no-cache
Referer:http://localhost:3000/
User-Agent:Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.75 Safari/537.36
and the response is
Cache-Control:no-cache, no-store, max-age=0, must-revalidate
Content-Type:application/json;charset=UTF-8
Date:Mon, 02 May 2016 06:52:03 GMT
Expires:0
Pragma:no-cache
Server:Apache-Coyote/1.1
Transfer-Encoding:chunked
WWW-Authenticate:Basic realm="Realm"
X-Content-Type-Options:nosniff
X-Frame-Options:DENY
X-XSS-Protection:1; mode=block
the result is
Request URL:http://localhost:5000/api/token
Request Method:GET
Status Code:401 Unauthorized
Remote Address:[::1]:5000
its also fine, but because the failing header Access-Control-Allow-Origin the ajax request is broken.
I assume that spring security doesnt notice that corsSupport is enabled, because the CrossOrigin Annotiation is at the RestController. Spring Security handles like a gateway before the request has a chance to reach the Annotation that enables the Cors Headers.
The solution is adding a CorsFilter in the Spring Security Chan.
My Spring Code
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
@RestController
@RequestMapping("api")
public class Controller {
@RequestMapping("token")
@CrossOrigin
Map<String, String> token(HttpSession session) {
return Collections.singletonMap("token", session.getId());
}
}
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
UserDetailsService userDetailsService;
@Autowired
PasswordEncoder passwordEncoder;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.requestMatchers(CorsUtils::isCorsRequest).permitAll()
.anyRequest().authenticated()
.and().httpBasic()
.and().addFilterBefore(new WebSecurityCorsFilter(), ChannelProcessingFilter.class);
}
}
public class WebSecurityCorsFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletResponse res = (HttpServletResponse) response;
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, PUT");
res.setHeader("Access-Control-Max-Age", "3600");
res.setHeader("Access-Control-Allow-Headers", "Authorization, Content-Type, Accept, x-requested-with, Cache-Control");
chain.doFilter(request, res);
}
@Override
public void destroy() {
}
}
The solution works but the external filter is ugly and should be work out of the box.
I hope spring boot is the right repository for that issue cause it refers to spring security as well.
About this issue
- Original URL
- State: closed
- Created 8 years ago
- Reactions: 10
- Comments: 27 (14 by maintainers)
Commits related to this issue
- Server: Cors handling Cors headers were not set when there was no security context (401) or authorizatin issue (403). This was a pb because Chrome didn't call fetch.then(xxx), but instead fetch.catc... — committed to gonzalad/iam-malta-2016 by gonzalad 8 years ago
- fixed Broken CORS Support with Spring Security #5834 https://github.com/spring-projects/spring-boot/issues/5834 — committed to chuan-su/klartext by chuan-su 7 years ago
Since Spring Security 4.1, this is the proper way to make Spring Security support CORS (also needed in Spring Boot 1.4/1.5):
and:
Do not do any of below, which are the wrong way to attempt solving the problem:
http.authorizeRequests().antMatchers(HttpMethod.OPTIONS, "/**").permitAll();
web.ignoring().antMatchers(HttpMethod.OPTIONS);
Reference: http://docs.spring.io/spring-security/site/docs/4.2.x/reference/html/cors.html
If you want to define CORS configuration in a single place and get it used by both Spring MVC and Spring Security, the filter based solution is indeed a good one. In that case, don’t use
@CrossOrigin
at@Controller
or@RequestMapping
level and just configure a global filter that will handle both Spring Security and Spring MVC requests. Be aware that you don’t have to create your own filter, we provide one with Spring Framework, based on the same logic than the@CrossOrigin
support.In order to get this filter taken in account, you can use it with Spring Security
addFilterBefore()
method like you did, or register it as a@Bean
. There may be an order issue if your register aCorsFilter
bean so currently you need to register aFilterRegistrationBean
bean to be able to specify the order:That said, for your use case there is maybe an improvement we discussed with @dsyer: supporting
@CrossOrigin
annotation with a global scope if used at@Configuration
level. That could result is creating a filter withorder = 0
like explained previously, but in a much more concise way for the user. This improvement + more guideline in the documentation in order to advise to use global CORS configuration for secured websites would resolve this kind of issue I think. This kind of feature may also be useful for enabling CORS on Spring Data REST. Any thoughts?If there is no way around, I would suggest to remove these Annotations if Spring Technologies dont support it sufficient. In every web app you need a kind of authentication. What is the benefit of those annotations if you cant use them with spring security and you need to develop your own filter system. Even if you dont need spring security you still need your own implementation of an authentication system that will not work with those annotations I guess.
If you would like us to look at this issue, please provide the requested information. If the information is not provided within the next 7 days this issue will be closed.
@rudolfschmidt I wanted to let you know that Spring Security 4.1.1 was released with what should be a fix for the CORS issues you were having. Take a look at the second example which leverages the CORS configuration from Spring MVC http://docs.spring.io/spring-security/site/docs/4.1.x/reference/htmlsingle/#cors
NOTE: You must use Spring Framework 4.3.1 to take advantage of this feature because it is the first release to provide the necessary hooks.
I have updated my Spring + CORS blog post to add more details about the filter based solution: https://spring.io/blog/2015/06/08/cors-support-in-spring-framework#filter-based-cors-support and also answered on Stackoverflow.
I will update it again if we achieve better integration between Spring MVC and Spring Security as suggested by @rstoyanchev.
We are also looking for ways to expose the knowledge that we have with
@CrossOrigin
at theHandlerMapping
level to Spring Security and I’ll provide an update once we know moreIn the meantime however as Sebastien pointed out, we do provide a
CorsFilter
so you can re-use the same infrastructure code that underlies the@CrossOrigin
support. In other words you don’t have to write such a Filter yourself.