Skip to main content

Command Palette

Search for a command to run...

JWT Tokens Explained: Structure, Security, and Best Practices

Published
12 min readView as Markdown

return (

/ Schema.org JSON-LD /

/ Breadcrumb Navigation /

Home / Blog / JWT Tokens Explained

/ Article Header /

# JWT Tokens Explained: Structure, Security, and Best Practices

Security March 7, 2026 15 min read

/ Article Content /

What Is a JWT?

A JSON Web Token (JWT, pronounced "jot") is a compact, URL-safe token format used to securely transmit claims between two parties. JWTs are the backbone of modern authentication: when you log into an application and subsequent requests are authenticated without hitting the database, a JWT is almost certainly involved.

Unlike session-based authentication where the server stores session data, JWTs are stateless. All the information needed to verify the user is contained within the token itself. The server signs the token with a secret key, and any service that knows the key (or has the public key) can verify its authenticity without network calls.

`// A JWT looks like this (three Base64-encoded parts separated by dots): eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ. SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

// Three parts: // 1. Header → algorithm and token type // 2. Payload → claims (user data, expiration, etc.) // 3. Signature → cryptographic proof of authenticity`

To decode and inspect any JWT instantly, paste it into our JWT Decoder. It splits the token into its three parts, decodes the header and payload, and shows the signature -- all in your browser without sending the token to any server.

The Three Parts of a JWT

1. Header

The header is a JSON object that specifies the signing algorithm and the token type. It is Base64Url-encoded to form the first part of the JWT.

` "alg": "HS256", // signing algorithm "typ": "JWT" // token type

// Base64Url-encoded: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

// Common algorithms: // HS256 — HMAC with SHA-256 (symmetric, shared secret) // RS256 — RSA with SHA-256 (asymmetric, public/private key) // ES256 — ECDSA with P-256 and SHA-256 (asymmetric, smaller keys) // EdDSA — Edwards-curve DSA (asymmetric, modern, fast)`

2. Payload (Claims)

The payload contains claims -- statements about the user and metadata. Claims are categorized into three types: registered (standard), public (community-defined), and private (application-specific).

` // Registered claims (standardized by RFC 7519) "iss": "https://auth.example.com", // issuer "sub": "user-12345", // subject (user ID) "aud": "https://api.example.com", // audience "exp": 1709856000, // expiration time (Unix timestamp) "nbf": 1709852400, // not before (token not valid before this) "iat": 1709852400, // issued at "jti": "a1b2c3d4-e5f6-7890", // JWT ID (unique identifier)

// Private claims (application-specific) "role": "admin", "permissions": ["read", "write", "delete"], "email": "user@example.com" `

Claim Full Name Purpose

issIssuerWho created and signed the token subSubjectWho the token is about (usually user ID) audAudienceWho the token is intended for expExpirationWhen the token expires (Unix timestamp) nbfNot BeforeToken is invalid before this time iatIssued AtWhen the token was created jtiJWT IDUnique identifier (prevents replay attacks)

3. Signature

The signature ensures the token has not been tampered with. It is created by encoding the header and payload, then signing them with the algorithm specified in the header.

`// HS256 signature formula: HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret )

// RS256 signature formula: RSASHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), privateKey )

// The signature is what makes JWTs tamper-proof: // If anyone changes the header or payload, the signature won't match // If anyone tries to re-sign, they don't have the secret/private key`

The header and payload are Base64Url-encoded, not encrypted. Anyone can decode them -- the signature only proves authenticity, not confidentiality. To understand Base64 encoding in depth, read our Base64 Encoding Explained guide, or use our Base64 Encoder/Decoder tool.

Signing Algorithms: HS256 vs RS256 vs ES256

Algorithm Type Key Best For

HS256SymmetricShared secret (256 bits)Monoliths, single-server apps RS256AsymmetricRSA key pair (2048+ bits)Microservices, third-party verification ES256AsymmetricECDSA P-256 key pairMobile apps, performance-sensitive EdDSAAsymmetricEd25519 key pairModern systems, fastest asymmetric

`// HS256: Same secret for signing AND verification // ✓ Simple setup // ✓ Fast // ✗ Secret must be shared with every verifier // ✗ If one service is compromised, all are

// RS256: Private key signs, public key verifies // ✓ Only auth server has private key // ✓ Any service can verify with public key // ✓ Public key can be freely distributed (JWKS endpoint) // ✗ Slower than HS256 // ✗ Larger key size

// ES256: Like RS256 but with smaller keys and faster signing // ✓ 256-bit key ≈ 3072-bit RSA security // ✓ Smaller tokens // ✓ Faster than RS256 // ✗ Slightly less library support than RS256`

HS256 uses HMAC-SHA256 under the hood. To understand how HMAC works, see the HMAC section in our Hash Functions Explained guide. For generating secure secrets, use our Password Generator with maximum length and all character types enabled.

Creating and Verifying JWTs

Node.js with jsonwebtoken

`// npm install jsonwebtoken const jwt = require('jsonwebtoken');

const SECRET = process.env.JWT_SECRET; // at least 256 bits (32+ chars)

// Create a token function createToken(user) return jwt.sign(

sub: user.id, email: user.email, role: user.role, , SECRET,

expiresIn: '15m', // 15 minutes issuer: 'https://api.example.com', audience: 'https://app.example.com',

);

// Verify a token function verifyToken(token) try const decoded = jwt.verify(token, SECRET, issuer: 'https://api.example.com', audience: 'https://app.example.com', algorithms: ['HS256'], // ALWAYS specify allowed algorithms ); return valid: true, payload: decoded ; catch (error) return valid: false, error: error.message ;

// Express middleware function authMiddleware(req, res, next) const authHeader = req.headers.authorization; if (!authHeader?.startsWith('Bearer ')) return res.status(401).json( error: 'Missing token' );

const token = authHeader.split(' ')[1]; const result = verifyToken(token);

if (!result.valid) return res.status(401).json( error: 'Invalid token' );

req.user = result.payload; next(); `

Python with PyJWT

`# pip install pyjwt

from datetime import datetime, timedelta, timezone

SECRET = "your-256-bit-secret"

Create a token

def create_token(user_id: str, role: str) -> str: payload = "sub": user_id, "role": role, "iat": datetime.now(timezone.utc), "exp": datetime.now(timezone.utc) + timedelta(minutes=15), "iss": "https://api.example.com",

return jwt.encode(payload, SECRET, algorithm="HS256")

Verify a token

def verify_token(token: str) -> dict: try: return jwt.decode( token, SECRET, algorithms=["HS256"], # ALWAYS specify algorithms issuer="https://api.example.com", ) except jwt.ExpiredSignatureError: raise ValueError("Token has expired") except jwt.InvalidTokenError as e: raise ValueError(f"Invalid token: e")

FastAPI dependency

from fastapi import Depends, HTTPException, Header

async def get_current_user(authorization: str = Header(...)): if not authorization.startswith("Bearer "): raise HTTPException(status_code=401, detail="Invalid auth header") token = authorization.split(" ")[1] try: return verify_token(token) except ValueError as e: raise HTTPException(status_code=401, detail=str(e))`

RS256 with Public/Private Keys

`// Generate RSA key pair: // openssl genrsa -out private.pem 2048 // openssl rsa -in private.pem -pubout -out public.pem

const fs = require('fs'); const jwt = require('jsonwebtoken');

const PRIVATE_KEY = fs.readFileSync('private.pem'); const PUBLIC_KEY = fs.readFileSync('public.pem');

// Auth server: sign with PRIVATE key const token = jwt.sign( sub: 'user-123' , PRIVATE_KEY, algorithm: 'RS256', expiresIn: '15m', );

// Any service: verify with PUBLIC key (no secret needed) const decoded = jwt.verify(token, PUBLIC_KEY, algorithms: ['RS256'], );

// JWKS endpoint (JSON Web Key Set) // Most auth providers expose their public keys at: // https://auth.example.com/.well-known/jwks.json // Services fetch the public key from this endpoint to verify tokens`

The JWT Authentication Flow

`// 1. User logs in POST /auth/login Body: "email": "user@example.com", "password": "..."

// 2. Server validates credentials, creates tokens Response: "accessToken": "eyJhbGci...", // short-lived (15 min) "refreshToken": "eyJhbGci...", // long-lived (7-30 days) "expiresIn": 900 // seconds until access token expires

// refreshToken is also set as HttpOnly cookie

// 3. Client stores access token in memory (NOT localStorage) let accessToken = response.accessToken;

// 4. Client sends access token with every API request GET /api/users Authorization: Bearer eyJhbGci...

// 5. When access token expires (401 response): POST /auth/refresh Cookie: refreshToken=eyJhbGci... // sent automatically

// 6. Server validates refresh token, issues new access token Response: "accessToken": "eyJhbGci...", // new token "expiresIn": 900

// 7. If refresh token is expired or invalid: redirect to login`

Security Best Practices

  • Always specify allowed algorithms -- The "none" algorithm attack exploits libraries that accept the algorithm from the token header. Always pass algorithms: ['HS256'] (or your chosen algorithm) when verifying.

  • Use short expiration for access tokens -- Access tokens should expire in 5-15 minutes. If a token is stolen, the window of vulnerability is limited.

  • Store tokens securely -- Access tokens in memory (JavaScript variable). Refresh tokens in HttpOnly, Secure, SameSite=Strict cookies. Never in localStorage or sessionStorage.

  • Validate all registered claims -- Check exp (expiration), iss (issuer), and aud (audience) on every verification. Do not trust a token just because the signature is valid.

  • Implement token rotation for refresh tokens -- Issue a new refresh token every time one is used. If an old refresh token is replayed, invalidate all tokens for that user (indicating theft).

  • Never put sensitive data in the payload -- JWTs are encoded, not encrypted. Anyone can decode the payload. Do not include passwords, SSNs, or API keys.

  • Use strong secrets -- For HS256, use at least 256 bits (32 characters) of cryptographically random data. A weak secret can be brute-forced.

  • Consider token size -- Every claim adds to the token size, and tokens are sent with every request. Keep payloads minimal. If you need extensive permissions data, fetch it from the server after verifying the token.

Common JWT Attacks and Defenses

Attack How It Works Defense

Algorithm: noneAttacker sets alg to "none", removes signatureAlways specify allowed algorithms in verify() Algorithm confusionSwitches RS256 to HS256, uses public key as HMAC secretSpecify algorithms; use separate verification paths for symmetric/asymmetric Token theft (XSS)JavaScript reads token from localStorageStore tokens in memory or HttpOnly cookies, implement CSP headers Brute-force secretWeak HMAC secret is guessableUse 256+ bit cryptographically random secrets Replay attackStolen token is reused before expirationShort expiration (5-15 min), jti claim for critical operations Refresh token theftLong-lived refresh token is stolenToken rotation, reuse detection, HttpOnly cookies

The "alg: none" Attack in Detail

`// VULNERABLE code — DO NOT do this: const decoded = jwt.verify(token, secret); // This reads the algorithm from the token header! // If the attacker sets "alg": "none", no verification happens

// SAFE code — always specify algorithms: const decoded = jwt.verify(token, secret, algorithms: ['HS256'] // only accept HS256 );

// If the token has "alg": "none" or "alg": "RS256", // verification will fail immediately`

Token Revocation Strategies

One of the biggest limitations of JWTs is that they cannot be revoked once issued (they are stateless). If a user logs out or their account is compromised, the access token remains valid until it expires. Here are strategies to address this.

Strategy How It Works Trade-off

Short expirationAccess tokens expire in 5-15 minutesRequires refresh flow; small revocation delay Token blocklistStore revoked token JTIs in RedisAdds a database check (partially defeats stateless benefit) Token versionStore version in user record; token has version claimRequires DB lookup; changing version invalidates all tokens Refresh token rotationNew refresh token per use; old one invalidatedReuse detection catches theft; slightly more complex

`// Token blocklist with Redis (Node.js) const Redis = require('ioredis'); const redis = new Redis();

// On logout: add token to blocklist async function revokeToken(token) const decoded = jwt.decode(token); const ttl = decoded.exp - Math.floor(Date.now() / 1000); if (ttl > 0) await redis.setex(`blocklist:$decoded.jti`, ttl, '1');

// On every request: check blocklist async function isTokenRevoked(token) const decoded = jwt.decode(token); const revoked = await redis.get(`blocklist:$decoded.jti`); return !!revoked;

// Middleware async function authMiddleware(req, res, next) const token = req.headers.authorization?.split(' ')[1]; if (!token) return res.status(401).json( error: 'Missing token' );

if (await isTokenRevoked(token)) return res.status(401).json( error: 'Token has been revoked' );

try req.user = jwt.verify(token, SECRET, algorithms: ['HS256'] ); next(); catch (err) return res.status(401).json( error: 'Invalid token' );

`

JWT vs Session Cookies: When to Use Each

Factor JWT Session Cookie

StateStateless (data in token)Stateful (data on server) ScalabilityNo shared state neededRequires shared session store RevocationDifficult (needs blocklist)Easy (delete from store) Cross-domainWorks via Authorization headerLimited by cookie domain rules Mobile appsNative supportRequires cookie handling Best forAPIs, microservices, mobileTraditional web apps, SSR

For a deeper comparison with code examples and architectural guidance, read our dedicated JWT vs Session Cookies article. For understanding the HTTP layer that carries these tokens, see our HTTP Status Codes Guide.

When NOT to Use JWTs

JWTs are not always the right choice. Here are situations where session-based authentication or other approaches work better.

  • When immediate revocation is critical -- Banking, healthcare, and other high-security applications need the ability to instantly invalidate sessions. Session cookies do this naturally; JWTs require a blocklist.
  • When you only have one server -- The stateless benefit of JWTs does not matter if you have a single server. Session cookies are simpler to implement and more secure by default.
  • When token size matters -- JWTs can be 500+ bytes. If they are sent in cookies, you may hit the 4KB cookie size limit. Session IDs are typically 32-64 bytes.
  • When you need to store lots of user data -- Putting extensive user data in the JWT payload makes it large. Store the data server-side and use the token only for identity.

Frequently Asked Questions

Where should I store JWT tokens on the client?

Store access tokens in memory (a JavaScript variable) and refresh tokens in HttpOnly, Secure, SameSite cookies. Never store tokens in localStorage or sessionStorage -- they are accessible to any JavaScript on the page, making them vulnerable to XSS attacks. HttpOnly cookies cannot be read by JavaScript, protecting against token theft. If you must use localStorage (for example, in mobile webviews), keep access tokens short-lived (5-15 minutes) and implement refresh token rotation.

What is the difference between HS256 and RS256?

HS256 (HMAC-SHA256) uses a single shared secret for both signing and verification. It is simpler and faster, but every service that verifies tokens needs the same secret. RS256 (RSA-SHA256) uses a key pair: a private key for signing and a public key for verification. Only the auth server needs the private key; any service can verify tokens using the public key. Use HS256 for monolithic applications. Use RS256 for microservices where multiple services verify tokens independently.

How do I handle JWT token expiration and refresh?

Use a two-token strategy: a short-lived access token (5-15 minutes) and a long-lived refresh token (7-30 days). When the access token expires, send the refresh token to a dedicated endpoint to get a new access token without requiring re-login. Implement token rotation -- issue a new refresh token each time one is used, and invalidate the old one. If a refresh token is used twice (indicating theft), invalidate all tokens for that user.

/ CTA Section /

Decode and Inspect JWT Tokens

Paste any JWT into our free decoder to see the header, payload, and signature. Verify the structure, check claims, and debug authentication issues -- all in your browser.

Open JWT Decoder

/ Related Articles /

[

JWT Tokens Deep Dive

Extended reference covering JWKS endpoints, token introspection, and OAuth2 flows.

](/blog/jwt-tokens-guide/) [

JWT vs Session Cookies

Compare stateful and stateless authentication methods for web and mobile.

](/blog/jwt-vs-session-cookies-authentication/) [

Hash Functions Explained

MD5, SHA-256, bcrypt -- how the HMAC behind HS256 actually works.

](/blog/hash-functions-explained/) [

Base64 Encoding Explained

How JWT header and payload are encoded and why Base64Url differs from Base64.

](/blog/base64-encoding-explained/)

)


Originally published on bytepane.com. Visit for free interactive calculators and tools.

More from this blog

R

Brazora

91 posts