Use JSON Web Tokens (JWT) to Authenticate Users over WebSockets

Traducciones al Español
Estamos traduciendo nuestros guías y tutoriales al Español. Es posible que usted esté viendo una traducción generada automáticamente. Estamos trabajando con traductores profesionales para verificar las traducciones de nuestro sitio web. Este proyecto es un trabajo en curso.
Create a Linode account to try this guide with a $ credit.
This credit will be applied to any valid services used during your first  days.

WebSockets allow you to add real-time communications to your web application. It is the technology frequently behind instant messaging, collaboration, and multiplayer gaming over the web. Any time you want to facilitate real-time communication between clients and servers, the WebSockets API is one of your best options.

However you use WebSockets, you likely want to limit connections to authorized users only. By default, WebSockets lack authentication, but you can effectively add your own using JSON Web Tokens (JWTs). These are lightweight and secure tokens that can provide an easy and reliable authentication solution.

This guide reviews the concepts behind WebSockets and JWTs and then walks you through an example application using JWTs to authenticate WebSocket connections.

What Are WebSockets?

The WebSocket Protocol is an open standard (RFC 6455) for real-time communications. It allows for full-duplex, bidirectional communication between clients and servers on the web.

WebSockets can be useful in numerous contexts where real-time information transmission is key. A typical example is an instant messenger or chat application. WebSockets can also handle things like collaborative document editing and online multiplayer games.

To learn more about WebSockets, take a look at our Introduction to WebSockets guide.

What Are JSON Web Tokens?

JSON Web Token (JWT) is also an open standard (RCF 7519). It defines a compact and secure way to transmit information via JSON. These tokens are significantly smaller than the ones generated by similar standards, such as the Security Assertion Markup Language (SAML) tokens. At the same time, JWTs are typically secured by a signing algorithm, ensuring their integrity from end to end.

Decoded JWTs are formatted as JSON, as opposed to the XML format often used in similar token standards. This tends to make them more approachable for web development, and also opens up JSON’s extensive web development tooling.

For more on JWTs, check out our How to Authenticate with JSON Web Tokens (JWTs) guide.

Using WebSockets and JWTs Together

On their own, WebSockets do not include any authentication. WebSocket connections can be resource expensive, so it is a good idea to limit whom you allow to connect. Beyond that, most WebSocket applications benefit from implementing user authentication. This lets you limit who can see your user’s communications and sensitive information.

JWTs provide a good authentication option. They are lightweight, secure, and fit comfortably into a wide range of web applications. WebSocket connections are frequently authenticated via URL parameters. This makes JWTs an ideal option when implementing authentication for WebSocket connections.

The list below provides an example model of how you might use WebSockets and JWTs in tandem. A host of other approaches are possible, but this provides a strong framework to start from.

  • Create an HTTP/HTTPS server with two endpoints:

    • One that generates a JWT provided set of valid credentials
    • One that establishes a WebSocket connection if a valid JWT is provided
  • Create a WebSocket server that:

    • Broadcasts messages to the client pool
    • Periodically notifies and removes clients with expired JWTs
  • Create a frontend that:

    • Accepts login credentials
    • Fetches a JWT from the server when credentials are submitted
    • Initiates a WebSocket connection when it receives a JWT

In the next section, you can follow along to build a chat application applying the above model.

Build an Example Application

In this section, you learn how to implement a WebSocket server and how to use JWTs to authenticate its connections. It uses the example model shown in the previous section. The result is a simple instant messaging application with user login and authentication handling.

Before You Begin

  1. If you have not already done so, create a Linode account and Compute Instance. See our Getting Started with Linode and Creating a Compute Instance guides.

  2. Follow our Setting Up and Securing a Compute Instance guide to update your system. You may also wish to set the timezone, configure your hostname, create a limited user account, and harden SSH access.

Note
The steps in this guide are written for a non-root user. Commands that require elevated privileges are prefixed with sudo. If you are not familiar with the sudo command, see the Linux Users and Groups guide.

Install Node.js

This example uses Node.js and the Express.js application framework. Express.js makes putting a server together quick and straightforward. For this guide’s example, it makes it easier to focus on the WebSocket and JWT implementation.

  1. Install Node.js.

    On Debian and Ubuntu distributions, use these commands:

     curl -fsSL https://deb.nodesource.com/setup_15.x | sudo -E bash -
     sudo apt install nodejs
    

    On CentOS, use the following command:

     curl -fsSL https://rpm.nodesource.com/setup_15.x | sudo -E bash -
     sudo yum install nodejs
    
  2. Set NPM to use the latest version of Node.js.

     sudo npm install npm@latest -g
    

Set Up the Project

  1. Create a directory for the project. In this example, ws-jwt-example is used as the project and directory name. The example project lives in the current user’s home directory.

     mkdir ~/ws-jwt-example
    
  2. Change into the project directory. For the remaining steps in this example, the guide assumes you are in ws-jwt-example directory.

     cd ~/ws-jwt-example
    
  3. Create a public directory to be used to serve static files for the application’s front end.

     mkdir public
    
  4. Initialize the Node.js project.

     npm init
    

    You are prompted to enter information related to your project. You can simply press Enter for each prompt to use the default values.

  5. Install Express JS, the JSON Web Token package, and the WebSocket package for your project.

     npm install express --save
     npm install jsonwebtoken --save
     npm install ws --save
    

Create the Server

  1. Create a JavaScript file called server.js. Add the contents of the example-server.js file. This file creates an Express.js server with an endpoint for providing JWTs for authentication and an endpoint for making WebSocket connections. It also serves static files for the application’s front end.

    Note

    The example stores credentials in the application code for the sake of convenience. However, in a production scenario, you should not store credentials in application code, and ideally, passwords should be stored encrypted.

    Additionally, the example uses a simple secret. But in production, you should use a secret that conforms to the standards for the signing algorithm you are using. For instance, the HMAC SHA256 algorithm in this example should be given a 256-bit secret. You can create one with a random 64-character hex string or a random 44-character Base64 string.

    What follows is a breakdown of some of the key points in the server.js file. These focus on the parts that provide authentication tokens and those that set up the WebSocket server.

    • The Express JS server provides an endpoint for authentication. It takes in a username and password and attempts to match them to stored credentials. If it finds a match, it serves a JWT in its response.

      File: server.js
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      
      // [...]
      
      // Create an endpoint for authentication.
      app.get('/auth', (req, res) => {
          res.send(fetchUserToken(req));
      });
      
      // Check request credentials, and create a JWT if there is a match.
      const fetchUserToken = (req) => {
          for (i=0; i<userCredentials.length; i++) {
              if (userCredentials[i].username == req.query.username
                  && userCredentials[i].password == req.query.password) {
                  return jwt.sign(
                      {
                          "sub": userCredentials[i].userId,
                          "username": req.query.username
                      },
                      jwtSecret,
                      { expiresIn: 900 } // Expire the token after 15 minutes.
                  );
              }
          }
      
          return "Error: No matching user credentials found.";
      }
      
      // [...]
              
      
    • The WebSocket server mounts to the Express JS server’s /ws route. When a connection attempt is made, the WebSocket server reads a token from the URL. If it can verify the JWT, then it allows the connection and adds the user to the client pool.

      When the server receives a message from one of the clients, it broadcasts it to everyone in the client pool. Before it broadcasts, the WebSocket server checks for and handles any clients with expired tokens.

      File: server.js
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      
      // [...]
      
      // Define the WebSocket server. Here, the server mounts to the `/ws`
      // route of the Express JS server.
      const wss = new WebSocket.Server({ server: expressServer, path: '/ws' });
      
      // Create an empty list that can be used to store WebSocket clients.
      var wsClients = [];
      
      // Handle the WebSocket `connection` event. This checks the request URL
      // for a JWT. If the JWT can be verified, the client's connection is added;
      // otherwise, the connection is closed.
      wss.on('connection', (ws, req) => {
          var token = url.parse(req.url, true).query.token;
      
          var wsUsername = "";
      
          jwt.verify(token, jwtSecret, (err, decoded) => {
              if (err) {
                  ws.close();
              } else {
                  wsClients[token] = ws;
                  wsUsername = decoded.username;
              }
          });
      
          // Handle the WebSocket `message` event. If any of the clients has a token
          // that is no longer valid, send an error message and close the client's
          // connection.
          ws.on('message', (data) => {
              for (const [token, client] of Object.entries(wsClients)) {
                  jwt.verify(token, jwtSecret, (err, decoded) => {
                      if (err) {
                          client.send("Error: Your token is no longer valid. Please reauthenticate.");
                          client.close();
                      } else {
                          client.send(wsUsername + ": " + data);
                      }
                  });
              }
          });
      });
              

Create the Client

  1. Create a JavaScript file named main.js in the public directory. Add the contents shown in the example-main.js file. This file fetches the JWT whenever login credentials are provided, makes the WebSocket connection, and displays any messages received.

    As with the server file above, below is a breakdown of some of the key points in the client-side JavaScript.

    • This function attempts to authenticate the user whenever a username and password are provided. If it gets back a token, it calls another function to open the WebSocket connection.

      File: public/main.js
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      
      // [...]
      
      // Take the entered username and password and attempt to authenticate them. If the
      // response indicates an error, provide the error message.
      const getJwtAuth = () => {
          var username = document.querySelector("#username").value;
          var password = document.querySelector("#password").value;
      
          fetch("http://localhost:3000/auth?username=" + username + "&password=" + password)
              .then(response => response.text())
              .then((response) => {
                  if (response.includes("Error")) {
                      errorMessageSpan.innerHTML = response;
                  } else {
                      errorMessageSpan.innerHTML = "";
                      openWsConnection(response);
                  }
              })
              .catch(err => console.log(err));
      }
      
      // [...]
              
      
    • This function attempts to open a WebSocket connection using the JWT provided by the server. It first makes sure to close any existing WebSocket connection (e.g., from a previous login). Whenever it opens a new connection, it sends a message over the WebSocket. Anytime a message is received, it displays it.

      File: public/main.js
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      
      // [...]
      
      // Open the WebSocket connection using the JWT.
      const openWsConnection = (jwtAuth) => {
          // If a connection already exists, close it.
          if (ws) {
              ws.close();
          }
      
          ws = new WebSocket("ws://localhost:3000/ws?token=" + jwtAuth);
      
          // Send a message whenever the WebSocket connection opens.
          ws.onopen = (event) => {
              console.log("WebSocket connection established.");
              ws.send("Hello, world!");
          }
      
          // Display any new messages received in the `messageDiv`.
          ws.onmessage = (event) => {
              console.log("WebSocket message received: ", event.data);
      
              newMessageDiv = document.createElement("div");
              newMessageDiv.textContent = event.data;
      
              messageDiv.appendChild(newMessageDiv);
          }
      
          ws.onerror = (event) => {
              console.log("WebSocket error received: ", event);
          }
      
          ws.onclose = (event) => {
              console.log("WebSocket connection closed.");
          }
      }
      
      // [...]
              
      
    • This function sends a user-entered message. Before doing so, it ensures that the user is logged in and that the message is not empty.

      File: public/main.js
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      
      // Send the message entered by the user. First, however, ensure that the user is logged
      // in and that the message field is not empty.
      const sendWsMessage = () => {
          var messageContent = document.querySelector("#messageContent").value;
      
          if (ws) {
              if (messageContent != "") {
                  ws.send(messageContent);
              } else {
                  errorMessageSpan.innerHTML = "Error: Message content cannot be empty."
              }
          } else {
              errorMessageSpan.innerHTML = "Error: You must log in to send a message."
          }
      }
              
      
  2. Complete the frontend by creating an index.html file in the public directory. Add the index.html file to the directory.

    File: public/index.html
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    
    <!doctype html>
    
    <html>
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Example App Using WebSockets and JWTs</title>
        <style>
            h1 {
                text-align: center;
            }
    
            .row {
                display: flex;
            }
    
            .column {
                flex: 50%;
            }
    
            #loginForm {
                text-align: center;
            }
    
            #errorMessage {
                color: red;
            }
        </style>
    </head>
    <body>
        <h1>Example App Using WebSockets and JWTs</h1>
        <div class="row">
            <div class="column" id="loginForm">
                <span id="errorMessage"></span>
                <form>
                    <label for="username">Username: </label>
                    <input type="text" id="username"/><br/>
                    <label for="password">Password: </label>
                    <input type="text" id="password"/><br/>
                    <input type="button" id="login" value="Login" onclick="getJwtAuth()"/>
                </form>
                <form>
                    <label for="messageContent">Message: </label>
                    <textarea id="messageContent"></textarea>
                    <input type="button" id="send" value="Send" onclick="sendWsMessage()"/>
                </form>
            </div>
            <div class="column" id="messages"></div>
        </div>
        <script src="./main.js"></script>
    </body>
    </html>
        

Run the Example

The example application is ready for a test run. Follow the steps below to try it out.

  1. Start the Express.js server.

     node server.js
    

    Express serves the application on localhost:3000. To visit the application remotely, you can use an SSH tunnel.

    • On Windows, you can use the PuTTY tool to set up your SSH tunnel. Follow the appropriate section of the Using SSH on Windows guide, replacing the example port number there with 3000.

    • On OS X or Linux, use the following command to set up the SSH tunnel. Replace example-user with your username on the application server and 192.0.2.0 with the server’s IP address.

        ssh -L3000:localhost:3000 example-user@192.0.2.0
      
  2. Navigate to localhost:3000 in your browser. There, enter credentials for one of the users you set up in the server.js file. In your browser, userA from the example above is used.

    Example application using WebSockets and JWTs

  3. Click Login. You should see a “Hello, world!” message from the user you logged in as.

    You can open a new browser window or tab and login as a different user. You should see a message from that new user also display on the window/tab where the first user is logged in.

    Enter messages using the Message field, and you can see them appearing for the other logged-in user as well.

    Verify that after 15 minutes of being logged in, you get an error message if you try to submit any more messages without reauthenticating.

More Information

You may wish to consult the following resources for additional information on this topic. While these are provided in the hope that they will be useful, please note that we cannot vouch for the accuracy or timeliness of externally hosted materials.

This page was originally published on


Your Feedback Is Important

Let us know if this guide was helpful to you.


Join the conversation.
Read other comments or post your own below. Comments must be respectful, constructive, and relevant to the topic of the guide. Do not post external links or advertisements. Before posting, consider if your comment would be better addressed by contacting our Support team or asking on our Community Site.
The Disqus commenting system for Linode Docs requires the acceptance of Functional Cookies, which allow us to analyze site usage so we can measure and improve performance. To view and create comments for this article, please update your Cookie Preferences on this website and refresh this web page. Please note: You must have JavaScript enabled in your browser.