I have two servers running concurrently on localhost
ports 3300
and 4400
. The first one is hosting my SSR Vite Vue application and the other one is hosting db.json for a mocked Web API using 'json-server'
.
Here is my json-server
:
import bodyParser from 'body-parser';
import cors from 'cors';
import dotenv from 'dotenv';
import express from 'express';
import jsonServer from 'json-server';
import mockLogin from './mocked-auth/login.handler.js';
import mockLogout from './mocked-auth/logout.handler.js';
import me from './mocked-auth/me.handler.js';
import { expressCookieSession } from './mocked-auth/session.middleware.js';
dotenv.config();
const server = jsonServer.create();
const router = jsonServer.router('./json-server/db.json');
const middlewares = jsonServer.defaults();
// Enable All CORS Requests
server.use(cors());
// #region mocked auth API endpoints
const app = express();
app.use(expressCookieSession);
app.use(bodyParser.json());
mockLogin(app, router);// POST /login
mockLogout(app);
me(app);
server.use(app);
// #endregion mocked auth API endpoints
server.use(middlewares);
server.use(router);
const port = process.env.JSON_SERVER_PORT;
server.listen(port, () => {
console.log('\x1b[32m%s\x1b[0m', `JSON Server is running at http://localhost:${port}/`);
});
The important thing here is how I declared my express-session and the /login
& /me
endpoints.
mocked-auth/session.middleware.js
import session from 'express-session';
const sessionConfig = {
secret: 'your_secret_key',
resave: false,
saveUninitialized: true,
cookie: {
// secure: true, // Set 'secure' to true if using HTTPS
httpOnly: true, // Prevent client-side JavaScript from accessing the cookie
sameSite: false,
},
};
// Create the session middleware
export const expressCookieSession = session(sessionConfig);
./mocked-auth/login.handler.js
/**
* Returns an `Express.Application` with a `RequestHandler` for a mocked `POST /login`.
*
* Traditionally, session management in `Express.js` involves storing a session ID in a cookie
* and maintaining session data typically using a session store (e.g. in-memory store, db, or
* external session store like Redis). For this project we are using express-session which does
* the in-memory session management for us.
*/
export default function mockLogin(app, router) {
app.post('/login', (req, res) => {
const { email, password } = req.body;
const users = router.db.get('users');
const user = users.find({ email, password }).value();
console.log(`SELECT * from 'users' WHERE email = '${email}' AND password = '${password}' :\n`, user);
if (user) {
req.session.user = user;
req.session.save();
res.jsonp({ user });
} else {
console.log('USER NOTE FOUND! Returning 401, { error: "Invalid credentials" } ');
// Clear the session cookie on the client side
// "connect.sid" is the default cookie name set by express-session.
res.clearCookie('connect.sid', { httpOnly: true });
res.status(401).jsonp({ error: 'Invalid credentials' });
}
});
return app;
}
./mocked-auth/me.handler.js
export default function me(app) {
app.get('/me', (req, res) => {
console.log('API GET /me req.headers.cookie:\n', req.headers.cookie);
console.log('API GET /me req.session:\n', req.session);
const user = req.session?.user;
if (user) res.jsonp(user);
else res.status(401).jsonp({ error: 'Unauthorized Request.' });
});
return app;
}
I am aware that express-session builtin memory storage instance is only alive on the json-server port, so I cannot simply add a expressCookieSession
middleware on my SSR server app and expect to magically get the express.session
from there. So I have to request a GET /me
from my SSR entrypoint by forwarding the cookie from the browser (which is automatically written by /login and carried over on the succeeding API requests).
Here is the SSR server app:
// Note that this file isn't processed by Vite, see https://github.com/brillout/vite-plugin-ssr/issues/562
import dotenv from 'dotenv';
dotenv.config();
import express from 'express';
import compression from 'compression';
import { root } from './root.js';
import auth from './middlewares/auth.middleware.js';
import ssr from './middlewares/ssr.middleware.js';
const isProduction = process.env.NODE_ENV === 'production';
startServer();
async function startServer() {
const app = express();
// Refer to https://expressjs.com/en/guide/using-middleware.html#middleware.application
app.use(compression());
if (isProduction) {
const sirv = (await import('sirv')).default;
app.use(sirv(`${root}/dist/client`));
} else {
const vite = await import('vite');
const viteDevMiddleware = (
await vite.createServer({
root,
server: { middlewareMode: true },
})
).middlewares;
app.use(viteDevMiddleware);
}
app.get('*', auth, ssr);
const port = process.env.VITE_SSR_PORT || 3200;
app.listen(port);
console.log(`Server running at http://localhost:${port}`);
}
And the auth
middleware:
import { RequestHandler } from 'express';
import { ofetch } from 'ofetch';
const auth: RequestHandler = async (req, res, next) => {
console.log('SSR Entrypoing, AUTH', req.headers.cookie);
const user = await ofetch('http://localhost:4400/me', {
method: 'get',
headers: {
cookie: req.headers.cookie as any,
},
credentials: 'include',
}).catch(err => {
console.log(err, '\njson-server has not found user from req.session');
});
if (!user) return next();
req.user = user;
next();
};
export default auth;
As you can see I just forwarded the req.headers.cookie
on the headers (and also tried adding credentias: 'include'
after reading from somewhere mentioning it). I can guarantee that the req.headers.cookie
contain the default session cookie and that it is passed successfully to the other server's /me
endpoint. Looking at the logs I have something like this for the req.session
(no .user
):
Session {
cookie: {
path: '/',
_expires: null,
originalMaxAge: null,
httpOnly: true,
sameSite: false
}
}
and the req.headers.cookie
actually has the session cookie (example notice the connect.sid
is there):
__stripe_mid=2e077326-a501-4f3f-bd3b-df8e8c9b2924a96499; _ga=GA1.1.1007045372.1684917775; _ga_8F4BEDXWDH=GS1.1.1684917775.1.1.1684918949.60.0.0; connect.sid=s%3AzZwnxnP5dOjNyEjoY4gdfBUN0bHVsxyT.%2FjYg918nKih9R7U4WLoNNwDh9%2B8qhetK0gVcnM6g9o4
I have made sure I hit req.session.save()
after setting req.session.user
on the login endpoint. I don't know what I am missing here. Is there something lacking on the ofetch from the SSR server app side? Or is there something I'm missing in how I should set the req.session
or the express-session storage on the other server? Or am I missing something else?
Note that I have confirmed that doing /login
and /me
to the json-server directly (I was using postman) is working without issues (i.e. req.session.user
is available after successful login). (Postman automatically passes the cookie written on it by the /login response)
UPDATE
I have updated the cors config to:
const corsOptions = {
origin: `http://localhost:${process.env.VITE_SSR_PORT}`,
credentials: true,
};
server.use(cors(corsOptions));
after reading this StackOverflow post, still didn't work for me.