← Blog

Understanding JWT Tokens: The Core of Web Authentication

January 22, 2026 · 9 min read

"How does staying logged in work?" That was my question when I first learned web development. HTTP is stateless—how does the server remember me?

First, Understanding Sessions

To understand JWT, you need to know the traditional session approach.

  1. User logs in
  2. Server creates a session ID and stores it in memory/database
  3. Session ID sent to browser as a cookie
  4. Cookie automatically sent with every request
  5. Server looks up user info by session ID

The problem: The server must remember all sessions. Multiple servers? You need session synchronization. That's complicated.

JWT: The Server Doesn't Need to Remember

JWT (JSON Web Token) takes a different approach. Instead of the server storing user info, it puts the information in the token itself and gives it to the client.

The token includes the server's signature, so later when it receives the token, it can verify "yep, I made this."

JWT Structure: Three Parts Separated by Dots

A JWT looks like this:

xxxxx.yyyyy.zzzzz

Header.Payload.Signature

eyJhbGciOiJIUzI1NiIs… . eyJzdWIiOiIxMjM0NTY3… . SflKxwRJSMeKKF2QT4fw… Header { "alg": "HS256", "typ": "JWT" } Anyone can decode it Payload { "sub": "1234567890", "name": "John Doe", "iat": 1516239022 } Not encrypted — no secrets here Signature HMACSHA256( base64Url(header) + "." + base64Url(payload), secret ) Cannot be forged without the secret
A JWT is three Base64Url-encoded parts — header, payload, and signature — joined by dots.

1. Header

Token type and signing algorithm. Base64Url encoded.

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

2. Payload

The actual data. Called "claims."

{

"sub": "1234567890",

"name": "John Doe",

"admin": true,

"exp": 1706234567

}

⚠️ Important: The payload is NOT encrypted! Anyone can decode it. Don't put sensitive data here.

3. Signature

Created by combining header + payload + secret key.

HMACSHA256(

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

secret

)

This signature lets the server detect if the token was tampered with.

JWT Authentication Flow

  1. User logs in
  2. Server creates and returns a JWT
  3. Client stores JWT (usually localStorage or httpOnly cookie)
  4. Subsequent requests include Authorization: Bearer {token} header
  5. Server verifies signature and uses payload info
1 2 3 Server: issue token Signs header + payload with the secret key → JWT created Client Stores it, sends it back: Authorization: Bearer eyJhbGci… Server: verify Re-computes the signature from header + payload, compares with the token's Match → valid ✓ Mismatch → reject ✗ Changing even one character of the payload changes the signature.
Signature verification — the server stores nothing; it simply re-computes the signature and compares.

Sessions vs JWT

Sessions JWT
Storage Server Client
Scalability Needs session sync No server sharing needed
Logout Delete session, done Hard to invalidate token
Security Server-managed Token theft is dangerous

JWT Security Considerations

1. Expiration (exp) is a must

Set an expiration time. Permanent tokens are dangerous. Usually 15 minutes to 1 hour.

2. Use Refresh Tokens

Short-lived access tokens with refresh token rotation is safer.

3. HTTPS is mandatory

Tokens are exposed in plain text over HTTP.

4. localStorage vs httpOnly cookies

localStorage is vulnerable to XSS; cookies to CSRF. httpOnly cookie + CSRF token is safest.

When to Use JWT

  • Microservices architecture (shared auth between services)
  • Mobile apps (instead of cookies)
  • Single Sign-On (SSO)
  • API authentication

For simple websites, sessions might be simpler. Choose the right tool for the job.

JWT Claims in Detail

The payload section of a JWT contains claims, which are statements about the user and metadata about the token. The JWT specification (RFC 7519) defines a set of registered claims that are not mandatory but strongly recommended.

  • iss (Issuer): Who created and signed the token. Typically a URL identifying your auth server, like "https://auth.example.com".
  • sub (Subject): The user or entity the token represents. Usually a user ID like "user-12345".
  • aud (Audience): The intended recipient of the token. An API server can reject tokens where the audience claim does not match its own identifier.
  • exp (Expiration Time): A Unix timestamp after which the token is no longer valid. The server must reject expired tokens.
  • iat (Issued At): When the token was created. Useful for determining token age.
  • nbf (Not Before): The token should not be accepted before this timestamp. Rarely used but available for scheduled access.
  • jti (JWT ID): A unique identifier for the token, useful for preventing replay attacks by tracking which tokens have been used.

Beyond registered claims, you can add custom claims for your application. Common examples include user roles, permissions, or tenant IDs in multi-tenant systems. Keep the payload lean, though. Every byte in the JWT is sent with every request, and large tokens waste bandwidth and slow down parsing.

Signing Algorithms: HS256 vs RS256

The signing algorithm determines how the signature is created and verified. The two most common choices are HS256 and RS256, and they work quite differently.

HS256 (HMAC + SHA-256)

This is a symmetric algorithm. The same secret key is used to both sign and verify the token. It is simple to implement and fast to compute. The drawback is that every service that needs to verify tokens must have access to the secret key. If you have a single backend server, this works well. If you have multiple services, you need to distribute the secret securely, which introduces risk.

// HS256: same key signs and verifies
const token = jwt.sign(payload, "my-shared-secret", { algorithm: "HS256" });
const decoded = jwt.verify(token, "my-shared-secret");

RS256 (RSA + SHA-256)

This is an asymmetric algorithm using a public/private key pair. The auth server signs tokens with its private key. Any service can verify tokens using the public key, which can be distributed freely without security risk. This is the standard choice for microservices architectures and third-party integrations. OIDC (OpenID Connect) providers like Google and Auth0 use RS256 by default.

// RS256: private key signs, public key verifies
const token = jwt.sign(payload, privateKey, { algorithm: "RS256" });
const decoded = jwt.verify(token, publicKey);

Other algorithms exist too. ES256 uses elliptic curve cryptography and produces shorter signatures than RS256. PS256 uses RSA-PSS padding. The none algorithm means no signature at all, and accepting it is a well-known security vulnerability that has led to real-world breaches. Always validate the algorithm in the header and reject none.

The Complete Authentication Flow

Let's walk through a complete JWT authentication cycle, step by step, to see how all the pieces fit together.

  1. Login: The user sends their username and password to the auth endpoint (POST /auth/login).
  2. Validation: The server checks the credentials against the database. If they match, it creates a JWT containing the user's ID, role, and an expiration time.
  3. Token delivery: The server returns the JWT (and optionally a refresh token) to the client. The client stores it, typically in an httpOnly cookie or memory.
  4. Authenticated requests: On every subsequent API call, the client includes the token in the Authorization: Bearer <token> header.
  5. Verification: The server extracts the token, verifies the signature using its secret or public key, checks the expiration, and reads the claims. No database lookup is needed for basic auth checks.
  6. Token refresh: When the access token expires, the client sends the refresh token to a dedicated endpoint (POST /auth/refresh). The server issues a new access token without requiring the user to log in again.

Access Tokens vs Refresh Tokens

A common pattern splits authentication into two tokens with different lifetimes and purposes.

Access Token Refresh Token
Lifetime Short (15 min - 1 hour) Long (7 - 30 days)
Purpose Authorize API requests Obtain new access tokens
Storage Memory or httpOnly cookie httpOnly cookie (secure)
Sent to API servers Auth server only
If stolen Limited damage (short-lived) Attacker can get new access tokens

Refresh token rotation adds another layer of safety. Each time a refresh token is used, the server issues a new refresh token and invalidates the old one. If an attacker steals and uses a refresh token that the legitimate user has already rotated, the server detects the reuse and revokes the entire token family, forcing a fresh login.

Security Best Practices

JWT security is not just about the token itself. It is about how you handle it throughout its lifecycle.

  • Always validate the signature. Never decode a JWT and trust the payload without verifying the signature first. Libraries like jsonwebtoken (Node.js) do this automatically when you use verify() instead of decode().
  • Check the algorithm. Pin the expected algorithm on the server side. Do not let the token's header dictate which algorithm to use. An attacker could set the algorithm to none and strip the signature.
  • Validate all relevant claims. Check exp (expiration), iss (issuer), and aud (audience) on every request. A token intended for Service A should not be accepted by Service B.
  • Keep the payload small. Do not store entire user profiles in the token. Include only what is needed for authorization decisions: user ID, roles, and permissions.
  • Use HTTPS everywhere. JWTs transmitted over HTTP can be intercepted by anyone on the network. There is no workaround for this except TLS.
  • Implement token revocation. Despite JWT being stateless, you need a way to invalidate tokens in emergencies (compromised accounts, password changes). A short-lived access token combined with a server-side refresh token blacklist is the most practical approach.
  • Rotate signing keys. Periodically change your signing keys. Use the JWKS (JSON Web Key Set) endpoint to publish current and recent public keys, allowing a graceful transition.

JWT vs Session-Based Auth: A Deeper Look

The choice between JWT and sessions is not simply "new vs old" or "better vs worse." Each approach has fundamental trade-offs that make it better suited to different architectures.

Sessions shine when you need instant revocation. Deleting a session from the server immediately locks out the user. With JWT, the token remains valid until it expires, unless you implement a blacklist, which reintroduces server-side state and partially defeats the purpose of JWT.

JWT shines in distributed systems. When you have 20 microservices behind a load balancer, sharing session state across all of them requires a centralized session store (like Redis). JWT tokens carry their own proof of identity, so any service can verify them independently using just the public key.

A hybrid approach is also common: use JWT for service-to-service communication within your infrastructure, and sessions for user-facing web applications where you need fine-grained control over user state. Many production systems use both, choosing the right tool for each specific boundary.

Frequently Asked Questions

Is JWT encrypted?

JWT is signed but not encrypted by default. Since it uses Base64URL encoding, anyone can read the contents. JWE (JSON Web Encryption) can encrypt the payload, but standard JWT only includes a signature.

How should I set JWT expiration time?

Set the exp (expiration) claim with a Unix timestamp. Access tokens typically expire in 15 minutes to 1 hour, refresh tokens in 7-30 days. Too long risks security, too short hurts usability.