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

2

17

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):

App.js

require('./configs/passport');

// ...

const app = express();

// trust proxy (https://mcmap.net/q/219803/-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) => {
        // ...
    });
Nothingness answered 6/3, 2021 at 8:24 Comment(0)
N
23

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)

Nothingness answered 9/3, 2021 at 19:21 Comment(5)
Hey man, I tried your method for storing httpOnly cookie to maintain a session but browser isn't storing cookies in application but i do get them in response i.e when i login i create a token and save it in request its an httpONly true token with sameSIte none and secure also i have added trust proxy before cors and cookie parser still not workingTheocentric
@MohammadFahad check the last 2 points ("Check the configuration for third-party cookies" & "...and deal with browser restrictions..."). If that doesn't solve your problem best option is to post a different question and provide code so that others can help you ;)Nothingness
you're a legend dude, i spent 2 whole days looking for solutions , the samesite: "none" fixed it for meFalkirk
Have answered in here #71072047Photophobia
This works for me but weirdly only on desktop...Dollarbird
D
0

The issue comes down to third party cookies.

If you're sending data from server.herokuapp.com to site.herokuapp.com you're going to have this issue.

The solution is to use a custom domain for your Heroku applications.

Please see this post for more details: Cookies Only set in Chrome - not set in Safari, Mobile Chrome, or Mobile Safari

Dollarbird answered 2/9, 2022 at 21:15 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.