Post

JWT Security Deep Dive: How JSON Web Tokens Work, Why They Break, and How Attackers Exploit Them

A comprehensive guide to JSON Web Tokens, covering how they work, common vulnerabilities, and how attackers exploit weak implementations.

JWT Security Deep Dive: How JSON Web Tokens Work, Why They Break, and How Attackers Exploit Them

Introduction

Authentication is one of the most fundamental problems every web application has to solve.

When you log into a website such as GitHub, Netflix, or your banking portal, the server needs a way to remember who you are without asking for your password on every request.

There are multiple ways to achieve this:

  • Traditional server-side sessions
  • OAuth
  • API Keys
  • JSON Web Tokens (JWT)

JWTs have become one of the most widely used authentication mechanisms because they are simple, portable, and stateless. However, they are also one of the most commonly misconfigured security mechanisms. A single mistake in how a JWT is generated, signed, or verified can completely compromise an application’s authentication system.

This article explains JWTs from first principles, explores how they work internally, discusses common vulnerabilities, and teaches you how to recognize and exploit JWT-related issues in CTFs and security assessments while also explaining how developers can securely implement them.

By the end of this article, you should understand not only what a JWT is, but why it works, how attackers abuse weak implementations, and how to defend against those attacks.


Why Do We Need Authentication?

Imagine a website where every page request required entering your username and password.

1
2
3
GET /profile
Username:
Password:

Now imagine loading a page with ten images.

Your browser would have to authenticate ten times just to display that page.

Clearly, this isn’t practical.

The server needs a way to remember:

“This request belongs to Alice.”

This remembered identity is called a session.


Traditional Session-Based Authentication

Historically, websites solved this problem using server-side sessions.

The authentication flow looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Browser
   |
   | Username + Password
   |
Server
   |
Valid Credentials?
   |
  Yes
   |
Creates Session
Session ID = A8F39D92
   |
Stores:
A8F39D92 → Alice
   |
Returns Cookie

The browser stores:

1
sessionid=A8F39D92

Every future request automatically includes:

1
Cookie: sessionid=A8F39D92

The server checks its session database:

1
2
3
4
A8F39D92
↓

Alice

The important point is:

The browser only stores a random identifier. The actual user information remains on the server.


The Problem with Traditional Sessions

While secure and widely used, server-side sessions have some drawbacks.

The server must maintain a session database for every logged-in user.

In large distributed applications, multiple servers need to share session data.

This increases infrastructure complexity.

JWTs were introduced to solve this scalability problem.


What is a JSON Web Token (JWT)?

A JWT is a compact, digitally signed token that allows the server to verify user information without storing session data.

Instead of storing:

1
Session ID → Alice

the server stores nothing.

Instead, it gives the browser a signed token containing the necessary information.

Example:

1
2
3
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJ1c2VybmFtZSI6ImFsaWNlIn0.
hKQX6...

The browser stores this token.

Every request sends:

1
Authorization: Bearer <JWT>

or

1
Cookie: session_token=<JWT>

The server verifies the signature and trusts the contents if the signature is valid.

This is why JWT authentication is called stateless authentication.


JWT Structure

Every JWT has three components.

1
HEADER.PAYLOAD.SIGNATURE

Example:

1
xxxxx.yyyyy.zzzzz

Each part is Base64URL encoded.


Part 1 — Header

Example:

1
2
3
4
{
  "alg": "HS256",
  "typ": "JWT"
}

This tells the server:

  • Which signing algorithm was used.
  • That this object is a JWT.

The header is not encrypted.

Anyone can decode it.


Part 2 — Payload

Example:

1
2
3
4
5
{
  "username": "alice",
  "role": "admin",
  "exp": 1750000000
}

The payload contains claims.

Claims are pieces of information about the authenticated user.

Common claims include:

ClaimMeaning
subSubject (user ID)
usernameUsername
roleUser role
emailEmail
expExpiration time
iatIssued at
nbfNot before

Again:

The payload is NOT encrypted.

Anyone can read it.

Never place secrets inside the payload.


Part 3 — Signature

The signature is what makes JWTs trustworthy.

The server computes:

1
2
3
4
HMAC_SHA256(
Base64(header) + "." + Base64(payload),
SECRET_KEY
)

Only someone possessing the secret key can generate a valid signature.

This prevents users from modifying the payload.


Why Can’t We Simply Modify the Payload?

Suppose your JWT contains:

1
2
3
{
  "role": "user"
}

You edit it to:

1
2
3
{
  "role": "admin"
}

The payload changes.

Therefore,

1
header.payload

changes.

Since the signature depends on those bytes, the old signature becomes invalid.

The server recomputes the signature.

If the values don’t match:

1
Invalid Signature

Authentication fails.


Base64 Is NOT Encryption

This is one of the most common misconceptions.

Many beginners believe JWT payloads are encrypted.

They’re not.

Base64 simply changes the representation.

Example:

1
2
3
4
alice
↓

YWxpY2U=

Anyone can decode it.

You can verify this using online Base64 decoders or command-line tools.


The Difference Between Signing and Encryption

Signing provides:

  • Integrity
  • Authenticity

Encryption provides:

  • Confidentiality

JWTs are usually signed, not encrypted.

That means everyone can read the payload, but nobody should be able to modify it without invalidating the signature.


Authentication vs Authorization

These terms are often confused.

Authentication answers:

Who are you?

Authorization answers:

What are you allowed to do?

Example:

1
2
3
4
5
6
7
8
User:
Alice

Authenticated?
Yes

Authorized to delete users?
No

A JWT only proves identity if correctly verified.

The application must still verify permissions.


Common JWT Vulnerabilities

Understanding these mistakes is critical for both penetration testing and CTFs.


1. Hardcoded Secret Keys

One of the most dangerous mistakes is exposing the signing key to the client.

Example:

1
const SECRET_KEY = "supersecret";

If an attacker knows the secret:

They can generate:

1
2
3
{
  "username":"admin"
}

Sign it correctly.

And produce a perfectly valid JWT.

At that point, the server cannot distinguish the forged token from a legitimate one.

This completely defeats the purpose of using JWTs.

Rule:

Secret keys must never be embedded in frontend code.


2. Weak Secret Keys

Examples:

1
2
3
4
5
6
7
password

secret

admin

123456

These can be brute-forced using tools like Hashcat or John the Ripper if attackers obtain a valid token and know the signing algorithm.

Always use long, random, cryptographically secure secrets.


3. Accepting “alg”:”none”

Older JWT libraries sometimes accepted:

1
2
3
{
  "alg":"none"
}

meaning:

“No signature.”

If the server trusted such tokens, anyone could forge arbitrary JWTs.

Modern libraries reject this by default, but understanding the historical vulnerability is important for CTFs.


4. Trusting User-Controlled Claims

Never assume:

1
2
3
{
  "is_admin": true
}

is safe simply because it’s inside a JWT.

Only the server should decide what claims to include.

Authorization decisions should be made carefully, ideally using trusted server-side data or verified claims generated exclusively by the server.


5. Long-Lived Tokens

Tokens without expiration can remain valid indefinitely if stolen.

Always include:

1
exp

and keep access tokens short-lived.


How JWT Verification Works

Every request follows this process:

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
Receive JWT

↓

Split into three parts

↓

Decode Header

↓

Decode Payload

↓

Recalculate Signature

↓

Compare Signatures

↓

Valid?

↓

Yes → User Authenticated

No → Reject

Everything depends on the server protecting its signing key.


Recognizing JWT Vulnerabilities During Security Assessments

When testing an application, you should examine how JWTs are used rather than assuming they are secure.

Questions to ask include:

  • Is the JWT stored in a cookie or Authorization header?
  • What algorithm is being used?
  • Does the payload contain sensitive information?
  • Is the payload trusted for authorization?
  • Does the token contain an expiration time?
  • Can the token be modified?
  • Is the signing key accidentally exposed?
  • Is the application accepting unexpected algorithms?

These observations often reveal implementation weaknesses.


How to Practice JWT Security

If you’re learning penetration testing or preparing for CTFs, build small applications that implement JWT authentication correctly, then intentionally introduce one vulnerability at a time. For example:

  • Expose the secret key in client-side code.
  • Use an extremely weak signing key.
  • Remove expiration claims.
  • Add an is_admin claim and see how authorization changes.
  • Experiment with different signing algorithms.

Understanding both the secure and insecure versions is one of the fastest ways to develop intuition.


Best Practices for Developers

A secure JWT implementation should follow these principles:

  • Keep signing keys on the server only.
  • Use strong, randomly generated secrets.
  • Always verify signatures before trusting claims.
  • Set appropriate expiration (exp) times.
  • Validate issuer (iss) and audience (aud) where appropriate.
  • Rotate signing keys when necessary.
  • Avoid storing sensitive information in the payload.
  • Use HTTPS so tokens cannot be intercepted in transit.
  • Carefully separate authentication from authorization logic.

Key Takeaways

JWTs are neither inherently secure nor inherently insecure—they are simply a mechanism for representing claims.

Their security depends entirely on how they are implemented.

Remember these core principles:

  • A JWT consists of a Header, Payload, and Signature.
  • The payload is encoded, not encrypted.
  • The signature prevents unauthorized modification.
  • The signing key must remain secret.
  • Authentication identifies a user; authorization determines permissions.
  • A leaked signing key allows attackers to forge valid tokens.
  • Always think critically about what claims an application trusts and why.

Whether you’re solving CTF challenges, conducting penetration tests, or building production web applications, understanding these concepts will help you recognize dangerous implementations and design secure ones.

Security is rarely about memorizing vulnerabilities—it is about understanding the assumptions a system makes and verifying whether those assumptions can be broken.

This post is licensed under CC BY 4.0 by the author.