-
Notifications
You must be signed in to change notification settings - Fork 365
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
authentication error in /meta/handshake sends the client into a 0 interval retry loop #542
Comments
Perhaps some kind of event/callback which returns something telling the client how to proceed (abort, retry_interval). |
How are you trying to implement authentication? |
For the purpose of reproducing this, only the authentication failure case matters: bayeux.addExtension({
incoming(message, request, callback) {
if (message.channel === '/meta/handshake')
message.error = 'authentication failed';
callback(message);
}
} |
I need to think about this. The intention with the design of extensions was that authn/authz should be applied to the things you do with the connection, i.e. publish and subscribe, rather than to the connection itself. Most applications won't want to grant blanket access to channels just b/c a client connected successfully. Still, it's possible that if an extension sets It would help if you could explain a more detailed use case where you want to apply authn/authz on handshakes, and then how you restrict publish and subscribe after that. |
It's also better for the client to handle errors if they're applied to publish/subscribe actions, because the client actually calls |
Not authorization but definitely authentication. If you only do a combined authentication/authorization in /meta/subscribe, then a client that fails authentication will remain connected indefinitely, consuming resources (websocket connection and /meta/connect and ping traffic). There is no good reason for this. Such a client is invalid and should not be able to proceed past the initial /meta/handshake request.
If further authorization is needed for subscription it can be done as in your example in the docs. Can it be assumed in /meta/subscribe that the client has passed authentication in /meta/handshake? I am assuming yes, but it just occurred to me that you have to fix/record its identity somehow at the point of /meta/handshake authentication, so it can't alter in a /meta/subscribe. Otherwise you'd need to repeat the authentication there. |
Well, raising an event is not unreasonable. It comes down to this, can you justify anonymous or auth failed clients connecting without restriction, opening websockets and staying connected and ping/ponging indefinitely? I can't, that's unreasonably open. To me its self evident that authentication needs to happen as early as possible (/meta/handshake) and failure should result in disconnection and without immediate automatic retries. And as I realized, the identity used for auth in /meta/handshake needs to be cached somehow (associated with the clientid or connection) so it can be used later in /meta/subscribe for additional authorization. What are your thoughts on the sensibility/feasibility of that? |
After some more thought and testing, here's a more concrete plan:
Do you see any problems with this? There are several things you could improve to make the above easier:
|
I've been pondering this for a few days and trying out some examples and your last message covers many of the reasons I'm reluctant to introduce these changes. In short, it introduces a lot of new coupling and complexity and I think there are simpler and more ergonomic solutions to this problem. It's important to note that the Faye client/server do not only talk to each other. They're implementations of the Bayeux protocol and people use the client with different server implementations and vice versa. Therefore the client can't build in assumptions about Faye-specific behaviour without breaking compatibility. Extensions are designed with the same mentality: they act like a proxy layer that can only see messages going in and out and doesn't make any non-portable assumptions about the implementation. The only exception we make to this is that the This surfaces the first problem: the client can't have special behaviour for authentication failures, because the Bayeux spec does not formalise those in any way. The job of the client is to maintain a consistent connection by sending server.addExtension({
incoming (message, callback) {
if (message.channel === '/meta/handshake') {
if (message.ext?.password !== 'secret') {
message.error = '801::invalid credential'
}
}
callback(message)
},
outgoing (message, callback) {
if (message.error?.startsWith('801::')) {
message.advice = message.advice || {}
message.advice.reconnect = 'none'
}
callback(message)
}
}) That will stop a well behaved client from reconnecting, but clients that are not under your control are still free to open WebSocket connections to your server and send whatever messages they like. Then there's the question of managing state. If you put the credentials for subscribing anywhere other than inside the It is also not advisable in general to use The other element of state management you've identified is that server-side extensions do not pass state from I think making the server pass all extra fields through from incoming to outgoing hooks adds too much complexity, and potential safety problems and deviations from the standard. We opted to only do this for the If you really must pass state through in this way, it is technically possible to use the request object, but I would consider this a hack and strongly advise against it. server.addExtension({
incoming (message, request, callback) {
// auth logic
request.__state__ = { sessionId: '...' }
callback(message)
},
outgoing (message, request, callback) {
message.ext = { sessionId: request.__state__.sessionId }
callback(message)
}
}) Faye tries to hide the network/HTTP layer from the application and the above interface is really only intended for accessing request headers, so I would consider this an abuse of existing functionality. There's no guaranteed one-to-one relationship between HTTP requests and Bayeux client sessions, so this caries some risk of going wrong in surprising ways, especially if You also propose overloading the Then there's the issue of how to handle this failure on the client side. Applications don't call try {
await client.subscribe('/foo', () => {})
} catch (error) {
// error = { code: 801, params: [ '' ], message: 'invalid credential' }
} If the server blocks handshakes, this just silently fails, because clients generally assume that handshake/connect will keep retrying as needed to maintain the connection and will eventually succeed when the client is online. Detecting this failure requires writing a client-side extension to detect the receipt of We could arrange things so that if the client goes into a To sum up, I would recommend performing access control at the subscribe/publish level, as it will be less complex and more reliable. You can prevent clients retrying on certain kinds of failure by setting |
I can't justify persistent anonymous and authn failed connections. They need to be stopped at /meta/handshake. Going to try to follow my plan, with your tips (request, advice), thanks. Wanted to understand client behavior on /meta/handshake failure with Are there other scenarios where a /meta/handshake would be re-attempted at some future point, long after the initial successful subscription? |
I guess when the server disconnects the client when it doesn't hear from it. Then the client would do the full reconnection with the handshake? Haven't tested it yet. What would be the expected client behavior in that case, on /meta/handshake failure with |
The client does stop, albeit after one final attempt to reschedule: } else {
this.info('Handshake unsuccessful');
global.setTimeout(function() { self.handshake(callback, context) }, this._dispatcher.retry * 1000);
this._state = this.UNCONNECTED;
} and a client extension can detect this. |
/meta/handshake is the appropriate time to do authentication (as opposed authorization) as that's the first request the client makes and if it can't authenticate it should not be allowed to remain connected.
The following server and client code is responsible:
server
faye/src/protocol/server.js
Lines 150 to 173 in 60141e8
client
faye/src/protocol/client.js
Lines 353 to 362 in 60141e8
If there's an error in /meta/handshake the server sends:
assign(response.advice, { reconnect: 'handshake' }, false);
note that it doesn't even pass the interval to the client causing 0 interval retries
And the client does
this._cycleConnection();
on response.This doesn't seem like good default behavior. At least optionally, an explicit /meta/handshake error should bubble up, ending retries and letting the user of the client decide what to do about it.
The text was updated successfully, but these errors were encountered: