feathers
feathers copied to clipboard
Handling of JWT expiration does not allow for proper logout on client nor terminate transport subscriptions.
Dear community I hope this finds you well.
I see there is quite a question mark regarding how to properly handle JWT expiration and login sessions of active client connections. Considering a vast majority of browser applications are not running 24/7, and a "normal" API call is triggered from time to time, a mere error handler on an authentication error should suffice on the client side:
app.hooks({
error: async (context) => {
if (context.error.name === "NotAuthenticated") {
logout()
}
}
})
Now consider this 1% of web applications which subscribe to long running real-time event subscriptions to get some live feed (I am in quite considered with IoT projects, and these solutions tend to run 24/7).
Currently, as soon as an active connection's JWT token expires, a disconnect event will be triggered by the authentication service:
https://github.com/feathersjs/feathers/blob/190d609fb9af6e20a408c283b78b84836fe399bd/packages/authentication/src/jwt.ts#L53
This blocks further "normal" API calls (get, find, remove, etc.) on authentication protected services (which is fine and expected). Yet all channel events do not get propagated to the client connection anymore as well. The client's connection is assumed to be disconnected and removed from all channels in an internal handler:
https://github.com/feathersjs/feathers/blob/2e45efca4a8bfb4cb746c9880e7c6a11749e39dc/packages/transport-commons/src/socket/index.ts#L32
The client on the other hand does not get any indication about this: The websocket/transport is still active, there is no event triggered at all! This prevents us from taking measures like a logout, a forced refresh, or showing a warning.
The main issue here is, that we assume(d) for the authentication module that there is no need for the client connection anymore after expiration of the JWT. But there are many scenarios where real-time events are required before login and after logout or deauthentication. Probably the proper way would be to implement an app.on('deauthenticate') server-handler and app.service('authentication').on('deauthenticate') client-handler instead.
Currently we can work around this by
-
Adding a JWT expiration timer on the client side as well as it has been already proposed: Quite the nasty hack, adds additional long-running timers and JWT parsing dependencies, and requires us to track the JWT token (app.on('login') and app.on('logout') are not available on client side, how can there be any guarantee that we won't miss any refresh?).
-
Adding a heartbeat: Quite silly, as we should depend only on the websocket/transport heartbeat and keep data traffic low.
-
Check whether the websocket/transport is still active in the
app.on('disconnect'), readd the connection to theanonymouschannel and emit a custom event, which is currently my favourite workaround:// Server: channels.ts app.on('disconnect', async (connection: any) => { app.channel('anonymous').join(connection); app.service('meta').emit('deauthenticate', { socket: connection.socketId }); }); app.service('meta').publish('deauthenticate', (data: any, hook: HookContext) => { return app.channel('anonymous').filter(connection => connection.socketId === data.socket) });// Client this.app.service('meta').on('deauthenticate', async () => { logout() } app.hooks({ error: async (context) => { if (context.error.name === "NotAuthenticated") { logout() } } })
As feathersjs is supposed to be a reliable real-time API and Event framework, and authentication is a major corner-stone of every web architecture, I would like to propose to fix this issue once and for all within the boundaries of the framework itself. There are quite a lot of other related issues regarding how to handle deauthentication in general and developers often do not think about them in the first place (why should they, it states that feathersjs handles JWT expiration). They feel themselves in false security.
Could you therefore please consider to add an app.on('deauthenticate') callback, as well as a app.service('authentication').on('deauthenticate') event or something similar?
Best regards Markus