Table of Contents

JWT Tokens: How they work and how to securely implement them

Introduction

JSON Web Tokens (JWT) are a way to send information in the form of a token. They are often used to authenticate users, but can be used for a lot of other things, such as managing password resets.

In this article, we will go over what JWT tokens are, how they work, and how to implement them properly.

Deconstructing a JWT token

Take a look at this string here: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIiwibmFtZSI6IlF1aW50ZXNzZW5jZSIsImlhdCI6MTUxNjIzOTAyMn0.iyJdZNCwXy_QL8Jba8p0apBQKGYx087C09uePV2w1ic

You may recognize some sort of pattern here, with 3 distinct parts that are base64 encoded.

  1. eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
  2. eyJzdWIiOiIxIiwibmFtZSI6IlF1aW50ZXNzZW5jZSIsImlhdCI6MTUxNjIzOTAyMn0
  3. iyJdZNCwXy_QL8Jba8p0apBQKGYx087C09uePV2w1ic

The header (eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9) contains information about the token, its algorithm, and its type.

You can read it like this:

1$ echo "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" | base64 -d
2{"alg":"HS256","typ":"JWT"}

And we discover that our algorithm is HS256, which stands for HMAC-SHA256.

There are two other families of algorithms that are commonly used: RS256 and ES256.

PS256 exists as well, but is less common.

We will talk more about the different algorithms later on.

Payload

The payload contains the actual data that we want to transmit.

1$ echo "eyJzdWIiOiIxIiwibmFtZSI6IlF1aW50ZXNzZW5jZSIsImlhdCI6MTUxNjIzOTAyMn0" | base64 -d
2{"sub":"2","name":"Quintessence","iat":1516239022}

This is a JSON object, which contains the following information:

  • sub: The subject of the token, which is the user ID in our case.
  • iat: The issued at timestamp, which is the time the token was created.

These two fields are standard, and are defined in the RFC 7519.

These other fields are also standard, but less common:

  • iss (Issuer): Identifies the principal that issued the JWT. Case-sensitive string (StringOrURI).
  • sub (Subject): Identifies the subject of the JWT. Must be unique within the issuer's context or globally. Case-sensitive string (StringOrURI).
  • aud (Audience): Specifies the recipients intended to process the JWT. Value can be a string or an array of strings.
  • exp (Expiration Time): Defines the time after which the JWT must not be accepted. NumericDate value.
  • nbf (Not Before): Defines the time before which the JWT must not be accepted. NumericDate value.
  • iat (Issued At): Specifies the time at which the JWT was issued. NumericDate value.
  • jti (JWT ID): Provides a unique identifier for the JWT to prevent replay attacks. Case-sensitive string.

Signature

This is the last, and most important part of the token. It is a signature that's computed using the algorithm specified in the header, and it allows us to verify that the token was created by the issuer.

Which means that a JWT can be read by anyone, but only the issuer can create a valid signature, thanks to the JWT key.

As this section it is neither JSON nor text data, we will hexdump it to see what's inside:

1$ echo "iyJdZNCwXy_QL8Jba8p0apBQKGYx087C09uePV2w1ic" | base64 -di | xxd
200000000: 8b22 5d64 d0b0 5f24 0bf0 96da f29d 1aa4  ."]d.._$........
300000010: 140a 198c 74f3 b0b4 f6e7 8f57 6c35 89    ....t......Wl5.

(note the i flag that is passed to base64, it means --ignore-garbage)

We can see a bunch of bytes that looks random, but this exact sequence of bytes is the HMAC-SHA256 (HS256) signature of the header and payload that's computed using the secret key, without this secret key, we wouldn't be able to create a valid signature and therefore the server will reject the token.

Signing algorithms

HS256 (HMAC-SHA256)

Working: Uses a shared secret key with HMAC and SHA-256 to sign and verify the JWT. Both signing and verification use the same key (symmetric).

Pros:

  • Simple and fast.
  • Efficient for smaller systems.

Cons:

  • Shared key increases risk of exposure.
  • Not suitable for multi-party or distributed systems.

RS256 (RSA-SHA256)

Working: Uses RSA with SHA-256 for signing. The private key signs the JWT, and the public key verifies it (asymmetric).

Pros:

  • Public-private key model adds flexibility and security.
  • Well-supported and widely understood.

Cons:

  • Larger keys lead to slower performance.
  • Requires careful key management.

ES256 (ECDSA-SHA256)

Working: Employs Elliptic Curve Digital Signature Algorithm (ECDSA) with SHA-256 for signing. Private key signs, and public key verifies (asymmetric).

Pros:

  • Provides higher security with smaller key sizes.
  • More efficient for resource-constrained environments (e.g., IoT).

Cons:

  • Slower than HS256.
  • More complex to implement than RSA.

PS256 (RSA-PSS with SHA-256)

Working: Utilizes RSA Probabilistic Signature Scheme (PSS) with SHA-256 for signing. Private key signs, and public key verifies (asymmetric).

Pros:

  • More secure padding compared to RS256, resistant to certain attacks.
  • Ideal for high-security applications.

Cons:

  • Computationally intensive.
  • Requires modern cryptographic libraries for implementation.

Attacking JWT tokens

JWT tokens are vulnerable to multiple attacks, and we will see 2 of them here, that are the most common.

For our example, we are a malicious user, and we want to authenticate as a legitimate user.

Replay attacks CWE-294

This attack is pretty simple, and it's the most common one. However its real-world impact is very low as we have to capture a token through HTTPS traffic or we would have to attack the user's machine using an infostealer.

But let's say that you successfully stole a token, congratulations, you just have to send it to the server via the Authorization header (or a cookie, depends on the remote implementation), and that's it. You have a valid session on the web application as long as the token hasn't expired. Meaning that its iat field is not too far in the future, or the exp field is in the past.

Most of the time, the server's implementation is faulty, and won't blacklist the token if the user has prematurely ended the session by logging out, as JWT can not be revoked as easily as a session cookie.

Improper verification of the signature CWE-347

If our web application was lazily implemented, they might not verify the signature of the token, thus allowing us to decode the payload, edit it, rencode it, and send our forged token to the server.

Our token's signature will be invalid, but the server will accept it in that case, and we will have a valid session on the web application.

Weak signing key CWE-1391

The signing key is the most important part of the JWT token, and it's the only thing that can be changed.

When using HS256, the secret key is a simple string, unlike RSA or ECDSA which have fully functional CLI tools to generate valid keypairs. This means that the developer has to manually generate a key, which could just be a cryptographically secure random string, or... password, for example.

In that case, the attacker can use hashcat and a wordlist to attempt to find the secret key, and if it happens to be a common password, it's game over.

Reforging a valid signature is as easy as running this python script which requires the PyJWT library:

1import jwt
2encoded = jwt.encode({"sub":"1","name":"Admin","iat":1516239022}, "password", algorithm="HS256")
3print(encoded)

And the output is:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIiwibmFtZSI6IkFkbWluIiwiaWF0IjoxNTE2MjM5MDIyfQ.uXUMnVnIA3FiDcuS4tSr4q2t3tA3frmCw38eXgIabGc

Which, testing in CyberChef reveals that our JWT is completely valid.

Algorithm confusion vulnerability

In 2023, the NodeJS library fast-jwt was found to be vulnerable to algorithm confusion attacks, which allows an attacker to forge a JWT token with a different algorithm than the one used by the server.

It was given the CVE-2023-48223 Common Vulnerabilities and Exposures (CVE) identifier.

The vulnerability worked by exploiting the improper handling of PEM-formatted public keys and the lack of strict algorithm enforcement during JWT verification. Specifically, it allowed an attacker to craft a malicious JWT token using the HS256 algorithm while signing it with the server's public RSA key. This exploit relied on the fact that the library's publicKeyPemMatcher function did not properly match all common PEM formats, particularly the BEGIN RSA PUBLIC KEY header.

By obtaining the server's public RSA key—potentially by deriving it from two JWT tokens, the attacker could exploit the flaw. Using tools like rsa_sign2n to extract public key information and jwt_tool to forge tokens, the attacker could sign arbitrary payloads. These forged tokens would be accepted as valid by the server if it failed to explicitly enforce the expected algorithm (e.g., RS256).

This vulnerability underscores the importance of strictly enforcing the expected algorithm when verifying JWT tokens and validating key formats. Applications using RS256 with public keys containing a BEGIN RSA PUBLIC KEY header were particularly susceptible, highlighting the need for rigorous library updates and algorithm validation practices.

Key points to implement JWT tokens properly

To securely implement JWT tokens, be sure to:

  1. If you're using HS256, use a strong secret key, tr -dc 'A-Za-z0-9!"#$%&'\''()*+,-./:;<=>?@[\]^_{|}~' </dev/random | head -c 64 can be used to generate a cryptographically secure random string in your terminal on Unix-like systems.
  2. Prefer using RS256 or ES256, use a proper keypair generator, such as OpenSSL.
  3. Set short expiration times, and prefer refreshing the token when it's about to expire to avoid replay attacks.
  4. Blacklist the token if the user manually logs out, this way if the token is stolen, the server will reject it.

Applying these 4 points is enough to harden your application against most of the attacks regarding JWT tokens.

Conclusion

JWT tokens are a powerful tool for secure communication and authentication when implemented correctly. They offer a compact, self-contained way to share information between parties. However, their flexibility and widespread adoption come with risks, especially if not implemented securely.

This article has outlined the anatomy of a JWT, explored various signing algorithms, and discussed common vulnerabilities such as replay attacks, improper signature verification, and weak signing keys. By following the implementation guidelines, including using robust keys, short expiration times, and blacklisting tokens upon logout, developers can significantly mitigate these risks and enhance the security of their applications.

Our services

We are a team of dedicated cybersecurity consultants focused on uncovering weaknesses and helping organizations strengthen their security posture.

We ensure confidentiality with encrypted communications via pgp and accept any confidentiality clauses you may propose.

We are specialized in pentesting, code auditing and monitoring to ensure the security of your services and infrastructure.

You can contact us at [email protected] if you have any questions or need help with your cybersecurity needs.