31

This might be opinion based, but I still wonder is there a best practice since I'm almost clueless about websocket practices

I have a SPA that gets a JWT token from my own OP. It then uses that JWT to connect to other services I own using both REST and WebSockets.

As far as it goes for REST, it's pretty straightforward:

  • The REST API validates the JWT (sent in Authorization: Bearer ...) and provides access to the protected resource or responds with a 401, letting the SPA know it needs to request a new token.

Now with websockets :

During the load of the SPA, once I got a token, I'm opening a WS to my webservice. First message I send is a login_message with my JWT, which then I keep on server's websocket instance to know who's sending the messages. Each subsequent message I receive, I validate the JWT to see if it's expired.

Once it has expired as far as I understand, I'm facing two options :

  1. Drop the websocket with a token_expired error of some kind and force the browser to establish a new websocket connection once the token get refreshed.

  2. Keep the websocket open, return an error message and send a new login message (once token is refreshed)

  3. Don't use a login message but just send the JWT in each request.

Question : Which method would you recommend and why? In terms of security, and performance. Are there any other common practice I did not listed?

mati.o
  • 1,398
  • 1
  • 13
  • 23
  • 1
    #1 What do you mean by ` OP` ? #2 Classic approach is to exchange user/password for a fresh jwt. So, what do you mean by **First message I send is a login_message with my JWT** – JRichardsz Nov 07 '20 at 05:15
  • `OP` as in OpenID Provider. We were trying at the time to apply authentication with JWT for a WebSocket, without annoying the user to enter his credentials, either by implementing it server side or by dropping the websocket and re-authorizing with a refreshed JWT. I'll share the practice we've came up with in detail in a day or two, having seen such an interest in this question. While with REST everything is pretty easy, a WS keeps the socket open, thus needs to address the case when a token is expired while the socket is still open. – mati.o Nov 07 '20 at 11:01
  • What is the basic flow? #1 user enter to acme.com, enter its credentials, a jwt token is generated to be able to consume any of your apis, if token expire, a new one is generated using websockets? or #2 acme.com does not have login form, user enter to acme.com, using some logic this user obtain a jwt token using websockets, if jwt expire a new one is generated using websockets. .......... If your main goal is to have a web in which your user must be able to consume any of your apis, I can share you a clean and standard approach in which you can use websockets (if apply) in some flow section. – JRichardsz Nov 07 '20 at 15:58
  • As described, the user obtains his JWT by say entering his credentials using standard OAuth2.0 flow. With that JWT he is able to access my REST APIs without problems. One of my services however offers realtime data through a WebSocket, the socket needs to authorize the user too, just like with REST, but while REST is stateless, the WebSocket is kept open and may reach a point in time where the short-lived JWT expires. The question is regarding this specific case of expiry - how to handle better, by closing the socket or by keeping it open waiting for the client to renew using application code – mati.o Nov 07 '20 at 16:16
  • Is your user able to re-login in order to renew the token? – JRichardsz Nov 07 '20 at 21:04

2 Answers2

11

Quite an old question I've asked, so I'd be happy to share our chosen practice:

  1. Once the client gets his JWT for the first time (when the application starts), a WebSocket is opened.

  2. To authenticate the channel, we send a message that we define as part of our protocol, called authMessage which contains that JWT.

  3. The server stores this data on the socket's instance and verifies it's validity/expiry before sending data down the wire or receiving from the client.

  4. The token gets refreshed silently in web application minutes before it is expired and another authMessage is issued to the server (repeat from step 2).

  5. If for whatever reason it gets expired before getting renewed, the server closes that socket.

This is roughly what we have implemented in our application (without optimization) and worked really well for us.

mati.o
  • 1,398
  • 1
  • 13
  • 23
  • If i am understanding correctly, once the webapp refreshes the token, you send the new info to the server. Then the server closes the old connection and opens a new socket connection with the new token info? – chris Nov 20 '20 at 12:48
  • 1
    Actually no. It just sends the new `authMessage` with the updated JWT and the server side updates it on the sockets' instance. Dropping a socket is a costly operation, therefor we'd like to keep it to minimum - only for refresh or expiration – mati.o Nov 20 '20 at 16:23
  • 1
    But this would allow clients to establish connections without being authenticated, correct? Would you advise doing the first connect/reconnects with the token as a path param to ensure connections aren't opened for unauthenticated clients, or is it not worth it? – Marian Klühspies Dec 11 '21 at 06:12
  • @MarianKlühspies good point. You can definitely use this technique for the initial WS connection. This is in fact trivial to implement rather than dealing with lingering sockets that won’t send an `authMessage`. Be sure do always do this securely. There is a nice post about this approach: https://www.linode.com/docs/guides/authenticating-over-websockets-with-jwt/ – mati.o Dec 11 '21 at 12:25
2

Oauth2 flow has two options to renew the token. As you said on of these options is prompt a message to the use to enforce a new login process.

The other option is to use the refresh_token in which you will avoid this login process to your user, every time the session expires and renew the token in a silent way.

In both case, you need to store the jwt in the client (commonly after login) and update it (after interactive login or silent regeneration). Localstorage, store or just a simple global variable are alternatives to handle the store and update the jwt in he client.

As we can see, jwt regeneration is solved following oauth2 spec and is performed at client side, SPA in your case.

So the next question is: How can I pass this jwt (new or renewed) to the external resources (classic rest api or your websocket)?

Classic Rest Api

In this case as probably you know, send the jwt is easy using http headers. In each http invocation we can send the old/valid jwt or the renewed jwt as header, commonly Authorization: Bearer ...

Websocket

In this case it's not so easy because according to a quickly review, there are not a clear way to update headers or any other "metadata" once the connection was established:

What's more, there is no concept of headers, so you need to send this information (jwt in your case) to your websocket using:

  • protocols
var ws = new WebSocket("ws://example.com/path", ["protocol1", "protocol2"]);
  • cookies
document.cookie = 'MyJwt=' + jwt + ';'
var ws = new WebSocket(
    'wss://localhost:9000/wss/'
);
  • simple get parameters
var ws = new WebSocket("ws://example.com/service?key1=value1&key2=value2");

Websocket only receive text

And according to the following links, websocket can extract header, get parameters and protocol just at the stabilization stage:

After that, websocket server only receive text:

const http = require('http');
const WebSocketServer = require('websocket').server;
const server = http.createServer();
server.listen(9898);
const wsServer = new WebSocketServer({
    httpServer: server
});
wsServer.on('request', function(request) {
    const connection = request.accept(null, request.origin);
    connection.on('message', function(message) {
      //I can receive just text 
      console.log('Received Message:', message.utf8Data);
      connection.sendUTF('Hi this is WebSocket server!');
    });
    connection.on('close', function(reasonCode, description) {
        console.log('Client has disconnected.');
    });
});

Send jwt in each request

Having analyzed the previous topics, the only way to send the new o renew token to your websocker backend is sending it in each request:

const ws = new WebSocket('ws://localhost:3210', ['json', 'xml']);
ws.addEventListener('open', () => {
  const data = {
         jwt: '2994630d-0620-47fe-8720-7287036926cd',
         message: 'Hello from the client!' 
     }
  const json = JSON.stringify(data);
  ws.send(json);
});

Not covered topics

  • how perform a jwt regeneration using refresh_token
  • how handle silent regeneration

Let me know if you need this not covered topics.

JRichardsz
  • 14,356
  • 6
  • 59
  • 94
  • Thanks for the detailed answer, however it did address the subject which is handling silent regeneration. I'll answer with our practice – mati.o Nov 10 '20 at 12:06