Repository navigation
Keep the per-user index in sync on reauthentication - #341
Conversation
681c67a to
e814824
Compare
Every successful authentication, including a reauth message, goes through Sockets.prototype.auth(), which pushed the socket onto the per-user index without checking whether it was already there. Each reauthenticate() on an open connection therefore added another copy of the same socket, and a client that reauthenticates periodically (for example on access token refresh) eventually hit maxConnectionsPerUser on its own socket: the reauth was rejected with 503 and the connection was closed. Skip the index update when the socket is already indexed for that user. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
e814824 to
158fa45
Compare
|
There's another subtle bug that this fixes, if a |
|
@MatthewHarrigan @mtharrison can you please file an issue for this with replication steps as to how it's failing for you today? We can look further into things from there. https://github.com/hapijs/.github/blob/master/MAINTENANCE.md Also, thank you for disclosing AI usage. |
Use a minimal reauthentication test that fails on master, and add a test that a broadcast targeted at a user reaches a reauthenticated socket once (master delivers it once per reauthentication). Fixes hapijs#342 Co-Authored-By: Matt Harrison <916064+mtharrison@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Changes in 030e021:
|
|
Thanks, tests look good. Both fail on master and pass here. Since this says
Can you deindex from the old user on a switch, in both places, with tests? Watch the case where the new user is already at the limit. Unfortunately, I can't push to you branch or I'd have done it myself. Here's what my agent generated zz-switch.js Good find guys. @Marsup |
When a reauthentication changes the credentials user, the socket now moves from the previous user to the new one in the Sockets index and in every indexed subscription it holds. Before, it stayed under the previous user: broadcast() and publish() targeted at the previous user still reached it, messages targeted at the new user did not, and on close remove() only looked at the new user, so the previous user's entry was never cleaned up and kept using one of their maxConnectionsPerUser slots. The maxConnectionsPerUser check now runs before the socket credentials are replaced. A reauthentication rejected because the new user is at the limit leaves the socket under its current user until it closes. Subscriptions made before a socket authenticated are now indexed under the user it later authenticates as. Fixes hapijs#342 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Changes in 2bbd809:
New tests (all fail on master and on 030e021):
The three cases from zz-switch.js:
The PR description is updated to match. |
Marsup
left a comment
There was a problem hiding this comment.
I saw a possible race condition where reauth would have put back a dead socket into the dictionary, pushed a commit fixing it. If everyone is fine with it, I'll merge/release soon.
Fixes #342.
When
auth.indexis enabled, every successful authentication goes throughSockets.prototype.auth(), includingreauthmessages (_processReauth()→_authenticate()→_setCredentials()). On master,auth()pushes the socket onto_byUser[user]without checking whether it is already there, and nothing moves the socket when a reauthentication changes the user.Effects on master:
maxConnectionsPerUserset, a client that reauthenticates periodically (for example on access token refresh) eventually hits the limit on its own socket. The reauth is rejected with 503 "Too many connections for the authenticated user" and the server disconnects the client.server.broadcast(message, { user })iterates_byUser[user], so a reauthenticated socket receives the message once per entry.SocketsandSubscriberslists. Messages for the previous user still reach it and messages for the new user don't. On close,remove()only looks at the new user, so the previous user's entry is never removed and keeps using one of theirmaxConnectionsPerUserslots.Changes:
Sockets.prototype.auth()runs before the socket credentials are replaced and receives the previous user. If the user is unchanged it does nothing. Otherwise it checks the new user againstmaxConnectionsPerUser, removes the socket from the previous user's list and adds it to the new user's.remove()then cleans up the current user's entry.Subscribers.prototype.reauth()makes the same move for each indexed subscription route the socket holds. This also indexes subscriptions made before the socket authenticated.Tests (all fail on master):
does not count reauthentication towards the connections limitsends to a reauthenticated user socket oncemoves the socket to the new user when reauthenticating as a different userrejects reauthentication as a different user who is at the connections limitupdates the user index when reauthenticating to and from credentials without a userpublishes to the new user after reauthenticating as a different userpublishes to a subscriber that authenticated after subscribingnpm test: 214 tests, 100% coverage, lint and types clean.🤖 Generated with Claude Code