Dashiell Rose Bark-Huss
Dashiell Rose Bark-Huss

Reputation: 2965

Fetch with cookie not working even with `credentials: 'include'`

I can't get fetch to send a cookie. I read that for cross origin request, you must use credentials: 'include'. But this still isn't giving me cookies.

Fetch html document

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div>fetch on load</div>
    <script>
      fetch('http://localhost:4001/sayhi', { credentials: 'include' })
        .then((res) => {
          return res.text();
        })
        .then((text) => {
          console.log('fetched: ' + text);
        })
        .catch(console.log);
    </script>
  </body>
</html>

Server file:

const express = require('express');
const mongoose = require('mongoose');

const session = require('express-session');
const MongoStore = require('connect-mongo')(session);

const app = express();
const cors = require('cors');

app.use(cors());
app.use((req, res, next) => {
  console.log(req.path);
 
  // this will log a cookie when several 
  // requests are sent through the browser 
  // but 'undefined' through `fetch`
  console.log('cookie in header: ', req.headers.cookie);
  
  next();
});

app.use(
  session({
    secret: 'very secret 12345',
    resave: true,
    saveUninitialized: false,
    store: new MongoStore({ mongooseConnection: mongoose.connection }),
  })
);

app.use(async (req, res, next) => {
  console.log(`${req.method}: ${req.path}`);
  try {
    req.session.visits = req.session.visits ? req.session.visits + 1 : 1;
    return next();
  } catch (err) {
    return next(err);
  }
});

app.get('/sayhi', (req, res, next) => {
  res.send('hey');
});

(async () =>
  mongoose.connect('mongodb://localhost/oneSession', {
    useNewUrlParser: true,
    useUnifiedTopology: true,
    useFindAndModify: true,
  }))()
  .then(() => {
    console.log(`Connected to MongoDB set user test`);
    app.listen(4001).on('listening', () => {
      console.log('info', `HTTP server listening on port 4001`);
    });
  })
  .catch((err) => {
    console.error(err);
  });

The middleware that runs console.log('cookie in header: ', req.headers.cookie); returns undefined for every fetch request.

Every fetch request is also creating a new session, I believe because the cookie isn't being set.

How can I get cookies to send with fetch?

Update: Partial answer

I think adding these helped so far:

//instead of app.use(cors());
app.use(
  cors({
    credentials: true,
    origin: 'http://localhost:5501', // your_frontend_domain, it's an example
  })
);

app.use((req, res, next) => {
   `res.header('Access-Control-Allow-Credentials', true);   
   res.header('Access-Control-Allow-Origin', 'http://127.0.0.1:5501');` // your_frontend_domain, it's an example
   next()
});

But I still have a problem:

In devtools, I went to 'Network' and refreshed the html file to send the request again. There I saw the response headers. I could see that the Set-Cookie header was sent but had a yellow triangle warning.

enter image description here

It says to set sameSite to true.

I needed to add cookie: { sameSite: 'none' } to the session options

app.use(
  session({
    secret: 'very secret 12345',
    resave: true,
    cookie: { sameSite: 'none' },
    saveUninitialized: false,
    store: new MongoStore({ mongooseConnection: mongoose.connection }),
  })
);

After I did that I got a new yellow triangle warning by the cookie that said I needed to set another option on cookie- secure: true. But this means only requests that send over HTTPS will work. But I'm in development and can't send over https.

Upvotes: 6

Views: 9441

Answers (2)

Dashiell Rose Bark-Huss
Dashiell Rose Bark-Huss

Reputation: 2965

UPDATE

The solution below worked when I was navigating on the browser to http://127.0.0.1:5501/index.html'. But if you navigate to localhost instead of 127.0.0.1 Jack Yu's answer works.


I needed to add options in the `cors` call `{ credentials: true, origin: 'http://localhost:5501' }`. As well as as options on the cookie `{ secure: false, sameSite: 'none'}`. As well as set the `res.header`s.
const express = require('express');

const session = require('express-session');

const app = express();
const cors = require('cors');

app.use(
  cors({
    credentials: true,
    origin: 'http://localhost:5501',
  })
);

app.use(
  session({
    saveUninitialized: false,
    resave: false,
    secret: 'very secret 12345',
    cookie: {
      secure: false,
      sameSite: 'none',
    },
  })
);

app.use(async (req, res, next) => {
  res.header('Access-Control-Allow-Credentials', true);
  res.header(
    'Access-Control-Allow-Headers',
    'Origin, X-Requested-With, Content-Type, Accept, authorization'
  );
  res.header('Access-Control-Allow-Methods', 'GET,POST,DELETE,PUT,OPTIONS');
  res.header('Access-Control-Allow-Origin', 'http://127.0.0.1:5501');

  try {
    req.session.visits = req.session.visits ? req.session.visits + 1 : 1;
    console.log('req.session.visits: ', req.session.visits);
    return next();
  } catch (err) {
    return next(err);
  }
});

app.get('/sayhi', (req, res, next) => {
  res.send('hey');
});

app.listen(4001).on('listening', () => {
  console.log('info', `HTTP server listening on port 4001`);
});

This works on firefox. But it still didn't work on chrome because Chrome now only delivers cookies with cross-site requests if they are set with SameSite=None and Secure. Mine is set to secure: false because I am not sending over HTTPS for development. So I followed these instructions:

You can completely disable this feature by going to "chrome://flags" and disabling "Cookies without SameSite must be secure". However, this will disable it for all sites, so it will be less secure when you aren't developing too.

source

But it concerns me that there isn't a better solution. I don't want to disable "Cookies without SameSite must be secure" for all sites if there's a security reason I shouldn't.

Upvotes: 2

Jack Yu
Jack Yu

Reputation: 2425

I wrote an example for your.
You don't need to setup "none" for sameSite attribute.

{
  saveUninitialized: false,
  resave: false,
  secret: 'very secret 12345',
  cookie: {
    secure: false,
  }
}

Frontend Server

// server.js
const express = require('express');
const app = express();
app.use(express.static("./public"));
app.listen(5001);
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <script>
        fetch('http://localhost:4001/test', {
            method: "POST",
            credentials: "include"
        }).then(response => {
            return response.json()
        }).then(json => {
            alert('Received:  ' + JSON.stringify(json));
        })
    </script>
</body>
</html>

Backend Server

// other-server.js
const express = require('express');
const app = express();
const session = require('express-session')
var sess = {
  saveUninitialized: false,
  resave: false,
  secret: 'very secret 12345',
  cookie: {
    secure: false,
  }
}

const bodyParser = require('body-parser');
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({
    extended: true
}));
app.use(session(sess))
app.use((req, res, next) => {
    res.setHeader("Access-Control-Allow-Headers", "X-Requested-With, Accept, Content-Type")
    res.setHeader("Access-Control-Allow-Origin", "http://localhost:5001")
    res.setHeader("Access-Control-Allow-Credentials", true)
    res.setHeader("Access-Control-Allow-Methods", "GET, HEAD, POST, PUT, DELETE, TRACE, OPTIONS, PATCH")
    next();
})
app.post('/test', (req, res) => {
    console.log(req.sessionID);
    req.session.a = "hi"
    res.json({a: 1})
})

app.listen(4001, () => {
    console.log('start');
});

Upvotes: 1

Related Questions