Do not count reauthentication towards maxConnectionsPerUser - #341
MatthewHarrigan wants to merge 1 commit into
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. |
| await server.stop(); | ||
| }); | ||
|
|
||
| it('does not count reauthentication towards the connections limit', async () => { |
There was a problem hiding this comment.
I'd argue this test isn't testing that reauth doesn't hit the limit, rather than that multiple connections do.
A minimal failing test on master could be:
it('does not count reauthentication towards the connections limit', async () => {
const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
server.auth.strategy('default', 'custom');
server.auth.default('default');
await server.register({ plugin: Nes, options: { auth: { index: true, maxConnectionsPerUser: 2 } } });
await server.start();
const client = new Nes.Client(getUri(server.info));
await client.connect({ reconnect: false, auth: { headers: { authorization: 'Custom john' } } });
await client.reauthenticate({ headers: { authorization: 'Custom john' } });
// a reauth is not a new connection
await expect(client.reauthenticate({ headers: { authorization: 'Custom john' } })).to.not.reject();
client.disconnect();
await server.stop();
});
When
auth.indexis enabled, every successful authentication goes throughSockets.prototype.auth(), includingreauthmessages (_processReauth()→_authenticate()→_setCredentials()).auth()pushes the socket onto_byUser[user]without checking whether it is already there, so eachclient.reauthenticate()on an open connection adds another copy of the same socket.With
maxConnectionsPerUserset, a client that reauthenticates periodically (for example whenever its access token is refreshed) 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. The extra entries are only cleared when the socket closes (remove()filters every copy), and until then they also count against genuinely new connections for that user, so opening another tab or device can fail too.Minimal repro:
maxConnectionsPerUser: 2, connect once, then callreauthenticate()twice with the same user. The second call rejects with 503 and the socket is closed.This change skips the index update when the socket is already indexed for that user, and adds a test that reauthenticates repeatedly and then checks the limit still applies to new connections. The new test fails on master with the 503 above.
npm testpasses: 208 tests, 100% coverage, lint and types clean.One related case I left alone to keep this focused: if a reauth switches a socket to a different user, the socket stays in the previous user's list until it closes, because
remove()only looks at the current credentials. Happy to follow up with a separate PR if you'd like that handled.🤖 Generated with Claude Code