0

tl:dr;

A Node (express) server is hosted on Heroku, and the UI is hosted on Netlify. When the UI makes a REST API call to the server, the session doesn't persist (but it persists if I ran both locally. localhost:5000 on the server, localhost:3000 on UI. The UI is proxying requests with package.json).

Code snippets

session.ts

export const sessionConfig = {
  secret: process.env.SESSION_KEY,
  store: new RedisStore({ client: redisClient }),
  resave: true,
  saveUninitialized: true,
  cookie: {
    secure: process.env.NODE_ENV === 'production',
    sameSite: process.env.NODE_ENV === "production" ? 'none' : 'lax',
  },
};

server.ts

const app = express();

app.use(express.json());
app.use(cookieParser());

app.set('trust proxy', 1);
app.use(session(sessionConfig)); // This sessionConfig comes from the file above

app.use(cors({
  credentials: true,
  origin: process.env.CLIENT_URL,
}));

I googled something like express session not persist when cross domain request. Then, I saw threads like this and this. It seems that app.set('trust proxy', 1) will make sure that session data will be persisted for cross-domain requests. Apparently, in my case, something is still missing.

Does anyone see what I'm doing wrong? Any advice will be appreciated!

PS:

I'm using sessions for captcha tests, which looks like...

captch.ts

CaptchaRouter.get('/api/captcha', async (req: Request, res: Response) => {
  const captcha = CaptchaService.createCaptcha();
  req.session.captchaText = captcha.text;
  res.send(captcha.data);
});

CaptchaRouter.post('/api/captcha', async (req: Request, res: Response) => {
    if (req.session.captchaText !== req.body.captchaText) {
      throw new BadRequestError('Wrong code was provided');
    }

    // The client sent the correct captcha
  },
);

Another PS: Here's how the response heders look like:

Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: https://example.netlify.app
Connection: keep-alive
Content-Length: 46
Content-Type: application/json; charset=utf-8
Date: Sun, 09 Jan 2022 00:00:00 GMT
Etag: W/"2e-cds5jiaerjikllkslaxmalmird"
Server: Cowboy
Set-Cookie: connect.sid=s%3ramdon-string-here; Path=/; Expires=Sun, 09 Jan 2022 00:00:00 GMT; HttpOnly; Secure; SameSite=None
Vary: Origin
Via: 1.1 vegur
X-Powered-By: Express
Hiroki
  • 3,893
  • 13
  • 51
  • 90
  • If you check chrome/firefox dev tools. Are the AJAX HTTP requests returning a 200 or are they erroring out? – Matt Davis Jan 09 '22 at 20:02
  • @MattDavis, all the requests return 200. There is no error (except the fact that the session data is missing) – Hiroki Jan 09 '22 at 20:07
  • If you log out `req.session.id` and `req.session.cookie` across multiple requests. I'm assuming that you get different IDs for every request? Sounds one of two scenarios: browser isn't sending the cookie with the request, or the sessions are not being stored persistently. – Matt Davis Jan 09 '22 at 20:16
  • 1
    Also worth noting. If you're using a recent version of `express-session` you don't need to use `app.use(cookieParser())`. `express-session` now reads and writes cookies directly from the req/res objects. Using `cookie-parser` as well can cause issues. Source: https://www.npmjs.com/package/express-session#:~:text=Since%20version%201.5.0%2C%20the%20cookie%2Dparser%20middleware%20no%20longer%20needs%20to%20be%20used – Matt Davis Jan 09 '22 at 20:21

1 Answers1

0

The cause was that the client (hosted on Netlify) wasn't proxying API requests.

The solution was:

  1. add _redirects under public of the client
/api/*  https://server.herokuapp.com/api/:splat  200

/*  /index.html  200

  1. make sure that API requests from the client will begin with the root URL
return axios({ method: 'POST', url: '/api/example', headers: defaultHeaders });

For future reference, here's my session config

const sessionConfig = {
  secret: process.env.SESSION_KEY || 'This fallback string is necessary for Typescript',
  store: new RedisStore({ client: redisClient }),
  resave: false,
  saveUninitialized: true,
  cookie: {
    secure: process.env.NODE_ENV === 'production', // Prod is supposed to use https
    sameSite: process.env.NODE_ENV === "production" ? 'none' : 'lax', // must be 'none' to enable cross-site delivery
    httpOnly: true,
    maxAge: 1000 * 60
  } as { secure: boolean },
};

...and here's server.ts

const app = express();
const port = process.env.PORT || 5000;

app.use(express.json());

app.set('trust proxy', 1);
app.use(session(sessionConfig));

app.use(cors({
  credentials: true,
  origin: process.env.CLIENT_URL,
}));

(As @Matt Davis pointed out, cookieParser was unnecessary)

PS

I haven't tried to set cookie.domain in the session config. If this was set to the client URL (provided by Netlify), did the session cookie persist?

Hiroki
  • 3,893
  • 13
  • 51
  • 90