SecurityJWTAuthentication

Doing JWT the right way

Baptiste Loison
BL

Baptiste Loison

Security Engineer

I really hate JWT, I find them way too complicated to implement and so, so easy to mess up, but I must admit, in 2025 they are everywhere. That is why this week we'll explore why you should avoid rolling your own JWT solution, the difference between signing and encrypting tokens, and how to secure your JWT configuration.

If you're not familiar with how JWTs work, I recommend checking out the detailed guide provided by Auth0: Auth0 JWT Documentation, as there is no way I can explain it better than them.

Do not implement JWT yourself

The JWT specification is highly complex, you can see for yourself here: https://datatracker.ietf.org/doc/html/rfc7519. While generating a JWT is relatively straightforward, validating a JWT is the challenging and error-prone part. I'll take a simple example: even if you follow the specification perfectly, you might still be vulnerable to attacks, as the specification allows for the "none" algorithm (it allows the validation of a JWT without any verification, thus creating a vulnerability). This is why I really advise against implementing your own JWT validation logic, and to use a well-known library instead.

JSON Web Tokens are not encrypted!

Contrary to what some people think, JSON Web Tokens are (usually, see this) not encrypted, they are signed, which means that anybody can decrypt them, and so you should absolutely not put sensitive data such as API keys or passwords in them. As a simple proof, I can give you this token, and using tools like jwt.io you can see that it is just a base64 encoded string (with a signature at the end). eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ0aGVzb2xvc2VjZGlnZXN0LmNvbSIsImlhdCI6MTc0NDExNzI4OCwiZXhwIjoxNzc1NjUzMjg4LCJhdWQiOiJldmVyeWJvZHkiLCJzdWIiOiJiYXB0aXN0ZS5sb2lzb25AdGhlc29sb3NlY2RpZ2VzdC5jb20iLCJHaXZlbk5hbWUiOiJCYXB0aXN0ZSIsIlN1cm5hbWUiOiJMb2lzb24iLCJSb2xlIjoiYWRtaW4iLCJkYXRhYmFzZS1wYXNzd29yZCI6IklfUjNBTExZX1NIMFVMRF9OMFRfQjNfSDNSMyJ9.CohYl6LhpSh8INOk2Nh2RApbbZS44z6BK0vfVpArucg

Take 5 minutes to think about the configuration of your JWT library

We'll spend a bit of time on the choice of the algorithm, but TL;DR, the best option when starting a new project is probably to use ES256, which is a good balance between performance of signature verification, security and size of the keys.

Even if you are using a very renowned library, you have to select a secure configuration. The most basic is, as we said before, to be sure that you've disabled the "none" algorithm if you use it for testing purposes.

There are three main types of signature algorithm for JWT:

  • HMAC algorithms (the most common being HS256) are symmetric algorithms, meaning the same key is used for both signing and verification. The secret acts like a password and can be easily brute-forced if it is short. This is why we recommend using a key length equal to the output size of the hashing algorithm divided by 8. For HS256, this is 256/8 = 32, so the secret should be 32 characters long.
  • RSA signatures (the most common being RS256) are asymmetric algorithms, meaning two different keys (one public and one private) are used for signing and verification.
  • ECDSA signatures (the most common being ES256) are also asymmetric algorithms, similar to RSA, but they use elliptic curves instead of prime numbers, offering better security with shorter key lengths.

Also, when using JWT, it is important to set an expiration date that is appropriate for the use case of the token. There is no easy way to revoke or invalidate a token, so the expiration date is crucial to mitigate the impact of an exposed or leaked token.

Case study: Algorithm confusion (CVE-2023-48238)

For this case study, let's look at json-web-token, a library for handling JWTs that is (hopefully) not widely adopted in production environments. It allows users to sign and verify tokens using both RSA and HMAC algorithms.

Algorithm confusion occurs when a JWT library supports both RS256 (asymmetric) and HS256 (symmetric) algorithms, allowing an attacker to modify the token's header to switch from RS256 to HS256. The attacker can then use the server's public key as the secret key for HS256, effectively bypassing the signature verification process and making the server accept a forged token as valid.

Here's the exact code of the json-web-token library that is vulnerable to this attack, Based on the above description of the attack, try to spot where the vulnerability is.

/* ... */
function decode(key, token, cb) {
  if (paramsAreFalsy(key, token)) {
    return prcResult("The key and token are mandatory!", null, cb);
  }

  const parts = token.split(".");

  // check all parts're present
  if (parts.length !== 3) {
    return prcResult("The JWT should consist of three parts!", null, cb);
  }

  // base64 decode and parse JSON
  const header = JSONParse(b64url.decode(parts[0]));
  const payload = JSONParse(b64url.decode(parts[1]));

  // get algorithm hash and type and check if is valid
  const algorithm = algorithms[header.alg];

  if (!algorithm) {
    return prcResult("The algorithm is not supported!", null, cb);
  }

  // verify the signature
  const res = verify(algorithm, key, parts.slice(0, 2).join("."), parts[2]);

  return prcResult((!res && "Invalid key!") || null, payload, header, cb);
}

/* ... */

function verify(alg, key, input, signVar) {
  return alg.type === "hmac"
    ? signVar === sign(alg, key, input)
    : crypto
        .createVerify(alg.hash)
        .update(input)
        .verify(key, b64url.unescape(signVar), "base64");
}

We can see that the decode function extracts the algorithm from the JWT header, and then calls the verify function with the algorithm and the key. The verify function then checks if the decoded algorithm is HMAC or RSA, and calls the appropriate function to check the signature.

But if the developer of an application that uses the library expected to decode an RS256 token, it will call decode with the RSA public key, and the attacker can then modify the JWT header to switch from RS256 to HS256, and use the server's public key as the secret key for HS256, effectively bypassing the signature verification process and making the server accept a forged token as valid.

If you want to see a proof of concept of this attack or more details on how to exploit it, you can check the GitHub advisory related to this vulnerability here.

Stay updated on cybersecurity

Get weekly security tips for SaaS founders and indie makers.