Build a WebSocket channel
Authenticate and authorize connections
Authenticate the handshake and authorize every command instead of trusting a connected socket forever.
An open WebSocket proves the handshake worked. It does not prove the caller may acknowledge incident 42 five minutes later when their role was revoked.
Authentication establishes identity once. Authorization still belongs on every privileged message.
Split public viewers from operators
Viewers read public incidents on SSE. Operators mutate state on WebSockets. The rules differ:
Viewer: may subscribe to public incident fields
Operator: may send command.ack_incident
Revoked operator: connection closed or commands rejected
Never put a long-lived secret in the query string (?token=...). Proxies and logs retain URLs. Prefer a short-lived cookie or a ticket exchanged during the upgrade on a trusted origin.
Authorize each command
function handleAckCommand(ws, msg) {
const operator = ws.context.operator
if (!operator) {
return sendError(ws, 'unauthenticated')
}
if (!operator.roles.includes('oncall')) {
return sendError(ws, 'forbidden')
}
if (!operator.active) {
ws.close(4403, 'session revoked')
return
}
applyAck(msg.payload)
}
Test four cases against the same channel: anonymous viewer, active operator, revoked operator, and a cross-origin page without your cookie. Only the active operator should succeed.
Origin checks still matter
Browsers send an Origin header on the WebSocket handshake. Compare it to your allow list when cookies carry the session. A random site must not open an operator socket from a user’s browser.
Close stale sessions when roles change. A live socket is not a permanent grant.
Cookies on the SSE side
The viewer feed can still require login. Browsers send cookies on same-origin EventSource requests automatically. Treat that stream with the same field filtering rules as the public JSON snapshot.
Session expiry mid-connection happens in real on-call shifts. When the auth middleware rejects the next command, close the socket with a documented code and tell the client to re-login through normal HTTP. Silent failure leaves operators clicking a dead button.
Try this on your own project: perform one privileged action through the socket after revoking the user in your auth store, and confirm the server rejects or closes the connection.
Lesson completed