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
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.