Search code examples
reactjsexpressgoogle-chromecookiesmern

MERN stack with https connection is unable to set cookies on Chrome but sets them on all other browsers


I am developing a typical MERN application and I've completed the authentication cycle. My NodeJS/Express back-end uses 'express-session' and 'connect-mongodb-connection' to create and handle sessions. The React front-end uses 'axios' for communicating with the API. The authentication cycle works on all browsers except Chrome. For all other browsers, a session is successfully created in MongoDB, cookies are set in the browser and I am successfully logged into a session.

But when testing this with Chrome, everything works perfectly except for the part where cookies are set. I've tested this rigorously over the span of a day and I can trace the cookie to the point where it's sent from the back-end. But Chrome refuses to save the cookie.

Here is my code for maintaining sessions:

server/app.js

var store = new MongoDBStore({
  uri: DB,
  collection: 'sessions'

});

// Catch errors
store.on('error', function (error) {
  console.log(error);
});

app.use(require('express-session')({
  secret: process.env.SESSION_SECRET,
  saveUninitialized: false, // don't create session until something stored
  resave: false, //don't save session if unmodified
  store: store,
  cookie: {
    maxAge: parseInt(process.env.SESSION_LIFETIME), // 1 week
    httpOnly: true,
    secure: !(process.env.NODE_ENV === "development"),
    sameSite: false
  },
}));
//Mongo Session Logic End

app.enable('trust proxy');

// 1) GLOBAL MIDDLEWARES
// Implement CORS
app.use(cors({
  origin: [
    process.env.CLIENT_ORIGINS.split(',')
  ],
  credentials: true,
  exposedHeaders: ['set-cookie']
}));

The CLIENT_ORIGINS are set to the https://localhost:3000 and http://localhost:3000 where my React client runs.

Some things I've tried:

  1. Trying all combinations of secure:true & secure:false with all combinations of sameSite:false & sameSite:'strict'
  2. Setting domain to NULL or empty string
  3. Trying to change path randomly

Here's my code for setting the cookies on login at the back-end:

exports.signIn = async (req, res, next) => {
  const { email, password } = req.body;
  if (signedIn(req)) {
    res.status(406).json('Already Signed In');
    return;
  }
  const user = await User.findOne({ email: email });
  if (!user) {
    res.status(400).json('Please enter a correct email.');
    return;
  }
  if (!(await user.matchPassword(password))) {
    res.status(400).json('Please enter a correct password.');
    return;
  }
  req.session.userId = user.id;
  res.status(200).json({ msg: 'Signed In', user: user });
};

This is the generic request model I use for calling my API from React using Axios:

import axios from "axios";
import CONFIG from "../Services/Config";

axios.defaults.withCredentials = true;
const SERVER = CONFIG.SERVER + "/api";

let request = (method, extension, data = null, responseTypeFile = false) => {
  //setting up headers
  let config = {
    headers: {
      "Content-Type": "application/json",
    },
  };
  // let token = localStorage["token"];
  // if (token) {
  //     config.headers["Authorization"] = `Bearer ${token}`;
  // }

  //POST Requests
  if (method === "post") {
    // if (responseTypeFile) {
    //     config['responseType'] = 'blob'
    // }
    // console.log('request received file')
    // console.log(data)
    return axios.post(`${SERVER}/${extension}`, data, config);
  }
  //PUT Requests
  else if (method === "put") {
    return axios.put(`${SERVER}/${extension}`, data, config);
  }
  //GET Requests
  else if (method === "get") {
    if (data != null) {
      return axios.get(`${SERVER}/${extension}/${data}`, config);
    } else {
      return axios.get(`${SERVER}/${extension}`, config);
    }
  }
  //DELETE Requests
  else if (method === "delete") {
    if (data != null) {
      return axios.delete(`${SERVER}/${extension}/${data}`, config);
    } else {
      return axios.delete(`${SERVER}/${extension}`, config);
    }
  }
};

export default request;

Some more things that I have tested:

  1. I have double checked that credentials are set to true on both sides.
  2. I have made sure that the authentication cycle is working on other browsers.
  3. I have also made sure that the authentication cycle works on Chrome when I run React on http instead of https
  4. I have also added my self signed certificate into the trusted root certificates on my local machine. Chrome no longer shows me a warning but still refuses to save cookies
  5. I have made sure that the authentication cycle works if I run an instance of Chrome with web security disabled.
  6. I've tried to make it work by using 127.0.0.1 instead of localhost in the address bar to no avail.
  7. No errors are logged on either side's console.

Any and all help would be appreciated


Solution

  • So I figured out the solution to my issue. My client-side was running on an https connection (even during development), because the nature of my project required so.

    After much research, I was sure that the settings to be used for express-session were these:

    app.use(require('express-session')({
      secret: process.env.SESSION_SECRET,
      saveUninitialized: false, // don't create session until something stored
      resave: false, //don't save session if unmodified
      store: store,
      cookie: {
        maxAge: parseInt(process.env.SESSION_LIFETIME), // 1 week
        httpOnly: true,
        secure: true,
        sameSite: "none"
      },
    }));
    

    Keep in mind that my client-side is running on an https connection even in development. However, despite using these settings, my login cycle did not work on Chrome and my cookies weren't being set.

    Express session refused to send back cookies to the client, because despite having my client run on an https connection, it contacted my server on an http connection (my server was still running on an http connection in development), hence making the connection insecure.

    So I added the following code to my server:

    const https = require('https');
    const fs = require('fs');
    
    var key = fs.readFileSync("./certificates/localhost.key");
    var cert = fs.readFileSync("./certificates/localhost.crt");
    var credentials = {
      key,
      cert
    };
    
    const app = express();
    
    const port = process.env.PORT || 3080;
    
    const server = process.env.NODE_ENV === 'development' ? https.createServer(credentials, app) : app;
    server.listen(port, () => {
      console.log(`App running on port ${port}...`);
    });
    

    I used a self-signed certificate to run my server on an https connection during development. This along with sameSite: "none" and secure: true resolve the issue on Chrome (and all other browsers).