JS login form on different domain and handling the CSRF token Having sorted out the CORS issue and succesfully logging into Craft from a different domain using Javascript, I've now turned CSRF protection back on. I've got a module returning the CSRF token name and value fine. I fetch the CSRF token with a GET request, then use that token in my login form submission. But I cannot get past Craft being unable to verify the data submission. The same code works fine when run from the same domain.
Does anyone have any experience of doing this?
function fetchCSRFToken() {

    var url = 'http://server.localhost/actions/server-module/server/get-csrf';

    function status(response) {
      if (response.status &gt;= 200 &amp;&amp; response.status &lt; 300) {
        return Promise.resolve(response)
      } else {
        return Promise.reject(new Error(response.statusText))
      }
    }

    function json(response) {
      return response.json()
    }

    return fetch(url)
        .then(status)
        .then(json)
        .then(function(tokenData) {
            return tokenData;
        }).catch(function(error) {
        });

}

function login(event, tokenData) {

    var rawAction = &quot;http://server.localhost/&quot; + event.target.action.value;
    var action = encodeURIComponent(event.target.action.value);
    var csrfTokenName = encodeURIComponent(tokenData.name);
    var csrfTokenValue = encodeURIComponent(tokenData.value);
    var loginName = encodeURIComponent(event.target.loginName.value);
    var password = encodeURIComponent(event.target.password.value);

    var data = &quot;action=&quot;+action+&quot;&amp;&quot;+csrfTokenName+&quot;=&quot;+csrfTokenValue+&quot;&amp;loginName=&quot;+loginName+&quot;&amp;password=&quot;+password;

    var ajax=new XMLHttpRequest();
    ajax.onreadystatechange = function(){
        if(this.readyState == 4 &amp;&amp; this.status ==200){
            console.log(this.responseText);
        }

    }
          
    ajax.open('POST', rawAction, true);
    ajax.setRequestHeader('Accept', 'application/json, text/javascript, */*; q=0.01');
    ajax.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
    ajax.setRequestHeader(&quot;Content-Type&quot;,&quot;application/x-www-form-urlencoded; charset=UTF-8&quot;);

    ajax.send(data);

}


document.querySelector('#loginform-js').addEventListener('submit', function(event) {

    event.preventDefault();

    fetchCSRFToken().then(function(token) {
        login(event, token);
    });

});

And how I get the CSRF token from a module controller:
    /**
     * @return Response
     */
    public function actionGetCsrf(): Response
    {
        return $this-&gt;asJson([
            'name' =&gt; Craft::$app-&gt;getConfig()-&gt;getGeneral()-&gt;csrfTokenName,
            'value' =&gt; Craft::$app-&gt;getRequest()-&gt;getCsrfToken(),
        ]);
    }

EDIT: I suspect the preflight CORS request is invalidating the CSRF token? The preflight is only necessary on the remote server.
I don't have a good answer for this, but my response is too long to to put into a comment so here goes. I've been experiencing the same thing — I can successfully retrieve the CSRF token, but when I try to log in I get the dreaded 400 Bad Request error.
I found this tidbit on a page about Yii CSRF protection:

When you ebable CSRF validation and use form builder to generate a
form(only post), Yii will auto generate a hidden field and put it in
the form, at the same time, Yii will create a cookie with CSRF token.
When you submit the form, Yii will compare two CSRF tokens from post
and cookie.

I know for a fact that I don't have a cross-domain cookie so there's nothing to compare to. I think the missing cookie is the problem.
In any case, searching for Yii CSRF has given me more to work with than sticking to the Craft documentation. Surely someone has figured this out. In the meantime I'm disabling CSRF from whitelisted domains in my Craft config like this: (for development only):
'enableCsrfProtection' =&gt; (
  $_SERVER['HTTP_ORIGIN']  != 'http://localhost:3000' &amp;&amp; 
  $_SERVER['HTTP_ORIGIN']  != 'https://staging.example.com' &amp;&amp; 
  $_SERVER['HTTP_ORIGIN']  != 'https://example.com'
),

Quick update: maybe this thread can be of help.