reactjsexpressaxiosxmlhttprequestsession-cookies

Browser not storing session cookie from React XHR request from express-sessions ** updated config


I am using a React frontend to log into a nodejs server running express-session. Frontend is running on localhost:3000, server is on localhost:5000.

Everything is working properly using postman from localhost (session cookie is sent from server when user is properly authenticated and received/stored by postman. Subsequent postman api request to different path on server uses the session cookie and correctly retrieves the data it should based on the session contents). I can also is login using the browser directly to the server (http://localhost:5000/api/authenticate). The server generates the session, sends the cookie to the browser and it stores the cookie locally.

What doesn't work is when I make the api request from within the React app. The server is returning the session cookie but the browser is not storing it. After researching this for the last few days (there are a lot of questions on this general subject), it seems to be an issue with cross site request but I can't seem to find the right set of app and server settings to get it working properly. The cookie is being sent by the server but the browser won't store it when the request from the app.

*** after some additional troubleshooting and research, I've made some updates. My initial XHR request requires a pre-flight and the request and response headers appear to be correct now but still no cookie being stored in browser. More details below the setup ****

Server Setup

var corsOptions = {
  origin: 'http://localhost:3000',
  credentials: true
};

app.options('*', cors(corsOptions)) // for pre-flight

app.use(cors(corsOptions));

app.use(session({
  genid: (req) => {
    console.log('Inside the session middleware');
    console.log(req.sessionID);
    return uuidv4();
  },
  store: new FileStore(),
  secret: 'abc987',
  resave: false,
  saveUninitialized: true,
  cookie: { httpOnly: false, sameSite: 'Lax', hostOnly: false }
}));


app.use( bodyParser.json() );
app.use(bodyParser.urlencoded({
  extended: true
}));

app.use(function(req, res, next) {
  res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, withCredentials, credentials');
  next();
});


app.post('/api/authenticate', function(req, res) {

  const usernameLower = req.body.username.toLowerCase();
  const passwordHash = md5(req.body.password);

  connection.query('select USERID from USERS where LOWER(USERNAME)=? && PASSWORD=? ', [usernameLower, passwordHash], function (error, results, fields) {
    if (error) {
      console.log(error);
      req.session.destroy();
      res.status(500)
        .json({
          error: 'Internal error please try again'
        });

    } else if (results[0]) {
          const userId = results[0].USERID;

          // setup session data
          mySession = req.session;
          mySession.user = {};
          mySession.user.userId = userId;
 
          res.json(mySession.user);

    } else {
      console.log('auth failed');
      req.session.destroy();
      res.status(401)
        .json({
          error: 'Incorrect email or password'
        });
    }
  });
});

Client setup -- the request is triggered by clicking a submit button in a form

  handleSubmit(event) {
    event.preventDefault();
    axios.defaults.withCreditials = true;
    axios.defaults.credentials = 'include';

    axios({
      credentials: 'include',
      method: 'post',
      url: 'http://localhost:5000/api/authenticate/',
      headers: {'Content-Type': 'application/json' },
      data: {
          username: this.state.username,
          password: this.state.password
        }
      })
      .then((response) => {
        if (response.status === 200) {
          this.props.setLoggedIn(true);
          console.log('userId: '+response.data.userId);
        } else {
          console.log("login error");
        }
     })
      .catch(error => console.log(error))
  }

Below is the response cookie sent to the browser but the browser is not storing it.

{"connect.sid":{"path":"/","samesite":"Lax","value":"s:447935ac-fc08-47c6-9b66-4fa30b355021.Yo5H3XVz3Ux3GjTPVhy8i2ZPJm2RM2RzUnznxU9wBvo"}}

Request headers from XHR request (pre-flight):

OPTIONS /api/authenticate/ HTTP/1.1
Host: localhost:5000
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type
Referer: http://localhost:3000/
Origin: http://localhost:3000
DNT: 1
Connection: keep-alive

Pre-flight server response headers

HTTP/1.1 204 No Content
X-Powered-By: Express
Access-Control-Allow-Origin: http://localhost:3000
Vary: Origin, Access-Control-Request-Headers
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET,HEAD,PUT,PATCH,POST,DELETE
Access-Control-Allow-Headers: content-type
Content-Length: 0
Date: Fri, 10 Jul 2020 21:35:05 GMT
Connection: keep-alive

POST request header

POST /api/authenticate/ HTTP/1.1
Host: localhost:5000
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0
Accept: application/json, text/plain, */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/json
Content-Length: 45
Origin: http://localhost:3000
DNT: 1
Connection: keep-alive
Referer: http://localhost:3000/

Server response headers

HTTP/1.1 200 OK
X-Powered-By: Express
Access-Control-Allow-Origin: http://localhost:3000
Vary: Origin
Access-Control-Allow-Credentials: true
Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept
Content-Type: application/json; charset=utf-8
Content-Length: 95
ETag: W/"5f-Iu5VYnDYPKfn7WPrRi2d2Q168ds"
Set-Cookie: connect.sid=s%3A447935ac-fc08-47c6-9b66-4fa30b355021.Yo5H3XVz3Ux3GjTPVhy8i2ZPJm2RM2RzUnznxU9wBvo; Path=/; SameSite=Lax
Date: Fri, 10 Jul 2020 21:35:05 GMT
Connection: keep-alive

I used the "Will it CORS" tool at https://httptoolkit.tech/will-it-cors/ and my request/response headers all seem to be correct but still no cookie stored.

Pre-flight request contains the correct origin Pre-flight response contains the correct allow-origin and allow-credentials POST request contains the correct origin and allow-credentials POST response contains the correct

Appreciate any help to unravel this....


Solution

  • I solved my issues and wanted to post the solution in case others come across this.

    To recap, the backend server is nodejs using express. The following setup allows the front-end to accept the cookies which were created on the nodejs server.

    app.use(function (req, res, next) {
        res.header("Access-Control-Allow-Origin", "https://frontendserverdomain.com:3000"); // update to match the domain you will make the request from
        res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
        res.header("Access-Control-Allow-Credentials", true); // allows cookie to be sent
        res.header("Access-Control-Allow-Methods", "GET, POST, PUT, HEAD, DELETE"); // you must specify the methods used with credentials. "*" will not work. 
        next();
    });
    

    The front-end app is based on React and uses axios to make http request. It is hosted at "https://frontendserverdomain.com:3000" which is added to the "Access-Control-Allow-Origin" header in the nodejs setup (see above).

    On the front-end, Axios needs the withCredentials setting applied.

    axios.defaults.withCredentials = true;
    
    

    With these settings, your app will be able to exchange cookies with the back-end server.

    One gotcha for me getting CORS working was to make sure the front-end host is properly added to the back-end servers header "Access-Control-Allow-Origin". This includes the port number if it's specified in your URL when accessing the front-end.

    Inn terms of cookie exchange, the "Access-Control-Allow-Credentials" and "Access-Control-Allow-Methods" headers must be set correctly as shown above. Using a wildcard on "Access-Control-Allow-Methods" will not work.