Improving the Security of JSON Web Tokens (JWTs)

#Security#JavaScript#Node#WebDev
Published on February 19, 2023
Improving the Security of JSON Web Tokens (JWTs)

JSON Web Tokens (JWTs) are a popular way to securely authenticate users and exchange information between systems. However, if not used properly, they can pose a security risk to your application. In this article, we will discuss the basic and advanced security measures you should take when using JWTs.

Basic Security Measures

First and foremost, to use JWTs safely, you should always use an HTTPS connection with a valid TLS certificate. This ensures that all data transmitted between the client and server is encrypted and secure.

Another important aspect of using JWTs securely is to keep the key you use for JWTs safe. This key is used to sign the token and verify its authenticity. If it falls into the wrong hands, attackers can generate their own JWTs and impersonate authorized users.

It is also important to never store sensitive data such as email address, password, credit card details in JWTs. Instead, store only data that is not sensitive and can be publicly visible.

Always check the validity of the JWT signature and the expiry date of the token. The signature ensures that the token has not been tampered with, while the expiry date ensures that the token is still valid. If the token is expired or has an invalid signature, then it should not be accepted.

Advanced Security Measures

Despite the above measures, JWTs still pose a security risk since they are irrevocable and have a fixed expiry date. To address this, we can add an extra layer of security to the utility of JWTs.

To achieve this is by incorporating an additional set of data into the JWT, in the form of a hash that includes the salt, client IP address, and client User Agent string. This hash, referred to as a fingerprint, is generated and compared with the one stored by the JWT during verification. If the client's IP address or User-Agent string is modified, the token will be invalidated.

Example Code

In order to assist you in implementing these measures, we have developed a simple example of this method, which is available for review and testing on our GitHub repository. It is important to note, however, that while we aim to convey the logic of the approach, the implementation can be done as desired, including the creation of a more robust fingerprint.

Sample fingerprint function

const createFingerprint = (req) => {
  const salt = '3d4bd49fadee0613cec5a145a0173876';
  const addr = req.headers['x-forwarded-for'] || req.socket.remoteAddress;
  const userAgent = req.get('user-agent');
  return crypto
    .createHash('sha256')
    .update(salt + addr + userAgent)
    .digest('hex');
};

Sample API routes with fingerprint validation

// Import the necessary modules, including crypto, express, and jsonwebtoken.
const crypto = require('crypto');
const router = require('express').Router();
const jwt = require('jsonwebtoken');

// Example constants are defined for a secret key, a user ID, and an expiration time for the JWT (JSON Web Token).
const secret = '6iwMV0Hc6WU0XPMpqugnHsOg3kJqhRTQ';
const userId = 'b427a317';
const expiresIn = '8h';

// Generate a unique fingerprint for a given HTTP request.
const createFingerprint = (req) => {
  const salt = '3d4bd49fadee0613cec5a145a0173876';
  const addr = req.headers['x-forwarded-for'] || req.socket.remoteAddress;
  const userAgent = req.get('user-agent');
  return crypto
    .createHash('sha256')
    .update(salt + addr + userAgent)
    .digest('hex');
};

// This route is used for generating a JWT with a unique fingerprint.
router.post('/getToken', function (req, res) {
  const fingerprint = createFingerprint(req);
  const data = { userId, fingerprint };
  const token = jwt.sign(data, secret, { expiresIn });

  res.json({ token });
});

// This route is used to verify a token. It extracts the token from the request's 'authorization' header
router.post('/checkToken', function (req, res) {
  const token = req.headers['authorization'];
  const fingerprint = createFingerprint(req);

  // If there's no token in the header, it returns a 401 status (Unauthorized).
  if (!token) return res.sendStatus(401);

  try {
    // If there is a token, it attempts to verify it using the secret key. If the token is valid and the fingerprint matches the one in the token, it returns the data contained in the token.
    const data = jwt.verify(token, secret);
    if (!data || String(data.fingerprint) !== String(fingerprint)) {
      return res.sendStatus(403);
    }
    res.json({ data });
  } catch (err) {
    // Otherwise, it returns a 403 status (Forbidden).
    return res.sendStatus(403);
  }
});

module.exports = router;

https://github.com/stacklegend/jwt-improve-security

Conclusion

In conclusion, JWTs are a powerful tool for securely authenticating users and exchanging information between systems. However, it is important to implement basic and advanced security measures to ensure that the JWTs are not misused.

At Stacklegend, we deliver IT solutions that work in enterprise environments with security in mind. Contact us to learn more about how we can help you build secure systems and achieve higher levels of security for your company.

Stacklegend - We build the digital future.

https://stacklegend.com/