falconry / falcon

The no-magic web data plane API and microservices framework for Python developers, with a focus on reliability, correctness, and performance at scale.
https://falcon.readthedocs.io/en/stable/
Apache License 2.0
9.51k stars 937 forks source link

CORS error with `cors_enable=True` enabled #1978

Closed gqoew closed 2 years ago

gqoew commented 2 years ago

I have a website running at https://domain.com and an falcon API server running at https://sub.domain.com.

I want my website to be able to query the API server. That's why I have enabled cors_enable=True in my falcon app:

app = falcon.App(middleware=[AuthMiddleware()], cors_enable=True)

Then, the client emits an axios request:

axios({
  method: "post",
  url: "https://sub.domain.com",
  data: {
    data=someData,
  },
  headers: { Authorization: `Bearer ${idToken}` },
})

But I get a CORS error (using Firefox)

OPTIONS https://sub.domain.com CORS Missing Allow Origin

Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at https://sub.domain.com  (Reason: CORS header 'Access-Control-Allow-Origin' missing).

Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at https://sub.domain.com (Reason: CORS request did not succeed)

However, when I use httpie on my laptop, the request works:

http POST https://sub.domain.com  data=someData

What am I doing wrong?

vytas7 commented 2 years ago

Hi @gqoew! I believe we need more data to understand what's going on.

The default CORS middleware that is added as an effect of cors_enable=True is quite simplistic, and it would render a permissive response to an OPTIONS request only if the request has succeeded, i.e. if no error was raised while handling it. That normally works great with the default OPTIONS responder, but could it be that your authentication middleware rejects the request, and the OPTIONS request fails to allow access for the browser?

IIRC the browser does not expose Authorization credentials when sending a preflight request, which might further explain why it fails only from the browser.

Could you check the output (headers in particular) of the following requests in httpie?

http OPTIONS https://sub.domain.com

http POST https://sub.domain.com data=someData 'Authorization:Bearer YOUR_BEARER_TOKEN'
gqoew commented 2 years ago

Hi @vytas7!

I think you are right. It seems the OPTIONS request sent by the browser fails because it has no Authorization header, so the middleware blocks it, and the POST request also fails.

This worked:

http OPTIONS https://sub.domain.com 'Authorization:Bearer YOUR_BEARER_TOKEN'

This didn't:

http OPTIONS https://sub.domain.com

In addition, in the browser I now see the following:

OPTIONS https://sub.domain.com. CORS Preflight Did Not Succeed

Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at https://sub.domain.com (Reason: CORS preflight response did not succeed).

Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at https://sub.domain.com. (Reason: CORS request did not succeed).

I also checked the falcon API logs and I can see the middleware blocking the OPTIONS request.

I am not sure how to force Firefox to send the OPTIONS preflight request with the Authorization header automatically though...? Or would it be possible to desactivate the middleware for OPTIONS preflight requests...?

vytas7 commented 2 years ago

I am not sure how to force Firefox to send the OPTIONS preflight request with the Authorization header automatically though...? Or would it be possible to desactivate the middleware for OPTIONS preflight requests...?

No, I don't think you can force Firefox or any other modern browser to do that; if you include the Authorization header, the browser signals that to the server by setting Access-Control-Request-Headers to authorization.

Currently, you cannot easily deactivate middleware based on specific request parameters.

What you can do, if you have access to the auth middleware's source code, is simply allowing OPTIONS to go unchecked, something along the lines of:

if req.method == 'OPTIONS':
    return

Alternatively, if possible, you can subclass your middleware like

class CORSAwareMiddleware(MyAuthMiddleware):
    def process_request(self, req, resp):
        # NOTE: Do not authenticate OPTIONS requests.
        if req.method != 'OPTIONS':
            super().process_request(req, resp)

If your middleware instead hooks into process_resource, you can use the same treatment.

gqoew commented 2 years ago

I have implemented your first suggestion and it worked 👍

When I have more time I will implement your second suggestion as CORSMiddleware seems to be the right thing to do to limit the requests to sub.domain.com from my domain.com only, as written in the docs.

CORSMiddleware(allow_origins='domain.com', allow_credentials='*')
gqoew commented 2 years ago

By the way, thank you for your time and support solving my issue 🙏

vytas7 commented 2 years ago

You're welcome @gqoew!

Let's keep this issue open at least in terms of documentation, this sounds like a good fit to our FAQ. (I have actually run across a similar issue myself as a user, even before we added CORS support to the framework.)