2

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 >= 200 && response.status < 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 = "http://server.localhost/" + 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 = "action="+action+"&"+csrfTokenName+"="+csrfTokenValue+"&loginName="+loginName+"&password="+password;

    var ajax=new XMLHttpRequest();
    ajax.onreadystatechange = function(){
        if(this.readyState == 4 && 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("Content-Type","application/x-www-form-urlencoded; charset=UTF-8");

    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->asJson([
            'name' => Craft::$app->getConfig()->getGeneral()->csrfTokenName,
            'value' => Craft::$app->getRequest()->getCsrfToken(),
        ]);
    }

EDIT: I suspect the preflight CORS request is invalidating the CSRF token? The preflight is only necessary on the remote server.

  • Just curious, what's the use case of logging in from a different domain? Wouldn't setting up another Craft site work here so it's actually under the same domain? – RitterKnight Jul 16 '20 at 14:58
  • @RitterKnight The main site handles the commerce side of things, where customers create their account. Then there is a customer portal on a different domain. It's been a real headache managing it through one installation with all the templates. I'm rebuilding the customer portal using Svelte, on a separate server. – Clive Portman Jul 27 '20 at 12:36
  • Would a webhook work for your use case? When a user registers or changes their password, it basically gets picked up by Commerce. Not sure how often people “flip” back and forth between portal or the site but at least a user only has to manage one password. Technically both systems would handle their own auth. – RitterKnight Jul 27 '20 at 16:11
  • Further purchases can be made within the portal, as well as masses of user fields which need updating all the time. I don't want two installations of Craft - is that what you're suggesting? – Clive Portman Jul 27 '20 at 21:46
  • Gotcha, so everything is in Craft then? Are these 2 separate "sites" inside Craft or just one? How are you dealing with different domains exactly? If it's just one, wouldn't users be logging into the same instance of Craft? If you need to have separate layouts for each for example, there's ways to change up the login form instead of dealing with separate domains... not sure if that could work for your use case. – RitterKnight Jul 29 '20 at 15:07
1

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' => (
  $_SERVER['HTTP_ORIGIN']  != 'http://localhost:3000' && 
  $_SERVER['HTTP_ORIGIN']  != 'https://staging.example.com' && 
  $_SERVER['HTTP_ORIGIN']  != 'https://example.com'
),

Quick update: maybe this thread can be of help.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.