Socket.io-client: Allow socket.handshake.query to be a function

Created on 22 May 2017  ·  9Comments  ·  Source: socketio/socket.io-client

You want to:

  • [x] report a bug
  • [ ] request a feature

Current behaviour

socket.handshake.query currently allows data to be set on connect, but cannot be updated by reference before reconnect is fired. This causes trouble when authorizing a socket connection with refresh tokens.

Steps to reproduce (if the current behaviour is a bug)

//client
io.connect("wss://socket.server.com", {
   query : {
      token:"something dynamic"
   }
});

If the server is restarted and clients reconnect, the token might be outdated when re-authorizing the socket.

//server
io.use(function(socket, next) {
   var query = socket.handshake.query;
   if(isValidToken(query.token)){
      next();//will never fire if the token has been updated
   }
});

Expected behaviour

Allow query to be a function:

io.connect("wss://socket.server.com", {
   query : function(){
      return {
         token:"something dynamic"
      }
   }
});

Setup

  • socket.io version: 2.0.1

How to fix

In current Socket constructor:

if (opts && opts.query) {
   this.query = opts.query;
}

Can be changed to this:

if(opts && opts.query) {
   this.query = typeof opts.query == 'function' && opts.query.call(this) || opts.query;
}

Most helpful comment

@reeltimedoktor currently you should be able to update the query object on reconnect_attempt event:

socket.on('reconnect_attempt', function () {
  socket.io.opts.query = { token: /* ... */ };
});

All 9 comments

+1
fix for bug #1086 should also solve your problem. Changing query options after successful connection was working with older socket.io-client version, but with current version the reference to query object get lost :(

@reeltimedoktor currently you should be able to update the query object on reconnect_attempt event:

socket.on('reconnect_attempt', function () {
  socket.io.opts.query = { token: /* ... */ };
});

@darrachequesne 's solution doesn't seem to work for me :(

I'm on socket.io-client version 2.3.0 on the client, with a node server with socket.io version 2.3.0.

On the client I'm doing this:

 socket.on('reconnect_attempt', async () => {
      const newToken = await getNewToken();
      socket.io.opts.query = {
        token: latestToken,
      };
    });

And then the server is authenticating the token using a middleware passed to io.use() (which should then fire when the socket is connecting or reconnecting, afaik?).

At the moment the server auth is failing on reconnecting, saying the token has expired - it seems the new token isn't being properly assigned to socket.io.opts.query.token - any ideas why?

Thanks :)

Update: Seems like there are a bunch of different places in the socket object where the auth token is stored, e.g. socket.io.engine.query.token, socket.io.engine.transport.query.token. These are not automatically populated when you set socket.io.opts.query.token. However, changing these still doesn't effect what the servers 'sees' - it still only sees the old token.

The token also seems to be included as a URL parameter in some of the url fields in the socket object. Perhaps these need to be updated too?

After investigating further, it seems that setting the token on socket.io.opts.query.token is sufficient to change the token for reauthentication on reconnecting - something else seems to be the problem for my app, perhaps something to do with firebase auth token generation. So for anyone else who comes across this, @darrachequesne 's solution works!

Ok, think I've fixed it - you need to make sure you are getting the new token synchronously in the 'reconnect_attempt' callback, i.e. don't do this:

 socket.on('reconnect_attempt', async () => {
      const newToken = await getNewToken(); // won't work
      socket.io.opts.query = {
        token: latestToken,
      };
    });

It seems like the server side authentication was being done before the new token had been set. If you get the token synchronously (by getting it some time previously and storing it, and then just retrieving it in the listener callback), the server gets the new auth token in time for reconnection authentication.

with "socket.io-client": "3.0.4" I am getting TS2341: Property 'opts' is private and only accessible within class 'Manager'. error. I am using "typescript": "4.1.3"
image

Ok, think I've fixed it - you need to make sure you are getting the new token _synchronously_ in the 'reconnect_attempt' callback, i.e. don't do this:

 socket.on('reconnect_attempt', async () => {
      const newToken = await getNewToken(); // won't work
      socket.io.opts.query = {
        token: latestToken,
      };
    });

It seems like the server side authentication was being done before the new token had been set. If you get the token synchronously (by getting it some time previously and storing it, and then just retrieving it in the listener callback), the server gets the new auth token in time for reconnection authentication.

We having a similar issue. But my current workaround is to disconnect from the server-side when the auth fail. upon receiving disconnect event on the client, we re-construct the query/header with the async promise call. By the time it reaches reconnecting event, it already have the new value in query/header.

For future readers:

The behavior of the query option in Socket.IO v2 is a bit weird, as it is used in both:

  • the query parameters (for the Manager instance)
  • the Socket.IO handshake (but only for a non-default namespace)

So I'm not sure how we could fix it in a backward-compatible way...

Please note that this is fixed in Socket.IO v3, as you can now use the auth option:

// plain object
const socket = io({
  auth: {
    token: "abc"
  }
});

// or with a function
const socket = io({
  auth: (cb) => {
    cb({
      token: "abc"
    });
  }
});

It should work with an async function too:

const socket = io({
  auth: async (cb) => {
    cb({
      token: "abc"
    });
  }
});

See also:

Was this page helpful?
0 / 5 - 0 ratings