Back to Blog

Understanding JWT Tokens: A Complete Guide

December 5, 2025·12 min read
securityjwtauthentication

Learn how JWT tokens work, when to use them, and common security pitfalls to avoid.

If you've worked on any modern web application, you've probably encountered JWT tokens. They're everywhere: in authentication systems, API calls, and single sign-on flows. But despite their popularity, JWTs are often misunderstood and misused.

I've seen teams store sensitive data in JWTs thinking it's encrypted (it's not), skip signature validation because "it's just for development" (famous last words), and use the same secret key across all environments (please don't).

Let's break down what JWTs actually are, how they work under the hood, and the mistakes you'll want to avoid.

What Exactly is a JWT?

A JSON Web Token is essentially a way to transmit information between two parties as a JSON object. What makes it special is that this information can be verified and trusted because it's digitally signed.

Think of it like a sealed envelope with a tamper-evident seal. Anyone can read what's inside (the envelope is transparent), but if someone tries to modify the contents, the seal breaks and you'll know something's wrong.

A JWT looks like this:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNTE2MjM5MDIyfQ.POstGetfAytaZS82wHcjoTyoqhMyxXiWdR7Nn7A29R8

Looks like gibberish, right? But it's actually three distinct parts separated by dots. Let's decode it using our JWT Decoder to see what's inside.

The Three Parts of a JWT

1. Header

The first part is the header, which typically contains two pieces of information:

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

The alg field specifies which algorithm was used to sign the token (more on this later), and typ simply identifies this as a JWT. This part is Base64Url encoded, not encrypted, just encoded.

2. Payload

The second part is where the actual data lives:

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

These key-value pairs are called "claims." Some are standardized (like sub for subject, iat for "issued at", exp for expiration), while others are custom (like role in this example).

Important: This payload is also just Base64Url encoded. Anyone who intercepts this token can decode it and read the contents. This is why you should never put sensitive information like passwords or credit card numbers in a JWT.

3. Signature

The third part is the magic sauce: the signature. It's created by taking the encoded header, the encoded payload, a secret key, and running them through the signing algorithm:

HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secret
)

This signature is what allows the receiving party to verify that the token hasn't been tampered with. If someone changes even a single character in the header or payload, the signature won't match, and the token will be rejected.

How JWT Authentication Actually Works

Let's walk through a real-world example. Say you're building an API for a task management app:

Step 1: User logs in

The user sends their email and password to your /login endpoint. Your server validates these credentials against the database.

Step 2: Server creates a JWT

If the credentials are valid, your server creates a JWT containing the user's ID and any other relevant info (like their role or permissions). The server signs this token with a secret key that only the server knows.

const token = jwt.sign(
  { userId: user.id, role: user.role },
  process.env.JWT_SECRET,
  { expiresIn: '24h' }
);

Step 3: Client stores the token

The server sends the JWT back to the client. The client stores it, usually in localStorage, sessionStorage, or an HTTP-only cookie (the cookie approach is generally more secure).

Step 4: Client sends token with requests

For every subsequent API request, the client includes the JWT in the Authorization header:

Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

Step 5: Server validates the token

The server extracts the token from the header, verifies the signature using the secret key, checks that it hasn't expired, and then uses the payload data to identify the user.

The beauty of this approach? The server doesn't need to store any session data. All the information needed to authenticate the user is contained within the token itself. This makes JWTs perfect for distributed systems and microservices.

When Should You Use JWTs?

JWTs shine in certain scenarios:

Stateless authentication — If you're running multiple server instances behind a load balancer, you don't need to worry about session synchronization. Any server can validate the token independently.

API authentication — JWTs are easy to include in HTTP headers, making them ideal for REST and GraphQL APIs. They're also language-agnostic: any backend can validate them.

Single Sign-On (SSO). Because JWTs are self-contained, they can be shared across different domains and applications. Log in once, access multiple services.

Short-lived authorization. Need to grant temporary access to a resource? Generate a JWT with a short expiration time. No database cleanup required.

The Security Pitfalls Nobody Warns You About

Now for the part that'll save you from a 3 AM incident response call.

Pitfall #1: Thinking JWTs are encrypted

I can't stress this enough: JWTs are encoded, not encrypted. The payload is completely readable by anyone who gets their hands on the token. Base64 is not encryption; it's just a different way to represent the same data.

Go ahead, paste any JWT into our JWT Decoder. You'll see the contents immediately, no password required.

If you need to transmit sensitive data, either encrypt the payload separately before putting it in the JWT, or better yet, don't put sensitive data in the JWT at all. Just include a user ID and look up the rest from your database.

Pitfall #2: Not validating the signature properly

The whole security model of JWTs depends on signature validation. If you skip this step, an attacker can craft their own token with whatever claims they want.

I've seen code that decodes the payload without verifying the signature, usually with a comment like "we'll add validation later." Later never comes, and suddenly you have a privilege escalation vulnerability.

Always use a proper JWT library that validates signatures. Never roll your own validation logic.

Pitfall #3: Using weak secrets

If you're using HMAC-based algorithms (HS256, HS384, HS512), your secret key needs to be strong. Really strong. We're talking 256+ bits of random data, not secret123 or your company name.

// Bad
const secret = "mysecret";

// Good
const secret = crypto.randomBytes(64).toString('hex');

For production systems, consider using asymmetric algorithms (RS256, ES256) instead. With these, you sign tokens with a private key and verify them with a public key. Even if the public key is compromised, attackers can't forge new tokens.

Pitfall #4: Never expiring tokens

JWTs should have an expiration time. Always. If a token is compromised, you want it to become useless after a short period.

jwt.sign(payload, secret, { expiresIn: '15m' }); // 15 minutes

For longer sessions, use refresh tokens. The access token expires quickly (15 minutes to an hour), and the refresh token is used to get new access tokens. This way, if an access token is stolen, the window of vulnerability is limited.

Pitfall #5: Storing tokens insecurely

Where you store the JWT on the client side matters:

  • localStorage — Vulnerable to XSS attacks. If your site has any XSS vulnerability, attackers can steal tokens.

  • sessionStorage — Same XSS risk, but at least it's cleared when the tab closes.

  • HTTP-only cookies — Can't be accessed by JavaScript, so XSS can't steal them directly. But you need to worry about CSRF attacks instead.

There's no perfect solution, but HTTP-only cookies with proper CSRF protection is generally the safest approach for browser-based apps.

Creating and Decoding JWTs

Want to experiment with JWTs? Use our JWT Creator to generate tokens with custom claims and different signing algorithms. You can see exactly how changing the payload or algorithm affects the final token.

To inspect existing tokens, our JWT Decoder will break down any JWT into its component parts, show you the decoded header and payload, and tell you if the token has expired.

Wrapping Up

JWTs are powerful tools for authentication and authorization, but they're not magic. They work best when you understand their limitations and follow security best practices:

  • Never store sensitive data in the payload
  • Always validate signatures on the server
  • Use strong secrets or asymmetric algorithms
  • Set reasonable expiration times
  • Store tokens securely on the client

Used correctly, JWTs can simplify your authentication architecture and scale beautifully. Used incorrectly, they can open up serious security holes. Now you know the difference.

Tools mentioned in this article:

Related Tools

More Articles