6

I am using the (ws package) to serve websockets from Node.js. I use the "on upgrade" event to authenticate the connecting clients using a token passed in as a URL parameter. Following the example here, in the event of the token being invalid/missing/expired, I use the following (typescript) code to reject the connection:

server.on('upgrade', async (req: Request, socket: Socket) => {
  // Make sure that we only handle WebSocket upgrade requests
  if (req.headers['upgrade'] !== 'websocket') {
    socket.destroy(new Error(`Expected headers upgrade == 'websocket', received ${req.headers['upgrade']}`));
    return;
  }
  if (_.get(this.config, 'security.enableAuth')) {
    logger.info('[socket] security: validating the session key parameter');
    const u = querystring.parse(req.url.substring(req.url.indexOf('?') + 1));
    if (!u.sessionKey) {
      logger.info('[debug] no session key found');
      socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
      socket.destroy();
      return;
    }
    const token = u.sessionKey as string;
    let result: AuthResult;
    try {
      result = await auth(token);
    } catch (err) {
      logger.warn('[socket] error validating session key:', err);
      socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
      socket.destroy();
      return;
    }
    // and so on...

This all works, except on the client side I do not receive the 401 message, nor anything that can tell me why the connection failed. Chrome network tab shows no messages received, and the close event has code 1006 (CLOSE_ABNORMAL), with no other information. How do I pass a sensible error code or message so that the client knows why the failure occurred?

see sharper
  • 11,505
  • 8
  • 46
  • 65

1 Answers1

5

It seems that it is not possible for the 401 response to be handled on the browser side, as strange as that seems. This makes the "recommended" method employed in the question rather limited in applicability, since there is no way to tell the client why the handshake was rejected. Any data written to the tcp socket is simply thrown away in the browser (though a different client may make use of it I guess). In my case, this limitation is a deal breaker. Instead of rejecting the handshake, I accept it, then immediately close the socket with a suitable code and reason:

this.wss.on('connection', async (ws: ExtendedWebSocket, req: Request) => {
logger.info('[socket] security: validating the session key parameter');
const u = querystring.parse(req.url.substring(req.url.indexOf('?') + 1));
if (!u.sessionKey) {
  logger.info('[debug] no session key found');
  ws.close(WS_CLOSE_CODES.CLOSE_POLICY_VIOLATION, 'Unauthorized'); // code=108
  return;
}
const token = u.sessionKey as string;
const result = await auth(token);
if (result.failed) {
  logger.warn(`[socket] invalid token (${token}) - auth failed`);
  ws.close(WS_CLOSE_CODES.CLOSE_POLICY_VIOLATION, 'Unauthorized');
  return;
}
// and so on...
see sharper
  • 11,505
  • 8
  • 46
  • 65
  • Related and worth reading: https://stackoverflow.com/a/50685387/5153063 It seems this behavior is by design (security concerns). The only problem with accept / close is that you're now advertising an open websocket channel to attackers. – Robert Jan 15 '22 at 21:14