Skip to content

Do not count reauthentication towards maxConnectionsPerUser - #341

Open
MatthewHarrigan wants to merge 1 commit into
hapijs:masterfrom
MatthewHarrigan:fix-reauth-connection-count
Open

MatthewHarrigan wants to merge 1 commit into
hapijs:masterfrom
MatthewHarrigan:fix-reauth-connection-count

Conversation

@MatthewHarrigan

@MatthewHarrigan MatthewHarrigan commented Sep 30, 2026 •

Copy link
Copy Markdown

When auth.index is enabled, every successful authentication goes through Sockets.prototype.auth(), including reauth messages (_processReauth() → _authenticate() → _setCredentials()). auth() pushes the socket onto _byUser[user] without checking whether it is already there, so each client.reauthenticate() on an open connection adds another copy of the same socket.

With maxConnectionsPerUser set, 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 call reauthenticate() 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 test passes: 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

@MatthewHarrigan
MatthewHarrigan force-pushed the fix-reauth-connection-count branch from 681c67a to e814824 Compare September 30, 2026 19:19
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>
@MatthewHarrigan
MatthewHarrigan force-pushed the fix-reauth-connection-count branch from e814824 to 158fa45 Compare September 30, 2026 20:47
@mtharrison

Copy link
Copy Markdown
Contributor

There's another subtle bug that this fixes, if a broadcast() is sent with a specific user as a target it will iterate over their sockets in _byUser sending to each. For a user that has reauthed that will result in receiving the same message several times. Worth an additional test I think @MatthewHarrigan

@damusix

damusix commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

@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.

Comment thread test/auth.js
await server.stop();
});

it('does not count reauthentication towards the connections limit', async () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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();
});

@mtharrison

Copy link
Copy Markdown
Contributor

Cheers @damusix I opened an issue #342

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants