Aller au contenu principal

How to use with express-session

attention

Starting with version 4.6.0, Express middlewares are now officially supported, so the workarounds detailed below can be simplified to:

import session from "express-session";

const sessionMiddleware = session({
secret: "changeit",
resave: false,
saveUninitialized: false
});

io.engine.use(sessionMiddleware);

The next sections are still applicable though:

There are two ways to share the session context between Express and Socket.IO, depending on your use case:

1st use case: Socket.IO only retrieves the session context​

This is useful when the authentication is handled by Express (or Passport) for example.

In that case, we can directly use the session middleware:

import express from "express";
import { createServer } from "http";
import { Server } from "socket.io";
import session from "express-session";

const app = express();
const httpServer = createServer(app);

const sessionMiddleware = session({
secret: "changeit",
resave: false,
saveUninitialized: false
});

app.use(sessionMiddleware);

app.post("/login", (req, res) => {
req.session.authenticated = true;
res.status(204).end();
});

const io = new Server(httpServer);

// convert a connect middleware to a Socket.IO middleware
const wrap = middleware => (socket, next) => middleware(socket.request, {}, next);

io.use(wrap(sessionMiddleware));

// only allow authenticated users
io.use((socket, next) => {
const session = socket.request.session;
if (session && session.authenticated) {
next();
} else {
next(new Error("unauthorized"));
}
});

io.on("connection", (socket) => {
console.log(socket.request.session);
});

Please check the example with Passport here.

2nd use case: Socket.IO can also create the session context​

This is useful if you want to use express-session without an Express application for example.

In that case, we need to customize the headers sent during the handshake:

import { createServer } from "http";
import { Server } from "socket.io";
import session from "express-session";

const httpServer = createServer();

const sessionMiddleware = session({
secret: "changeit",
resave: false,
saveUninitialized: false
});

const io = new Server(httpServer, {
allowRequest: (req, callback) => {
// with HTTP long-polling, we have access to the HTTP response here, but this is not
// the case with WebSocket, so we provide a dummy response object
const fakeRes = {
getHeader() {
return [];
},
setHeader(key, values) {
req.cookieHolder = values[0];
},
writeHead() {},
};
sessionMiddleware(req, fakeRes, () => {
if (req.session) {
// trigger the setHeader() above
fakeRes.writeHead();
// manually save the session (normally triggered by res.end())
req.session.save();
}
callback(null, true);
});
},
});

io.engine.on("initial_headers", (headers, req) => {
if (req.cookieHolder) {
headers["set-cookie"] = req.cookieHolder;
delete req.cookieHolder;
}
});

io.on("connection", (socket) => {
console.log(socket.request.session);
});

Please check the example here.

Modifying the session​

Since it is not bound to an HTTP request, the session must be manually reloaded and saved:

io.on("connection", (socket) => {
const req = socket.request;

socket.on("my event", () => {
req.session.reload((err) => {
if (err) {
return socket.disconnect();
}
req.session.count++;
req.session.save();
});
});
});

You can also use a middleware which will be triggered for each incoming packet:

io.on("connection", (socket) => {
const req = socket.request;

socket.use((__, next) => {
req.session.reload((err) => {
if (err) {
socket.disconnect();
} else {
next();
}
});
});

// and then simply
socket.on("my event", () => {
req.session.count++;
req.session.save();
});
});
attention

Calling req.session.reload() updates the req.session object:

io.on("connection", (socket) => {
const session = socket.request.session;

socket.use((__, next) => {
session.reload(() => {
// WARNING! "session" still points towards the previous session object
});
});
});

Handling logout​

You can use the session ID to make the link between Express and Socket.IO:

io.on("connection", (socket) => {
const sessionId = socket.request.session.id;

socket.join(sessionId);
});

app.post("/logout", (req, res) => {
const sessionId = req.session.id;

req.session.destroy(() => {
// disconnect all Socket.IO connections linked to this session ID
io.in(sessionId).disconnectSockets();
res.status(204).end();
});
});

Handling session expiration​

const SESSION_RELOAD_INTERVAL = 30 * 1000;

io.on("connection", (socket) => {
const timer = setInterval(() => {
socket.request.session.reload((err) => {
if (err) {
// forces the client to reconnect
socket.conn.close();
// you can also use socket.disconnect(), but in that case the client
// will not try to reconnect
}
});
}, SESSION_RELOAD_INTERVAL);

socket.on("disconnect", () => {
clearInterval(timer);
});
});

With TypeScript​

To add proper typings to the session details, you will need to extend the IncomingMessage object from the Node.js "http" module.

Which gives, in the first case:

import { Request, Response, NextFunction } from "express";
import { Session } from "express-session";

declare module "http" {
interface IncomingMessage {
session: Session & {
authenticated: boolean
}
}
}

io.use((socket, next) => {
sessionMiddleware(socket.request as Request, {} as Response, next as NextFunction);
});

And in the second case:

import { Request, Response } from "express";
import { Session } from "express-session";
import { IncomingMessage } from "http";

declare module "http" {
interface IncomingMessage {
cookieHolder?: string,
session: Session & {
count: number
}
}
}

const io = new Server(httpServer, {
allowRequest: (req, callback) => {
// with HTTP long-polling, we have access to the HTTP response here, but this is not
// the case with WebSocket, so we provide a dummy response object
const fakeRes = {
getHeader() {
return [];
},
setHeader(key: string, values: string[]) {
req.cookieHolder = values[0];
},
writeHead() {},
};
sessionMiddleware(req as Request, fakeRes as unknown as Response, () => {
if (req.session) {
// trigger the setHeader() above
fakeRes.writeHead();
// manually save the session (normally triggered by res.end())
req.session.save();
}
callback(null, true);
});
},
});

io.engine.on("initial_headers", (headers: { [key: string]: string }, req: IncomingMessage) => {
if (req.cookieHolder) {
headers["set-cookie"] = req.cookieHolder;
delete req.cookieHolder;
}
});

Reference: TypeScript's Declaration Merging