Search code examples
reactjslaravellaravel-7csrf-tokenlaravel-sanctum

Laravel 7 Sanctum: Same domain (*.herokuapp.com) but separate React SPA gets CSRF Token Mismatch


I've read a lot from this forum and watched a lot of tutorial videos on how to connect separate React/Vue SPA to Laravel API with Sanctum Auth but none of the solutions worked for me. This is for my school project.

So here's what I did so far.

I created 2 folders, one for api and one for frontend. I installed Laravel on the api folder and installed React app on the frontend folder. Both of these are Git initialized and have their own Github repositories. Also, both of them are deployed to Heroku.

API

Repository: https://github.com/luchmewep/jarcalc_api

Website: https://jarcalc-api.herokuapp.com

Front-end

Repository: https://github.com/luchmewep/jarcalc_front

Website: https://jarcalculator.herokuapp.com

On local, everything runs fine. I can set error messages to email and password fields on the front-end so that means I have received and sent the laravel_session and XSRF_TOKEN cookies. I have also displayed the authenticated user's information on a dummy dashboard so everything works fine on local.

On the internet, both my apps run but won't communicate with each other. In the official documentation, they must at least be on the same domain and in this case, they are subdomains of the same domain which is .herokuapp.com.

Here are my environment variables for each Heroku apps.

API

SANCTUM_STATEFUL_DOMAINS = jarcalculator.herokuapp.com

(I've tried adding "SESSION_DRIVER=cookie" and "SESSION_DOMAIN=.herokuapp.com" but still not working!)

Update

Found out that axios is not carrying XSRF-TOKEN when trying to POST request for /login. It is automatically carried on local testing.

Here is the relevant code:

api.tsx

import axios from "axios";

export default axios.create({
  baseURL: `${process.env.REACT_APP_API_URL}`,
  withCredentials: true,
});

Login.tsx

...
const handleSubmit = (e: any) => {
    e.preventDefault();
    let login = { email: email.value, password: password.value };
    api.get("/sanctum/csrf-cookie").then((res) => {
      api.post("/login", login).then((res) => {
        /**
         * goes here if login succeeds...
         */
        console.log("Login Success");
        ...
      })
      .catch((e) => {
        console.log("Login failed...")
      });
    })
    .catch((e) => {
      console.log("CSRF failed...");
    });
  };

UPDATE

".herokuapp.com is included in the Mozilla Foundation’s Public Suffix List. This list is used in recent versions of several browsers, such as Firefox, Chrome and Opera, to limit how broadly a cookie may be scoped. In other words, in browsers that support the functionality, applications in the herokuapp.com domain are prevented from setting cookies for *.herokuapp.com." https://devcenter.heroku.com/articles/cookies-and-herokuapp-com

COOKIES ON LOCAL enter image description here

COOKIES ON DEPLOYED enter image description here

Explanation: Although the API and frontend both have .herokuapp.com, that does not make them on the same domain. It is explained on Heroku's article above. This means that all requests between *.herokuapp.com are considered cross-site instead of same-site.

SOLUTION

Since laravel_session cookie is being carried by axios, the only problem left is the xsrf-token cookie. To solve the problem, one must buy a domain name and set the subdomain name for each. In my case, my React frontend is now at www.jarcalculator.me while my Laravel backend is now at api.jarcalculator.me. Since they are now same-site regardless of where they are deployed (React moved to Github pages while Laravel at Heroku), the cookie can be set automatically.


Solution

  • Finally fixed my problem by claiming my free domain name via Github Student Pack. I set my React app's domain name to www.jarcalculator.me while I set my Laravel app's domain name to api.jarcalculator.me. Since they are now subdomains of the same domain which is jarcalculator.me, passing of cookie that contains the CSRF-token and laravel_session token is automatic. No need for modification on axios settings. Just setting the axios' withCredentials to true is all you need to do.