31

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 applicationContext-security.xml has got POST and GET requests working for my app (temporarily exposes controller methods, so I can test CORS):

  • Before: <intercept-url pattern="/**" access="isAuthenticated()" />
  • After: <intercept-url pattern="/**" access="permitAll" />

Unfortunately the following URL which allows Spring Security logins through AJAX isn't responding: http://localhost:8080/mutopia-server/resources/j_spring_security_check. I am making the AJAX request from http://localhost:80 to http://localhost:8080.

In Chrome

When attempting to access j_spring_security_check I get (pending) in Chrome for the OPTIONS preflight request and AJAX call returns with HTTP status code 0 and message "error".

In Firefox

The preflight succeeds with HTTP status code 302 and I still get the error callback for my AJAX request directly afterwards with HTTP status 0 and message "error".

enter image description here

enter image description here

AJAX Request Code

function get(url, json) {
    var args = {
        type: 'GET',
        url: url,
        // async: false,
        // crossDomain: true,
        xhrFields: {
            withCredentials: false
        },
        success: function(response) {
            console.debug(url, response);
        },
        error: function(xhr) {
            console.error(url, xhr.status, xhr.statusText);
        }
    };
    if (json) {
        args.contentType = 'application/json'
    }
    $.ajax(args);
}

function post(url, json, data, dataEncode) {
    var args = {
        type: 'POST',
        url: url,
        // async: false,
        crossDomain: true,
        xhrFields: {
            withCredentials: false
        },
        beforeSend: function(xhr){
            // This is always added by default
            // Ignoring this prevents preflight - but expects browser to follow 302 location change
            xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
            xhr.setRequestHeader("X-Ajax-call", "true");
        },
        success: function(data, textStatus, xhr) {
            // var location = xhr.getResponseHeader('Location');
            console.error('success', url, xhr.getAllResponseHeaders());
        },
        error: function(xhr) {
            console.error(url, xhr.status, xhr.statusText);
            console.error('fail', url, xhr.getAllResponseHeaders());
        }
    }
    if (json) {
        args.contentType = 'application/json'
    }
    if (typeof data != 'undefined') {
        // Send JSON raw in the body
        args.data = dataEncode ? JSON.stringify(data) : data;
    }
    console.debug('args', args);
    $.ajax(args);
}

var loginJSON = {"j_username": "username", "j_password": "password"};

// Fails
post('http://localhost:8080/mutopia-server/resources/j_spring_security_check', false, loginJSON, false);

// Works
post('http://localhost/mutopia-server/resources/j_spring_security_check', false, loginJSON, false);

// Works
get('http://localhost:8080/mutopia-server/landuses?projectId=6', true);

// Works
post('http://localhost:8080/mutopia-server/params', true, {
    "name": "testing",
    "local": false,
    "generated": false,
    "project": 6
}, true);

Please note - I can POST to any other URL in my app via CORS except the Spring Security login. I've gone through lots of articles, so any insight into this strange issue would be greatly appreciated

Neuron
  • 5,141
  • 5
  • 38
  • 59
Aram Kocharyan
  • 20,165
  • 11
  • 81
  • 96

8 Answers8

24

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:

  • Only add CORS headers to response if CORS request detected
  • Respond to pre-flight OPTIONS request with a simple non-empty 200 response, which also contains the CORS headers.

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.

Keeth
  • 2,100
  • 2
  • 21
  • 29
  • the return of `request.getHeader(ORIGIN)`is an string and not a boolean!? could you explain plz?? – azerafati Aug 21 '14 at 08:41
  • 1
    @Bludream the above code is Groovy, not Java. Does that help? – Keeth Dec 04 '14 at 02:16
  • uh, sure!! it was just my mistake not see groovy here! read comments under my answer! http://stackoverflow.com/questions/18264334/cross-origin-resource-sharing-with-spring-security#comment-39698844 – azerafati Dec 05 '14 at 05:37
  • In the attemptAuthentication() method if HTTP request type is OPTIONS, we are just returning Ok String in response. This method should return Authentication instance per definition. What is the purpose of return statement here ? – user1555669 May 14 '16 at 11:54
17

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" />
Abdennour TOUMI
  • 87,526
  • 38
  • 249
  • 254
azerafati
  • 18,215
  • 7
  • 67
  • 72
  • 2
    Don't you think your if statement should be reversed ? Shouldn't it if it is not equal to the null string ? – Stephane Aug 20 '14 at 22:29
  • @StephaneEybert, uh no it's correct this way, when you sent a Cross Domain request from a browser, this field is set to null, I used it to set my response appropriately. I think this line `String origin = request.getHeader(ORIGIN);` made you think so. it is there only for debugging purposes. – azerafati Aug 21 '14 at 03:01
  • It just seems Keeth had it the other way around. – Stephane Aug 21 '14 at 06:25
  • @StephaneEybert, have you tested anything yet? my android backbone is right now using this code and it's working fine. and keeth's code seems broken since the return of `request.getHeader(ORIGIN)`is an string and not a boolean. – azerafati Aug 21 '14 at 08:41
  • 4
    Yes, my code is running just fine. As for Keeth code, note that it is groovy, which may be doing some silent casting. Also, I understand your code to state the following: if header origin is null then use it. It doesn't make much sense to me. What do you think ? Is it me :-) – Stephane Aug 22 '14 at 09:56
  • uh yeah groovy might be the case, for that piece of the code when browser sends a cross domain request, it sends it a origin header of null which here by checking that I will which request is cross domain and by giving back `*` or `a specific ip` you can tell browser to trust and parse the response instead of an error. – azerafati Aug 23 '14 at 10:16
  • I used this code and the request.getHeader(ORIGIN) is not coming as "null", instead the value i get is "http://localhost:8079". Hence this if condition to compare origin with null is not working for me – Anil Bharadia Feb 15 '16 at 20:07
  • @AnilBharadia, I meant this code to be used on the server and not on the local machine. – azerafati Feb 16 '16 at 10:07
  • 1
    This gives a NullPoinerException in the `.equals("null")`. After tuning it a bit it works! :D Many thanks! I have been for two days hitting my brains with this! – Alfonso Nishikawa Jun 10 '16 at 17:56
  • `request.getHeader(ORIGIN)` return`null` if the header doesn't exist (not a CORS or POST request). Moreover, if the origin of the request is a server it will have its value set to the server name for example `"example.com"` and that may be `"null"` if you client send it as it (for example other server configured to do so) so I think this `if (request.getHeader(ORIGIN).equals("null")) {` could be `if (request.getHeader(ORIGIN).equals("example.com")) {` so people understand it better, or `if (request.getHeader(ORIGIN) != null) {` if you want to add it for all origins. – PhoneixS Mar 05 '18 at 11:06
  • I have omitted the protocol and port, but browsers usually include it in the Origin header like in `Origin: https://developer.mozilla.org` see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Origin for more information – PhoneixS Mar 05 '18 at 11:27
12

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):

@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedMethods("HEAD", "GET", "PUT", "POST", "DELETE", "PATCH");
    }
}

and:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
//        http.csrf().disable();
        http.cors();
    }

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        final CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(ImmutableList.of("*"));
        configuration.setAllowedMethods(ImmutableList.of("HEAD",
                "GET", "POST", "PUT", "DELETE", "PATCH"));
        // setAllowCredentials(true) is important, otherwise:
        // The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'.
        configuration.setAllowCredentials(true);
        // setAllowedHeaders is important! Without it, OPTIONS preflight request
        // will fail with 403 Invalid CORS request
        configuration.setAllowedHeaders(ImmutableList.of("Authorization", "Cache-Control", "Content-Type"));
        final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }
}

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

Hendy Irawan
  • 20,498
  • 11
  • 103
  • 114
  • note please that the question is about forbidden unauthorized access to login endpoint while other authorized requests work normally. Can you point which part of the answer solves the problem? – Дмитрий Кулешов Aug 31 '17 at 18:29
  • When I define corsConfigurationSource bean like you do (and enable cors in configure(HttpSecurity), it overrides addCorsMappings in your example above. So I need only the bottom code to make CORS work for every request (authentication and all other), is that correct? I had originally the first code, but it didn't work for authentication. So I added the second one for authentication, fired a request elsewhere and got 403, because I asked for "origin" header, which is disabled in your second example. So I removed the first example and now CORS works for all endpoints. – Dalibor Filus Mar 07 '19 at 22:01
  • P.S. That makes sense, because HttpSecurity is applied to all controller requests, not just authentication. P.S. maybe you should also add origin to your allowedHeaders. – Dalibor Filus Mar 07 '19 at 22:02
  • Since `WebSecurityConfigurerAdapter` is depricated, you can use: `implements WebMvcConfigurer`. – Deb May 21 '20 at 17:48
5

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

Community
  • 1
  • 1
nolan4954
  • 111
  • 1
  • 9
3

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);
    }
}
Morteza Malvandi
  • 1,656
  • 7
  • 30
  • 73
Ajinath
  • 31
  • 1
3

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.

Anthony De Smet
  • 2,265
  • 3
  • 17
  • 24
3

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:

1. prepare CORS configuration source.

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);
        }
    }
    

2. enable CORS support with the defined configuration

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
    }

3. customize CORS config

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;
    }
}

4. troubleshooting

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);
2

I totally agree with the answer given by Bludream, but I have some remarks:

I would extend the if clause in the CORS filter with a NULL check on the origin header:

public class CorsFilter extends OncePerRequestFilter {

    private static final String ORIGIN = "Origin";


    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
        throws ServletException, IOException {

        if (request.getHeader(ORIGIN) == null || request.getHeader(ORIGIN).equals("null")) {
            response.addHeader("Access-Control-Allow-Origin", "*");
            response.setHeader("Access-Control-Allow-Credentials", "true");
            response.addHeader("Access-Control-Max-Age", "10");

            String reqHead = request.getHeader("Access-Control-Request-Headers");

            if (!StringUtils.isEmpty(reqHead)) {
                response.addHeader("Access-Control-Allow-Headers", reqHead);
            }
        }
        if (request.getMethod().equals("OPTIONS")) {
            try {
                response.getWriter().print("OK");
                response.getWriter().flush();
            } catch (IOException e) {
            e.printStackTrace();
            }
        } else{
            filterChain.doFilter(request, response);
        }
    }
 }

Furthermore, I noticed the following unwanted behavior: If I try to access a REST API with an unauthorized role, Spring security returns me an HTTP status 403: FORBIDDEN and the CORS headers are returned. However, if I use an unknown token, or a token that isn't valid anymore, an HTTP status 401: UNAUTHORIZED is returned WITHOUT CORS headers.

I managed to make it work by changing the filter configuration in the security XML like this:

<security:http use-expressions="true" .... >
    ...
    //your other configs
    <sec:custom-filter ref="corsFilter" before="HEADERS_FILTER"/>
</security:http>

And the following bean for our custom filter:

<bean id="corsFilter" class="<<location of the CORS filter class>>" />
Mathias G.
  • 4,875
  • 3
  • 39
  • 60