How to use with express-session
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();
  });
});
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