7

we have are using spring mvc with @RestController annotations on our controllers, and we're handling authorization in the controller. we use the same code to set the allowed methods in responses to CORS pre-flight request. to achieve this, we have:

    <init-param>
        <param-name>dispatchOptionsRequest</param-name>
        <param-value>true</param-value>
    </init-param>

in the configuration of the dispatcher servlet, and then we have:

    @RequestMapping(value="/some/collections", method=RequestMethod.OPTIONS)
    public void collectionOptions(
            HttpServletRequest req,
            HttpServletResponse res) {
        List<RequestMethod> methods = new ArrayList<>();
        // check actual permissions, add the appropriate methods
        CORS.setAllowedMethodHeaders(res,methods);
    }

we also have an interceptor that do basic checks on CORS pre-flight to see if the origin can possibly have any permissions at all.

we do it like this mostly because permissions for some requests actually depend on @RequestParams, i.e.:

    OPTIONS /api/collections?userId=122

might be allowed if you have administrative privileges OR you actually the user with the ID 122. also, we have API keys, so

    OPTIONS /api/collections?userId=122&apiKey=ABC

might be OK for one origin, but not for another one.

this works fine, but spring 4.2 now decides wether or not it handles an OPTIONS request, via a call to:

    CorsUtils.isCorsRequest(request);

in AbstractHandlerMapping and then returns

        HandlerInterceptor[] interceptors = chain.getInterceptors();
        chain = new HandlerExecutionChain(new PreFlightHandler(config), interceptors);

instead of the HandlerMethod ...

what we would need is some way to tell spring to let the controller handle OPTIONS requests, no matter what preflight request handlers are present.

We don't seem to be able to find a point where we can EITHER tell the built-in CORS handling to be quiet or to configure some subclass somewhere that would allow us to bypass the newly added code in:

  AbstractHandlerMapping.getHandler(HSR request)

Is this in any way possible? Wouldn't it be nice for a feature like this to be quiet until I actively enable it (through WebMvcConfigurerAdapter or via those @CrossOrigin annotations)?

-------- EDIT -------------

the HTTP standard says the following about the OPTIONS method:

The OPTIONS method represents a request for information about the communication options available on the request/response chain identified by the Request-URI. This method allows the client to determine the options and/or requirements associated with a resource, or the capabilities of a server, without implying a resource action or initiating a resource retrieval.

thinking beyong just CORS, i think that intercepting a CORS options call although a corresponding method is mapped on the controller is not the right way to go. yes, CORS is one thing you can do with an OPTIONS call. but it is by no means the only one.

if there is nothing mapped and if a handler method is mapped with a different request method and the @CrossOrigin annotation, the assumptions that i want the built in CORS support to be triggered would be fine, but i do not think that any request that has the origin header set should automatically ONLY go to the CORS handlers.

rmalchow
  • 2,689
  • 18
  • 31
  • 2
    It's a bad idea to mix access control and CORS. GET requests don't have a preflight request, e.g., so you need a separate security layer anyway. CORS is about the browser, not the server. – a better oliver Mar 14 '16 at 09:19
  • i am not under the illusion that CORS actually keeps anyone from calling stuff. but some examples i've seen (Access-Control-Allow-Origin: *) aren't exactly picky about stuff. we have way more information that goes into this decision. and we obviously ALSO enforce the authorization on the actual call. what we use the OPTIONS call for is to allow the FE to en/disable certain things based on the actual permissions of the user without duplicating the authorization decision logic. – rmalchow Mar 14 '16 at 10:49
  • Why not just change on the fly `OPTIONS` to `GET`, to allow mvc handler do mapping as usual? – Ken Bekov Mar 14 '16 at 11:40

2 Answers2

4

I just convinced Spring 4.3 to pass CORS preflights to my controller by adding a custom handler mapping:

public class CorsNoopHandlerMapping extends RequestMappingHandlerMapping {

    public CorsNoopHandlerMapping() {
        setOrder(0);  // Make it override the default handler mapping.
    }

    @Override
    protected HandlerExecutionChain getCorsHandlerExecutionChain(HttpServletRequest request,
             HandlerExecutionChain chain, CorsConfiguration config) {
        return chain;  // Return the same chain it uses for everything else.
    }
}

Note: you'll still need to tell Spring to dispatch OPTIONS requests to your controller to begin with - that is, set dispatchOptionsRequest to true in dispatcherServlet like it says in this question.

WHY IT WORKS

Sébastien's above answer suggests using your own CorsProcessor. As far as I can tell, this will still not use your controller as the handler; it will just pass a different CorsProcessor to its own CORS handler.

By default, it looks like the method AbstractHandlerMapping#getCorsHandlerExecutionChain will throw out your controller when it detects a preflight. Instead of using your controller as the handler, it instantiates a new PreFlightHandler and uses that instead. See Spring source code. This is the problematic line:

chain = new HandlerExecutionChain(new PreFlightHandler(config), interceptors);

What it's doing here is rebuilding the execution chain with a PreFlightHandler instead of your controller. This is not what we want, so we can override it to return the input chain.

maybe not
  • 56
  • 5
3

As suggested by zeroflagl, I also think that this is not a good idea to mix access control and CORS. And you should keep in mind that only preflight CORS requests are OPTIONS ones.

If you need to customize Spring CORS handling, you can use AbstractHandlerMapping#setCorsProcessor() to provide your own implementation that could eventually extend DefaultCorsProcessor.

Please notice that by default, CORS processing is enabled but no remote origin is allowed so customizing CorsProcessor is rarely needed. More info on this blog post.

Your access control check could be done as a regular HandlerInterceptor since a successful CORS preflight request will be followed by an actual request.

Sébastien Deleuze
  • 5,950
  • 5
  • 37
  • 38
  • the response to the preflight request is NOT actually about access control. we just use the same basic logic there. we answer with the valid methods in order to allow the FE to determine authorization ahead of time. the question is "can i create/delete/update/read this object in this situation" and the same answer is used both for the ACTUAL call (when the actual controller is invoked) and in the preflight request (so that the FE can determine permissions on specific objects without actually trying). @zeroflagL – rmalchow Mar 14 '16 at 10:44
  • the problem we have is that the OPTIONS request never makes it to the controller (which already has to know the answer to the authorization question anyways). so all we want is an option to do a straight passthrough. – rmalchow Mar 14 '16 at 10:46
  • @rmalchow `GET` requests (=read) are not subject to CORS. The same is true for requests not made by a browser. The idea of CORS is to allow (not deny) a JavaScript application running in a browser to make requests that would be forbidden otherwise. It simply doesn't make sense to be picky, as you put it. I honestly cannot see the value of your approach. If you insist I would follow @Sebestien's answer. It's an authoritative answer after all. +1 – a better oliver Mar 14 '16 at 12:05
  • @zeroflag I added some more context. my point is that CORS is not the only application for an OPTIONS call. all i would want is the possibility to tell the request mapper to just pass it through straight, or possibly some way to have a generic CORS Handler that in turn passes through. but the current implementation does not seem to allow this. it would, for example, help to have the HandlerMethod that is *actually* mapped available in the DefaultCorsHandler so that I could invoke that from a custom subclass. – rmalchow Mar 14 '16 at 14:12
  • @rmalchow _"CORS is not the only application for an OPTIONS call"_ True, but you are referring to preflight requests exclusively. Your clients don't send an `OPTIONS` request deliberately, do they? It's not a good idea mix CORS with anything else, which is what you try to do. – a better oliver Mar 14 '16 at 14:35
  • why wouldn't you send a OPTIONS request deliberately. I also do not quite understand why it isn't a good idea ... after all, what the client can do is what the client can do, no? if a method isn't actually allowed in the current context, why would i include it in my allowed methods, and if the answer is correct, why not use it? as for the non-preflight request: yes, indeed, we do that. we do call OPTIONS explicitly to ask what is allowed. this is (as per the citation from the RFC) what the call is designed to do. – rmalchow Mar 14 '16 at 14:43
  • don't get me wrong ... i am trying to understand why, but so far ... hmm ... so far i don't. i'm sorry :/ – rmalchow Mar 14 '16 at 14:48
  • oh ... it is beginning to dawn on me ... so what you're saying is basically, that the presence of the origin header clearly identifies a pre-flight request, in which case i can give a somehwat broader answer ... but if i chose to explicitly call the OPTIONS method, as a separate call to determine what i am allowed to do, then I could return something a bit more precise, because CorsUtils.isCorsRequest() would return false in that case? hmm. – rmalchow Mar 14 '16 at 14:55
  • @sébastien-deleuze i have a small project on github: https://github.com/rmalchow/spring-boot-options, and it shows that for some reason, ALL possible methods are returned (at least for security.basic.enabled=false) this is, for sure, wrong behaviour. why would this happen? – rmalchow Apr 13 '16 at 00:45
  • This is what is expected from a regular non CORS request. As mentioned by @zeroflagL, you should not perform this request your self but let the browser perform CORS request automatically for cross-origin requests, see https://www.w3.org/TR/cors/ for more details about CORS and https://spring.io/blog/2015/06/08/cors-support-in-spring-framework for how to enable CORS in a Spring Boot application. – Sébastien Deleuze Apr 14 '16 at 07:44
  • @SébastienDeleuze - i am not sure who is giving the answer, but i have my doubts it is tomcat. maybe it is some kind of weird default in spring boot? i'll have to try to build the same as a war and deploy it in jetty and tomcat and see how they behave. apart from that: i do understand that CORS is handled automatically by the browser. however, the original purpose of the OPTIONS method is to let the browser figure out what methods are allowed. think ACLs, then you'd want some way for the client to get realiable information on the specific access rights on this. – rmalchow Apr 21 '16 at 07:46
  • @SébastienDeleuze the thing is, in this example, i am using the OPTIONS call exactly as it is supposed to be used: the client asks the server what it can or can not do. maybe the CORS specs are to be blamed ... but i have my doubts this is actually a useful approach. for our use case, it would be perfectly enough if the spring cors handling would A.) default to empty and B.) check for an explicit handler method mapping (@RequestMapping(method=OPTIONS)) before taking over, and if there IS anything configured, don't handle it. – rmalchow Apr 21 '16 at 07:51
  • A regular OPTIONS request is not detected as a CORS one, so maybe are you refering to the fact that by default OPTIONS request are not dispatched in Spring MVC as described on http://stackoverflow.com/questions/9521690/how-to-handle-http-options-with-spring-mvc ? – Sébastien Deleuze Apr 26 '16 at 22:33