Saturday, October 12, 2019

Websocket authorization using "ws", Node, and external tokening system

My project users an external system to manage a user's authorization, this external system returns a token which is stored in a cookie on the browser.  The backend Node.js application needs to make sure authorization is appropriate prior to letting a user make HTTP or WebSocket calls.  NOTE: User authentication across node instances will not be discussed in this post.

Environment:
CentOS Linux release 7.3.1611 (Core)
node v8.16.0
Backend ws version 7.0.0

HTTP:  Nothing additional needs to happen for the client code since the cookie Header is automatically sent with the HTTP request.  The backend code for HTTP calls include grabbing the cookie Header, parsing it for the token, and determine token's validity (return HTTP 401 Unauthorized if not valid, and continue on if valid).

WebSocket:
This ws issue and this article helped me a lot with this possible solution.

Client side code:
Since an external token system is being used (and not basic auth), the Authentication header cannot be used for my use case as well.  Adding custom headers are also not allowed for websocket connections.

Therefore, one workaround includes adding a URL param to the WebSocket URL, as seen in the below example.

const host = window.location.host;
const socket = new WebSocket(`wss://${host}/wsapi/?token=${token}`);

Backend code:

  • The token needs to be retrieved from the WebSocket URL.  
  • A subsequent call to determine if the token's validity needs to happen.  
    • Valid:  For this example, I have assumed it is a valid token.  
      • Look up any user info you want with the valid token, and add custom data "userInfo" to the request object
      • The verifyClient callback function sets the result true.
    • Not Valid: The verifyClient callback function sets the result as false, code as 401, and name as Unauthorized.

NOTE:  The ws documentation discourages the use of verifyClient, and instead recommends the upgrade event.  I'll have to try this sometime.


// During websocket handshake, validate the user is authorized
const wss = new WebSocketServer({
  server: server,
  verifyClient: (info, callback) => {
    const token = info.req.url.split('token=')[1];

    // check if this is a valid token, assume true
    const isValidToken = true;
    
    if (!isValidToken) {
        console.log("Error: token not valid");
        callback(false, 401, 'Unauthorized');
    } else {
      // NOTE: If we wanted to add custom user info here, we would look it up and add it.
      
      const userData = lookUpUserInfo(token);
      // add custom attributes to req object
      info.req.userInfo = {
        firstname: userData.firstname,
        lastname: userData.lastname,
        email: userData.email,
      }

      callback(true);
    }
  }
});

// From this point on we know the user is authorized since it was handled in the initial handshake
wss.on('connection', (ws, request) => {
  console.info("websocket connection open");

  // NOTE: If we wanted to access the custom user info here, we could access with request.user
  ...
  
  // Handle when message is sent from client
  ws.on('message', requestData => {
    // We don't have access to request.userInfo from when the connection was initially established.
    // So if you want access to the userInfo you would have to pass in the token again, and retrieve
    // the user data with lookUpUserInfo(token)
  
  });
  ...
});

1 comment:

I appreciate your time in leaving a comment!