CORS preflight check failing for AJAX login form on different domain I've built an AJAX frontend login form, using plain Javascript. It works fine until I try and access it from a different domain.
The problem is CORS and the preflight request. The preflight request doesn't contain the payload from the login form so Craft routes it to a template called 'users/login'. But as the template doesn't exist, the preflight test is returned 'not ok'. CORS looks to be setup okay on the server, because I can get around the problem by creating an empty template for users/login. Then the login form works (preflight test returns ok, so browser proceeds with main request).
What I really need is for the preflight check to not trigger a 404 and return ok, even without the proper payload. Which means Craft not routing that to a template.
Console message without template existing:
Access to XMLHttpRequest at 'http://server.localhost/users/login' from origin 'http://test.localhost' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: It does not have HTTP ok status.

General (under headers tab in Chrome):
Request URL: http://server.localhost/users/login
Request Method: OPTIONS
Status Code: 404 Not Found
Remote Address: [::1]:80
Referrer Policy: origin-when-cross-origin

Response header:
Access-Control-Allow-Credentials: true
Access-Control-Allow-Headers: x-Requested-With, Content-Type, origin, authorization, accept, client-security-token
Access-Control-Allow-Methods: POST, GET, OPTIONS, DELETE, PUT
Access-Control-Allow-Origin: http://test.localhost
Access-Control-Expose-Headers: Content-Security-Policy, Location
Access-Control-Max-Age: 600
Connection: Keep-Alive
Content-Type: text/html; charset=UTF-8
Date: Tue, 14 Jul 2020 05:49:33 GMT
Keep-Alive: timeout=5, max=100
Server: Apache/2.4.41 (Win64) PHP/7.0.33
Transfer-Encoding: chunked
X-Powered-By: Craft CMS

I'm using Wampserver and adding those via .htaccess.
Thanks for posting your headers, that was very helpful. I experienced a very similar problem just the other day and was able to work it out via trial and error and a whole bunch of Google/StackOverflow searches. I'm hoping I can save you the trouble.
So your .htaccess was not set up to respond to OPTIONS requests, which are sent as a preflight before every request. That can be fixed with the following:
&lt;IfModule mod_rewrite.c&gt;
    RewriteEngine On
    RewriteCond %{REQUEST_METHOD} OPTIONS
    RewriteRule ^(.*)$ $1 [R=200,L]
&lt;/IfModule&gt;

This just ensures that the server sends an OK response to a preflight request.
Now you have to make sure that your origin domain is actually allowed to make the request you're attempting. Here's how I solved this:
&lt;ifModule mod_headers.c&gt;
    Header always set Access-Control-Allow-Headers &quot;X-Requested-With, Authorization, Content-Type, Request-Method&quot;
    Header always set Access-Control-Allow-Methods &quot;POST, GET, OPTIONS, DELETE, PUT&quot;
    SetEnvIf Origin &quot;^http(s)?://(.+\.)?(localhost|staging.example.com|example.com)(:[0-9]+)?$&quot; origin_is=$0
    Header always set Access-Control-Allow-Origin %{origin_is}e env=origin_is
&lt;/ifModule&gt;

We're updating the headers to allow certain kinds of requests through which would normally be disabled by default. You can adjust these as necessary, for example you may not need to allow Authorization headers or DELETE requests.
Then we whitelist a specified list of origins that should be allowed to make the request in these magic lines:
SetEnvIf Origin &quot;^http(s)?://(.+\.)?(localhost|staging.example.com|example.com)(:[0-9]+)?$&quot; origin_is=$0
Header always set Access-Control-Allow-Origin %{origin_is}e env=origin_is

Update the list of allowed Origins, including a pipe between each. You may be able to wildcard entire domains. What this is doing is saying &quot;if a request is from staging.example.com, set the appropriate Access-Control-Allow-Origin header.&quot;

Update from the future: Craft 3.5 includes an allowed GraphQL origins config setting which should presumably make this much simpler. You should be able to leave out both of the origin-related headers in my second example.