Why won't my React app send HTTP-only cookies in WebSocket upgrade requests in production?
I'm currently building a full-stack TypeScript live chat app with React + Vite on the frontend and Node on the backend. I have two separate servers running: one is a REST API and OAuth2 auth server built with Express and Passport.js and the other one is a WebSockets server built with the ws package. They run independently (no interprocess communication whatsoever) and use stateless auth in the form of JWTs.
Here's how my current flow works: users first log in with either their Google or GitHub account, and once the first server has verified their identity, it sends an HTTP-only cookie down to the client. This cookie is send back to the server on all subsequent requests and I have some middleware that runs on the REST API to parse and verify the JWTs on protected routes. Once it has the cookie, the client then initiates a WS connection with the second server, which also checks for the JWT cookie in the incoming HTTP Upgrade request and verifies its signature before allowing the new client to continue exchanging messages:
import { WebSocket, WebSocketServer } from "ws";
import { baseDataSchema } from "./zod/schemas";
import prisma from "./prisma";
import { asyncJWTverify } from "./misc/jwt";
import { UserJwtReceived } from "../types/jwt";
import { handleJoinGroup } from "./websockets-handlers/join-group";
// Websockets server setup
const wss = new WebSocketServer({ port: Number(process.env.WS_PORT) });
const userSocketMap = new Map<string, WebSocket>();
wss.on("listening", () => {
console.log(`WebSockets server started on port ${process.env.WS_PORT}`);
});
wss.on("connection", async function connection(ws, req) {
// authenticate incoming websocket connection
const cookies = req.headers.cookie;
if (!cookies) return ws.close();
let currentUser: UserJwtReceived = { id: "", iat: 0 };
try {
// Decode auth JWT
const token = cookies.split("=")[1];
currentUser = (await asyncJWTverify(
token,
process.env.JWT_SECRET as string
)) as UserJwtReceived;
} catch (err) {
console.error(err);
return ws.close();
}
// check for JWT expiry
const expiryTime = Number(process.env.JWT_EXPIRY);
if (Math.round(Date.now() / 1000) - currentUser.iat > expiryTime) {
return ws.close();
}
// Bind user ID to WebSocket, add it to map
// TypeScript doesn't complain about this because I've extended ws's WebSocket interface
ws.userId = currentUser.id;
userSocketMap.set(currentUser.id, ws);
console.log(`User ID ${ws.userId} connected`);
ws.on("message", async function message(rawData) => {
// ... actual app logic goes here
})
ws.on("close", function () {
if (!ws.userId) return;
console.log(`User ID ${ws.userId} has disconnected`);
userSocketMap.delete(ws.userId);
});
})
Both servers and the React frontend app run on different URLs, both on local dev and prod, so all requests are cross-origin, but CORS is enabled on the REST API/auth server and as far as I know the WebSockets protocol doesn't implement any CORS policies...
The problem I'm currently facing is that in my local dev environment, the cookie that contains the JWT is sent along with Upgrade request no problem, but after deploying my app to AWS Lightsail (it's a VPS service similar to EC2) and setting up NGINX, my React frontend is no longer able to include the cookie with the upgrade request.
Aft开发者_JS百科er spending literally the whole day debugging, I've been able to rule out a faulty NGINX config as the root of the problem, since I can use wscat
to connect (and most importantly, successfully authenticate) to my production WS server by manually including the Cookie header.
I still have no idea why my React app won't properly send the HTTP-only auth cookie to my WS server. Does anyone have any clue as to why this is happening?
I expected the HTTP-only cookie containing the JWT to be sent along with the HTTP Upgrade request, just like I've been able to do in my local dev environment, but no luck.
精彩评论