I\'m trying to make CORS play nicely with Spring Security but it\'s not complying. I made the changes described in this article and changing this line in applicationCo
well This is my code working very well and perfect for me: I spent two days working on it and understanding spring security so I hope you accept it as the answer, lol
public class CorsFilter extends OncePerRequestFilter {
static final String ORIGIN = "Origin";
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
System.out.println(request.getHeader(ORIGIN));
System.out.println(request.getMethod());
if (request.getHeader(ORIGIN).equals("null")) {
String origin = request.getHeader(ORIGIN);
response.setHeader("Access-Control-Allow-Origin", "*");//* or origin as u prefer
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setHeader("Access-Control-Allow-Headers",
request.getHeader("Access-Control-Request-Headers"));
}
if (request.getMethod().equals("OPTIONS")) {
try {
response.getWriter().print("OK");
response.getWriter().flush();
} catch (IOException e) {
e.printStackTrace();
}
}else{
filterChain.doFilter(request, response);
}
}
}
well then you need to also set your filter to be invoked:
<security:http use-expressions="true" .... >
...
//your other configs
<security:custom-filter ref="corsHandler" after="PRE_AUTH_FILTER"/> // this goes to your filter
</security:http>
Well and you need a bean for the custom filter you created:
<bean id="corsHandler" class="mobilebackbone.mesoft.config.CorsFilter" />
Mostly,the OPTIONS request dont carry cookie for the authentication of the spring security.
To resovle that,can modify configuration of spring security to allow OPTIONS request without authentication.
I research a lot and get two solutions:
1.Using Java config with spring security configuration,
@Override
protected void configure(HttpSecurity http) throws Exception
{
http
.csrf().disable()
.authorizeRequests()
.antMatchers(HttpMethod.OPTIONS,"/path/to/allow").permitAll()//allow CORS option calls
.antMatchers("/resources/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.httpBasic();
}
2.Using XML(note. cant not write "POST,GET"):
<http auto-config="true">
<intercept-url pattern="/client/edit" access="isAuthenticated" method="GET" />
<intercept-url pattern="/client/edit" access="hasRole('EDITOR')" method="POST" />
<intercept-url pattern="/client/edit" access="hasRole('EDITOR')" method="GET" />
</http>
On the end,there is the source for the solution... :)
For me, the problem was that the OPTIONS
preflight check failed authentication, because the credentials weren't passed on that call.
This works for me:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.security.SecurityProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.data.web.config.EnableSpringDataWebSupport;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Configuration
@EnableAsync
@EnableScheduling
@EnableSpringDataWebSupport
@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.httpBasic().and()
.authorizeRequests()
.anyRequest().authenticated()
.and().anonymous().disable()
.exceptionHandling().authenticationEntryPoint(new BasicAuthenticationEntryPoint() {
@Override
public void commence(final HttpServletRequest request, final HttpServletResponse response, final AuthenticationException authException) throws IOException, ServletException {
if(HttpMethod.OPTIONS.matches(request.getMethod())){
response.setStatus(HttpServletResponse.SC_OK);
response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, request.getHeader(HttpHeaders.ORIGIN));
response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, request.getHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS));
response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, request.getHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD));
response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
}else{
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage());
}
}
});
}
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(userDetailsService)
.passwordEncoder(new BCryptPasswordEncoder());
}
}
The relevant part being:
.exceptionHandling().authenticationEntryPoint(new BasicAuthenticationEntryPoint() {
@Override
public void commence(final HttpServletRequest request, final HttpServletResponse response, final AuthenticationException authException) throws IOException, ServletException {
if(HttpMethod.OPTIONS.matches(request.getMethod())){
response.setStatus(HttpServletResponse.SC_OK);
response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, request.getHeader(HttpHeaders.ORIGIN));
response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, request.getHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS));
response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, request.getHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD));
response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
}else{
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage());
}
}
});
That fixes the OPTIONS
preflight issue. What happens here is when you receive a call and authentication fails, you check if it's an OPTIONS
call and if it is, just let it pass and let it do everything it wants to do. This essentially disables all browser-side preflight checking, but normal crossdomain policy still applies.
When you're using the latest version of Spring, you can use the code below to allow cross origin requests globally (for all your controllers):
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
@Component
public class WebMvcConfigurer extends WebMvcConfigurerAdapter {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**").allowedOrigins("http://localhost:3000");
}
}
Note that this is rarely a good idea to just hard code it like this. In a few companies I've worked for, the allowed origins were configurable through an admin portal, so on development environments you would be able to add all the origins you need.
In my case, response.getWriter().flush() was't working
Changed the code as below and it started working
public void doFilter(ServletRequest request, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
LOGGER.info("Start API::CORSFilter");
HttpServletRequest oRequest = (HttpServletRequest) request;
HttpServletResponse response = (HttpServletResponse) res;
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Methods", "POST,PUT, GET, OPTIONS, DELETE");
response.setHeader("Access-Control-Max-Age", "3600");
response.setHeader("Access-Control-Allow-Headers",
" Origin, X-Requested-With, Content-Type, Accept,AUTH-TOKEN");
if (oRequest.getMethod().equals("OPTIONS")) {
response.flushBuffer();
} else {
chain.doFilter(request, response);
}
}
Since main part of question is about unathorized CORS POST-request to login point I immediately point you to step 2.
But regarding to answers count this is the most relevant question to Spring Security CORS request. So I will describe more elegant solution for configuring CORS with Spring Security. Because except rare situations it is not necessary to create filters/interceptors/… to put anything in response. We will do that declaratively by Spring. Since Spring Framework 4.2 we have CORS-stuff like filter, processor, etc out-of-the-box. And some links to read 1 2.
Let's go:
It can be done in different ways:
as global Spring MVC CORS config (in configuration classes like WebMvcConfigurerAdapter
)
...
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
...
}
as separate corsConfigurationSource
bean
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.applyPermitDefaultValues();
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
}
as external class (which can be used via constructor or autowired as a component)
// @Component // <- for autowiring
class CorsConfig extends UrlBasedCorsConfigurationSource {
CorsConfig() {
orsConfiguration config = new CorsConfiguration();
config.applyPermitDefaultValues(); // <- frequantly used values
this.registerCorsConfiguration("/**", config);
}
}
We will enable CORS support in Spring Security classes like WebSecurityConfigurerAdapter
. Be sure that corsConfigurationSource
is accessible for this support.
Else provide it via @Resource
autowiring or set explicitly (see in example).
Also we let unauthorized access to some endpoints like login:
...
// @Resource // <- for autowired solution
// CorseConfigurationSource corsConfig;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors();
// or autowiring
// http.cors().configurationSource(corsConfig);
// or direct set
// http.cors().configurationSource(new CorsConfig());
http.authorizeRequests()
.antMatchers("/login").permitAll() // without this line login point will be unaccessible for authorized access
.antMatchers("/*").hasAnyAuthority(Authority.all()); // <- all other security stuff
}
If base config works then we can customize mappings, origins, etc. Even add several configurations for different mappings. For example, I explicitly declare all CORS parameters and let UrlPathHelper to not trim my servlet path:
class RestCorsConfig extends UrlBasedCorsConfigurationSource {
RestCorsConfig() {
this.setCorsConfigurations(Collections.singletonMap("/**", corsConfig()));
this.setAlwaysUseFullPath(true);
}
private static CorsConfiguration corsConfig() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedHeader("*");
config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE"));
config.setAllowCredentials(true);
config.addAllowedOrigin("*");
config.setMaxAge(3600L);
return config;
}
}
To debug my problem I was tracing org.springframework.web.filter.CorsFilter#doFilterInternal
method.
And I saw that CorsConfiguration search returns null
because Spring MVC global CORS configuration was unseen by Spring Security.
So I used solution with direct usage of external class:
http.cors().configurationSource(corsConfig);
I was able to do this by extending UsernamePasswordAuthenticationFilter... my code is in Groovy, hope that's OK:
public class CorsAwareAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
static final String ORIGIN = 'Origin'
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response){
if (request.getHeader(ORIGIN)) {
String origin = request.getHeader(ORIGIN)
response.addHeader('Access-Control-Allow-Origin', origin)
response.addHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE')
response.addHeader('Access-Control-Allow-Credentials', 'true')
response.addHeader('Access-Control-Allow-Headers',
request.getHeader('Access-Control-Request-Headers'))
}
if (request.method == 'OPTIONS') {
response.writer.print('OK')
response.writer.flush()
return
}
return super.attemptAuthentication(request, response)
}
}
The important bits above:
You need to declare this bean in your Spring configuration. There are many articles showing how to do this so I won't copy that here.
In my own implementation I use an origin domain whitelist as I am allowing CORS for internal developer access only. The above is a simplified version of what I am doing so may need tweaking but this should give you the general idea.