Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Cross-Domain Session Cookie (Express API on Heroku + React App on Netlify)

I have a React App making calls to an API in node.js/Express.

Frontend is deployed in Netlify (https), Backend deployed on Heroku (https).

My problem:

  • Everything working in dev environment (localhost)
  • In production (Netlify/Heroku), the api calls to register and login seem to work but the session cookie is not stored in the browser. Because of that, any other calls to protected routes in the API fail (because I don't receive the user credentials).

             cross-domain authentication cookie react express




Talking is cheap, show me the code....

Backend (Express API):

  • I'm using passport (local strategy), express-session, cors.

App.js

require('./configs/passport');

// ...

const app = express();

// trust proxy (https://stackoverflow.com/questions/64958647/express-not-sending-cross-domain-cookies)
app.set("trust proxy", 1); 

app.use(
  session({
    secret: process.env.SESSION_SECRET,
    cookie: {
      sameSite: process.env.NODE_ENV === "production" ? 'none' : 'lax',
      maxAge: 60000000,
      secure: process.env.NODE_ENV === "production",
    },
    resave: true,
    saveUninitialized: false,
    ttl: 60 * 60 * 24 * 30
  })
);

app.use(passport.initialize());
app.use(passport.session());

// ...


app.use(
  cors({
    credentials: true,
    origin: [process.env.FRONTEND_APP_URL]
  })
);

//...

app.use('/api', require('./routes/auth-routes'));
app.use('/api', require('./routes/item-routes'));


CRUD endpoint (ex. item-routes.js):

// Create new item
router.post("/items", (req, res, next) => {
    Item.create({
        title: req.body.title,
        description: req.body.description,
        owner: req.user._id // <-- AT THIS POINT, req.user is UNDEFINED
    })
    .then(
        // ...
    );
});

Frontend (React App):

  • Using Axios with the option "withCredentials" set to true...

User registration and login:

class AuthService {
  constructor() {
    let service = axios.create({
      baseURL: process.env.REACT_APP_API_URL,
      withCredentials: true
    });
    this.service = service;
  }
  
  signup = (username, password) => {
    return this.service.post('/signup', {username, password})
    .then(response => response.data)
  }

  login = (username, password) => {
    return this.service.post('/login', {username, password})
    .then(response => response.data)
  }
   
  //...
}

Creating a new item...:

    axios.post(`${process.env.REACT_APP_API_URL}/items`, {
        title: this.state.title,
        description: this.state.description,
    }, {withCredentials:true})
    .then( (res) => {
        // ...
    });
like image 494
ludovico Avatar asked Mar 06 '21 08:03

ludovico


1 Answers

Short answer:

It wasn't working as expected because I was testing on Chrome Incognito and, by default, Chrome blocks third party cookies in Incognito mode (more details).

Below is a list with some things to check if you're having a similar issue ;)



Checklist

In case it helps, here's a checklist with different things that you main need ;)

  • (Backend) Add "trust proxy" option

If you're deploying on Heroku, add the following line (you can add it before your session settings).

app.set("trust proxy", 1);

  • (Backend) Check your session settings

In particular, check the option sameSite and secure (more details here).

The code below will set sameSite: 'none' and secure: true in production:

app.use(
  session({
    secret: process.env.SESSION_SECRET || 'Super Secret (change it)',
    resave: true,
    saveUninitialized: false,
    cookie: {
      sameSite: process.env.NODE_ENV === "production" ? 'none' : 'lax', // must be 'none' to enable cross-site delivery
      secure: process.env.NODE_ENV === "production", // must be true if sameSite='none'
    }
  })
);
  • (Backend) CORS config
app.use(
  cors({
    credentials: true,
    origin: [process.env.FRONTEND_APP_URL]
  })
);
  • (Backend) Environment Variables

Setup the environment variables in Heroku. For example:

FRONTEND_APP_URL = https://my-project.netlify.app

IMPORTANT: For the CORS URL, avoid a trailing slash at the end. The following may not work:

FRONTEND_APP_URL = https://my-project.netlify.app/ --> avoid this trailing slash!
  • (Frontend) Send credentials

Make sure you're sending credentials in your API calls (you need to do that for all calls you make to the API, including the call for user login).

If you're using axios, you can do use withCredentials option. For example:

    axios.post(`${process.env.REACT_APP_BACKEND_API_URL}/items`, {
        title: this.state.title,
        description: this.state.description,
    }, {withCredentials:true})
    .then( (res) => {
        // ...
    });
  • (Browser) Check the configuration for third-party cookies

For testing, you probably want to make sure you're using the default configuration provided by each browser.

For example, as of 2021, Chrome blocks third-party cookies in Incognito mode (but not in "normal" mode), so you probably want to have something like this:

enter image description here

  • ...and deal with browser restrictions...:

Finally, keep in mind that each browser has a different policy for third party cookies and, in general, those restrictions are expected to increase in the coming years.

For example, Chrome is expected to block third-party cookies at some point in 2023 (source).

If your App needs to bypass those restrictions, here are some options:

  • Implement Backend & Frontend under the same domain

  • Implement Backend & Frontend under subdomains of the same domain (example, example.com & api.example.com)

  • Have your Backend API under a proxy (if you're using Netlify, you can easily setup a proxy using a _redirects file)

like image 68
ludovico Avatar answered Sep 20 '22 12:09

ludovico