BrianT
BrianT

Reputation: 339

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....

Upvotes: 1

Views: 1337

Answers (3)

Bart McLeod
Bart McLeod

Reputation: 161

In my case, the cors configuration was enough in its basic form: allowing the origin site.

Essential seemed to be setting axios.defaults.withCredentials = true; in the frontend react app, before making the request.

For completeness, this is an example from a Laravel app with the following cors config on the backend, where the cookie is created:

<?php

return [

    /*
    |--------------------------------------------------------------------------
    | (FruitCake) Laravel CORS Options
    |--------------------------------------------------------------------------
    |
    | The allowed_methods and allowed_headers options are case-insensitive.
    |
    | You don't need to provide both allowed_origins and allowed_origins_patterns.
    | If one of the strings passed matches, it is considered a valid origin.
    |
    | If ['*'] is provided to allowed_methods, allowed_origins or allowed_headers
    | all methods / origins / headers are allowed.
    |
    */

    /*
     * You can enable CORS for 1 or multiple paths.
     * Example: ['api/*']
     */
    'paths' => ['*'],

    /*
    * Matches the request method. `['*']` allows all methods.
    */
    'allowed_methods' => ['*'],

    /*
     * Matches the request origin. `['*']` allows all origins. Wildcards can be used, eg `*.mydomain.com`
     */
    'allowed_origins' => explode(',', env('CORS_ALLOWED_ORIGINS'))),

    /*
     * Patterns that can be used with `preg_match` to match the origin.
     */
    'allowed_origins_patterns' => [],

    /*
     * Sets the Access-Control-Allow-Headers response header. `['*']` allows all headers.
     */
    'allowed_headers' => ['*'],

    /*
     * Sets the Access-Control-Expose-Headers response header with these headers.
     */
    'exposed_headers' => [],

    /*
     * Sets the Access-Control-Max-Age response header when > 0.
     */
    'max_age' => 0,

    /*
     * Sets the Access-Control-Allow-Credentials header.
     */
    'supports_credentials' => true,
];

Upvotes: 0

BrianT
BrianT

Reputation: 339

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.

Upvotes: 2

Son Nguyen
Son Nguyen

Reputation: 1482

This does not look right:

axios.defaults.headers.common = {
  credentials: "include",
  withCredentials: true
}

There are no such request headers. Instead credentials is controlled via XHR request.

Use this instead to make sure your client accepts cookies:

 axios.defaults.withCredentials = true;

Upvotes: 1

Related Questions