Alero

Curiosity and boredom can be catalysts for great things

Stateless Insecurities: JWTs—Why They Work and When They Fail

Introduction

If you’ve spent any time working on web authentication, you’ve likely come across JSON Web Tokens, or JWTs. For those who haven’t, here’s the quick rundown: JWTs are a compact, URL-safe way to represent claims between two parties. They’re part of the broader JavaScript Object Signing and Encryption (JOSE) standard and have become a go-to solution for modern web authentication.

Why? Well, JWTs seem to solve a lot of problems. They’re stateless, efficient, and come with the promise of cryptographic security—all in a neat little JSON package. Whether it’s microservices, single sign-on (SSO), or API authentication, JWTs have become a cornerstone of distributed systems.

But here’s the thing: while JWTs offer plenty of benefits, they’re not without their drawbacks. Like any tool, they have limitations and vulnerabilities. The real issue isn’t the JWT spec itself—it’s how they’re used. In the rush to adopt the latest and greatest, it’s easy to overlook critical security implications and design trade-offs.

In this article, we’ll take a critical look at JWTs: how they work, why they’re so popular, and the security pitfalls you need to avoid. If you’re considering using JWTs for authentication or already have them in production, this is your chance to ensure you’re using them the right way—and to understand why they might not always be the best choice.

JWT Anatomy: Understanding the Basics

A JWT consists of three parts – header, payload, and signature. The header and payload are base64url-encoded (without padding) JSON objects which can be decoded from the token to reveal information. Base64url encoding ensures the token remains URL-safe and compact, allowing for easy transmission across different systems.

The header includes critical information about the token, i.e., metadata such as the type of the token (JWT), and the signing algorithm being used, such as HS256 or RSA. The payload contains the actual data being sent to the server, typically known as “claims” – essentially a set of statements about an entity (usually the user) and additional metadata.

There are no constraints on the payload’s content, although it’s crucial to note that a JWT is not encrypted. This means that even though JWTs are cryptographically signed (which ensures their integrity), the payload can still be viewed by anyone who intercepts it. To see this in action, just paste your JWT on https://jwt.io and see for yourself.

Let us look at a JWT token just to make things clearer:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.reGQzG3OKdoIMWLDKOZ4TICJit3EW69cQE72E2CfzRE

The above token contains the information below:

HEADER: ALGORITHM & TOKEN TYPE

{
  "alg": "HS256",
  "typ": "JWT"
}

PAYLOAD: DATA

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true,
  "iat": 1516239022
}

And lastly SIGNATURE:

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  your-256-bit-secret
)


JWT Misconceptions and Limitations

1. Stateless Nature: A Double-Edged Sword

Now that we know what JWTs are, let us talk about how insecure JWTs can be and why it’s probably not a great idea to use JWTs as a means of authentication in your application without giving it proper thought.

JWTs were originally designed to be “short-lived” tokens used for verification and identification purposes. However, the current landscape tells a different story. JWTs are widely used as the primary means of authenticating client applications and managing user sessions across most services—a usage that dramatically diverges from their initial design.

By design, JWTs are stateless, meaning the verifying server doesn’t need to store token information. Instead, all verification details are contained within the token itself. This architectural approach initially seemed revolutionary—eliminating the need for server-side session storage and reducing database lookups.

This stateless property introduces a level of simplicity in authentication mechanisms that attracts developers like bees to flowers. Ironically, this very simplicity becomes the first significant downside to JWTs as authentication mechanisms.

Here’s the big problem: you can’t revoke a JWT before it expires, which makes invalidating sessions pretty much impossible. This means you can’t really log out or instantly invalidate a user session with standard JWT mechanisms.

The problem becomes exponentially more complex when JWTs are used to validate user roles and permissions (a practice I strongly advise against). Consider a scenario where a user’s permissions are revoked mid-session: despite the administrative action, the user retains full privileges until the token expires.

So, what’s the next step? You might think about creating a revocation mechanism with a list of allowed or blocked tokens. While it sounds like a good idea, it kind of defeats the whole point of JWTs being stateless. At this point, one might as well revert to traditional session tokens.

I’m not suggesting session tokens are inherently superior to JWTs—they each have nuanced strengths and weaknesses. The core message is simple: before adopting JWTs as your primary authentication strategy, invest serious thought into their practical implications and potential security vulnerabilities.

2. Security Vulnerabilities

JWTs, like most things in the world of cybersecurity, have their vulnerabilities—no surprises there. If I were to dive into every single one, this article would turn into a small book, so I’ll focus on the ones I consider the most important.

a. Improper Validation

This is hands down one of the most common vulnerabilities I’ve come across, especially in Node.js applications. Developers often forget to verify a token’s signature and jump straight to decoding it. The JWT standard allows tokens to be decoded without verification (it’s part of the design), so libraries like jsonwebtoken make this behavior accessible. Unfortunately, it leads to developers misusing functions like decode, thinking it validates the token when it doesn’t.

Here’s a typical example of what not to do:

import jwt from "jsonwebtoken";

function decodeToken() {
  const payload = jwt.decode(token, {}); // <- THIS BAD! Does not verify signature
  return payload;
}

What should you do instead? Always verify the token’s signature before decoding it. This ensures the token hasn’t been tampered with and was signed using the expected secret or key. Here’s how that looks:

import jwt from "jsonwebtoken";
import crypto from "node:crypto";

const secretKey = crypto.randomBytes(32).toString("hex"); // 256 bits (32 bytes)

function verifyToken() {
  const payload = jwt.verify(token, secretKey, { algorithms: ["HS256"] }); // <- THIS GOOD! Verifies the signature
  return payload;
}

It’s also a good idea to specify an allowlist of acceptable algorithms in your verification step. This prevents scenarios where a library defaults to the algorithm specified in the JWT header. For example, if an attacker sets the algorithm to none, the library might skip validation altogether. This can result in severe security issues like account takeovers.

b. Brute Force Attacks

This shouldn’t come as a shock—anything relying on a secret key is vulnerable to brute force attacks. For JWTs signed with symmetric algorithms like HS256, the strength of your secret key is critical. If your key is weak or predictable, attackers can brute force their way into generating valid tokens.

To mitigate this:

  • Use a cryptographically secure random number generator to create your secret keys. Keys should be at least 256 bits long to withstand brute force attempts.
  • Rotate your keys periodically to reduce the impact of potential leaks.
  • Implement rate-limiting on token verification endpoints to slow down brute force attacks.
    Of course, that is until quantum computers become practical and render much of elliptic curve cryptography obsolete. Until then, these steps are your best defense.
c. Algorithm Confusion Attacks

Now, this one is a bit trickier to explain but stick with me. It primarily affects tokens signed with RSA. Here’s how it works: the attacker takes a JWT signed with RSA, extracts the public key, and modifies the header to use HS256 instead of RS256. Then, they use the public key (intended only for verification) as the shared secret to re-sign the token after tampering with the payload.
If your server isn’t configured to reject such manipulations, it will accept the tampered token as valid.
The solution? Always enforce an allowlist of signing algorithms on your server. This means your server should completely ignore the alg field in the JWT header and only verify tokens using explicitly defined algorithms, like RS256. Fortunately, most modern JWT libraries are designed to mitigate this issue, but developers should always double-check their configurations to avoid unexpected vulnerabilities.

For more in-depth explanations and other examples of vulnerabilities that JWTs are subject to you can check out this article

The Bigger Picture

The security vulnerabilities of JWTs often boil down to developer mistakes and misunderstandings. They’re not inherently insecure, but the design decisions made in the JOSE spec can sometimes make them tricky to use safely. By sticking to best practices—like verifying signatures, using strong secrets, and strictly enforcing algorithms—you can minimize the risks and use JWTs more confidently.
Nevertheless, JWTs aren’t the best choice for managing user sessions because their stateless nature makes things like revoking tokens and managing active sessions more challenging.

PASETO: A More Secure Token Format

While JWTs have their issues, PASETO (Platform-Agnostic Security Tokens) emerges as a more secure alternative for token-based operations. Created by Scott Arciszewski in 2018, PASETO was specifically designed to prevent the cryptographic implementation mistakes that plague JWT implementations.

Understanding PASETO

Unlike JWT’s flexible-but-dangerous approach, PASETO enforces strict standards through its versioning system. Each version specifies exact cryptographic protocols and primitives, eliminating algorithm confusion attacks by design. Currently, there are two main versions in use:

  • v2: Uses sodium_crypto for local tokens and public-key signatures for public tokens
  • v4: Implements more modern cryptographic primitives, including Blake2b and EdDSA

Here’s what a PASETO token looks like:

v2.local.QAxIpVe-ECVNI1z4xQbm_qQYomyT3h8FtV8bxkz8F2rLZ_1noIN0f_5wUUybxR7vZxal1-Aq4a27XIUwhwK45Bsl2n410iql7wf5x5AdQUTdQ5VkOzV58flnVqm99q8_NvfRtSJr6AFYR2f-TVnTd3RsVheVOjYL_E0ED-M587M9jtSpzUYhXHEkEY_zVLZB-eaYiA

The structure is straightforward:

version.purpose.payload.footer
Why PASETO Succeeds Where JWT Falls Short
  1. No Algorithm Confusion
  • Unlike JWT’s vulnerable alg header, PASETO’s version prefix strictly defines the cryptographic methods
  • There’s no way to downgrade or modify the algorithm, eliminating entire classes of attacks
  1. Standardized Implementation
  • PASETO mandates specific cryptographic primitives for each version
  • No choices means no mistakes: developers can’t accidentally choose weak algorithms
  • All tokens are encrypted by default in local mode
  1. Built-in Mitigation of Common Attacks
   // JWT - Vulnerable to algorithm confusion
   const jwt = require('jsonwebtoken');
   jwt.verify(token, publicKey); // Could be tricked by alg: none

   // PASETO - No algorithm confusion possible
   const paseto = require('paseto');
   const token = await paseto.V2.encrypt(payload, key); // Version and purpose are fixed
Important Note on Usage

It’s crucial to understand that, like JWTs, PASETO tokens are not designed for session management. They are meant for secure data transport and limited-time authorizations. If you need session management, you should still use traditional server-side sessions with session IDs.

PASETO’s primary benefits come into play when you need to:

  • Transmit authenticated data between services
  • Create short-lived access tokens
  • Implement stateless authorization checks
  • Pass verifiable claims between systems
Practical Implementation

Here’s a basic example of PASETO usage for a short-lived authorization token:

import { V2 } from 'paseto';
import { randomBytes } from 'crypto';

// Generate a secure key
const key = randomBytes(32);

async function createAuthorizationToken(payload) {
    // Note the short expiration time - this is not for sessions!
    const token = await V2.encrypt({
        data: payload,
        expiresIn: '5m' // Short-lived token for specific operations
    }, key);
    return token;
}

async function verifyToken(token) {
    try {
        const decoded = await V2.verify(token, key);
        return decoded;
    } catch (err) {
        throw new Error('Invalid token');
    }
}
Best Practices for PASETO Implementation
  1. Version Selection
  • Use v4 for new implementations
  • v2 remains secure and is suitable for existing systems
  1. Key Management
   // Generate and store keys securely
   const localKey = randomBytes(32);
   const keyRegistry = new Map();
   keyRegistry.set('current', localKey);
  1. Token Usage
  • Keep tokens short-lived
  • Use for specific, limited operations
  • Don’t store sensitive user data in tokens
  • Never use for long-term sessions

Conclusion: Making Informed Choices About Token Usage

Throughout this article, we’ve taken a critical look at JWTs and their role in modern web authentication. While JWTs have become ubiquitous, our analysis reveals several crucial insights that developers should consider:

Key Takeaways
  1. JWT Misuse is Widespread
    The fundamental issue isn’t with JWTs themselves, but with how they’re commonly misused. The industry’s adoption of JWTs for session management represents a significant departure from their intended purpose, leading to unnecessary security risks and implementation complexities.
  2. Security Implications Matter
    We’ve seen how JWT implementations can fall victim to various attacks – from algorithm confusion to brute force attempts. These vulnerabilities, combined with the stateless nature of JWTs, create significant security challenges that many developers underestimate.
  3. Better Alternatives Exist
    PASETO offers a more secure approach for token-based operations, eliminating many of JWT’s pitfalls through its opinionated design. However, it’s crucial to understand that neither JWT nor PASETO is suitable for session management – that task is better handled by traditional session mechanisms.
Recommendations for Developers

When building authentication systems:

  • Use traditional server-side sessions with session IDs for user session management
  • If you must use tokens, keep them short-lived and purpose-specific
  • Consider PASETO for scenarios where tokens are absolutely necessary
  • Never store sensitive user data or long-term permissions in tokens
  • Implement proper token validation and security measures regardless of the chosen solution

The future of web authentication doesn’t lie in finding a perfect token format, but in using the right tool for each specific use case. By understanding the limitations and appropriate uses of tools like JWT and PASETO, we can build more secure and maintainable authentication systems.

Remember: in security, the most popular solution isn’t always the right one. Take time to understand your specific requirements, consider the security implications, and choose tools that align with both your needs and security best practices.

authentication composition design patterns jwt paseto programming security typescript

Looking for something else?

Leave a Reply

Your email address will not be published. Required fields are marked *