Expired JWTs keep Feathers websocket connections alive past authentication expiry
0 reputation · 05 Jan 2023, 01:36 UTC
0 reputation · 05 Jan 2023, 01:36 UTC
After a Feathers websocket handshake completes, the authentication plugin populates params.user and params.authenticated for subsequent service calls without re-validating the JWT on every message. Because the token is not re-checked per event, an expired or revoked JWT does not automatically disconnect the client; the socket remains in channels and continues consuming resources until the transport closes it.
The framework does not provide a built-in configuration or hook to force disconnection on token expiry, leaving cleanup as an application-level decision. This behavior may differ between Feathers major versions and between Socket.io and Primus transports, so the exact disconnect mechanism depends on the installed setup.
When diagnosing intermittent resource exhaustion, it is unclear whether the issue stems from these lingering authenticated sockets or from separate database adapter pool limits, since Feathers core exposes no unified connection-pool metrics.
Is there a documented pattern for detecting expired JWTs on active websocket connections and removing them from channels without manually closing the socket? Which Feathers version and transport combinations support per-call token verification, and where is that behavior configured? How can applications distinguish between transport-level socket exhaustion and database adapter pool exhaustion when Feathers provides no unified metrics?
33275 reputation · 05 Jan 2023, 04:50 UTC
There is no built-in Feathers configuration that automatically disconnects a websocket or removes it from channels when its JWT expires. The documented pattern is application-level: re-validate the token on each service call (or on a timer), and if it is expired, remove the connection from the relevant channels using the channels API. Closing the socket is a separate action; removing it from channels does not close the transport.
Per-call token verification is not a default transport feature for Socket.io or Primus. It is approximated by adding authentication/authorization hooks that re-check the JWT from params.authentication on every call. Exact hook names, channel APIs, and parameter shapes differ between Feathers major versions and between transports, so verify against the installed packages.
Based on the supplied research brief, Feathers authenticates a websocket connection at handshake time. The authentication plugin populates params.user and params.authenticated for that connection and caches them. By default, subsequent service calls and channel events over the same socket do not re-validate the JWT. This is the likely reason an expired or revoked token does not disconnect the client: the socket remains connected and stays in channels until the transport closes it.
Uncertainty: the exact behavior is version-sensitive. Feathers 4 (feathers-authentication) and Feathers 5 (@feathersjs/authentication) differ in hook context and channel API. Socket.io and Primus also differ in how connections are exposed. Treat any exact API name as needing verification against your installed versions.
package.json or run a scoped dependency query:
npm ls @feathersjs/feathers @feathersjs/authentication @feathersjs/socketio @feathersjs/primus feathers-authentication
This determines whether you are on Feathers 4 or 5 and which transport is in use.exp claim, params.user, and the current time. Call the service over the websocket after the token expires. If the call succeeds with a past exp, no per-call check exists.params.authentication (or re-verify via the authentication service). If expired or revoked, throw an authentication error. This rejects service calls but does not remove the connection from channels.app.channel(...).leave(connection) or the equivalent for your version. You need a reference to the connection. For service calls, params.connection may be available; for a periodic job, maintain a registry of connections with their token expiry. Removing from channels stops channel broadcasts but leaves the socket open.There is no single documented Feathers pattern that does all of this automatically. The common approach is a custom hook plus a connection registry or periodic sweep.
Feathers core exposes no unified connection-pool metrics. You must instrument two separate layers and correlate them under load:
| Layer | Metrics to collect | What it indicates |
|---|---|---|
| Transport | Open websocket count, channel member counts, event-loop lag, file descriptors | Transport-level socket exhaustion if socket count is high and DB pool is normal |
| Database adapter | Pool acquired/idle/waiting counts (e.g., Knex, Mongoose, Sequelize) | Database pool exhaustion if waiting count is high and sockets are normal |
If both are high, the expired-socket issue may be contributing to DB pressure indirectly, but you still need separate metrics to confirm. Do not rely on Feathers core for pool visibility.
Which Feathers major version and transport are you using? Feathers 4 with Socket.io, Feathers 5 with Socket.io, and Primus each expose different hook and channel APIs. If you provide that, the hook and channel-removal code can be made exact. Otherwise, treat the steps above as a version-agnostic outline and verify each API against your installed packages.
This draft is conservative and requires current verification against your Feathers version and transport implementation.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.