Understand JSON Web Tokens (JWT) — how they work, their structure, common use cases, security best practices, and how to decode them with free online tools.
If you have ever logged into a web application, called a protected API, or implemented single sign-on, you have almost certainly encountered a JWT token — whether you realized it or not. JSON Web Tokens are one of the most widely used authentication mechanisms on the modern web, and understanding how they work is essential for any developer building secure applications.
Yet most developers treat JWTs as black boxes. They copy-paste a library, call jwt.sign(), and move on. That works until something breaks — until a token expires and users get locked out, until you find yourself debugging a 401 error at 2 AM, or until a security audit reveals that your tokens are leaking sensitive data in plaintext.
This guide explains everything you need to know about JWTs. What they are, how they are structured, how to decode them, when to use them, and what security pitfalls to avoid.
A JSON Web Token (JWT) is an open standard (RFC 7519) for securely transmitting information between parties as a compact, URL-safe JSON object. The information in a JWT can be verified and trusted because it is digitally signed.
JWTs can be signed using a secret key (with the HMAC algorithm) or a public/private key pair (using RSA or ECDSA). When a token is signed, the recipient can verify that the content has not been tampered with. When a token is encrypted, the content is also hidden from third parties.
The most common use case is authentication. After a user logs in, the server issues a JWT. The client stores this token (typically in memory or an HTTP-only cookie) and sends it with every subsequent request. The server validates the token's signature and extracts the user's identity without needing to query a database or maintain server-side session state.
This is fundamentally different from traditional session-based authentication, where the server stores session data and the client holds only a session ID. With JWTs, the token itself carries the information.
Every JWT consists of three parts, separated by dots:
xxxxx.yyyyy.zzzzz
Those three parts are the header, the payload, and the signature. Each part is Base64URL-encoded, which means you can decode them without any special tools — just a Base64 Encoder/Decoder will get you most of the way there, though a dedicated JWT Decoder handles the URL-safe variant and displays the result in a structured format.
Here is a real example of what a JWT looks like:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
That looks intimidating, but it is just three JSON objects encoded in Base64URL and concatenated with dots. Let us break each part down.
The header typically contains two fields: the signing algorithm (alg) and the token type (typ).
{
"alg": "HS256",
"typ": "JWT"
}This tells the recipient that the token is a JWT and was signed using HMAC-SHA256. Other common algorithms include RS256 (RSA with SHA-256), ES256 (ECDSA with SHA-256), and EdDSA (Edwards-curve Digital Signature Algorithm).
The header is Base64URL-encoded to produce the first part of the token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
The payload contains the claims — statements about the user and additional metadata. This is where the actual data lives.
{
"sub": "1234567890",
"name": "John Doe",
"email": "john@example.com",
"role": "admin",
"iat": 1516239022,
"exp": 1516242622
}Claims fall into three categories:
Registered claims are predefined by the JWT specification. They are not mandatory but are recommended for interoperability:
iss (issuer) — who created the tokensub (subject) — who the token is about (usually a user ID)aud (audience) — who the token is intended forexp (expiration time) — when the token expires (Unix timestamp)nbf (not before) — the token is not valid before this timeiat (issued at) — when the token was createdjti (JWT ID) — a unique identifier for the tokenPublic claims are defined by the developer but should use collision-resistant names (often URIs) to avoid conflicts.
Private claims are custom claims agreed upon between parties, like role, permissions, or tenant_id.
Here is the critical thing to understand: the payload is not encrypted. It is only encoded. Anyone who has the token can decode the payload and read its contents. Never put sensitive information — passwords, credit card numbers, social security numbers — in a JWT payload.
The signature is what makes JWTs trustworthy. It is created by taking the encoded header, the encoded payload, and a secret key, then applying the algorithm specified in the header.
For HMAC-SHA256, the signature is computed like this:
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
The signature serves two purposes. First, it verifies that the token was issued by someone who knows the secret key. Second, it ensures the token has not been modified. If anyone changes a single character in the header or payload, the signature will not match, and the token will be rejected.
For asymmetric algorithms like RS256, the server signs the token with a private key and anyone can verify it with the corresponding public key. This is particularly useful in microservice architectures where multiple services need to verify tokens but only one service (the auth server) should be able to create them.
Here is the typical flow when JWTs are used for authentication:
Step 1: The user sends their credentials (username and password) to the authentication endpoint.
Step 2: The server verifies the credentials. If valid, it creates a JWT containing the user's ID, roles, and an expiration time, then signs it with a secret key.
Step 3: The server sends the JWT back to the client.
Step 4: The client stores the token and includes it in the Authorization header of subsequent requests:
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Step 5: For each request, the server extracts the token from the header, verifies the signature, checks the expiration time, and extracts the user's identity from the payload.
You can test this entire flow using an API Tester to see how authorization headers work with real endpoints.
Decoding a JWT is straightforward because the payload is simply Base64URL-encoded, not encrypted. There are several ways to do it.
The fastest way is to paste your token into a JWT Decoder. It will instantly split the token into its three parts, decode the header and payload, show the claims in a readable format, and display the expiration status.
function decodeJWT(token) {
const parts = token.split(".");
if (parts.length !== 3) {
throw new Error("Invalid JWT format");
}
const header = JSON.parse(atob(parts[0].replace(/-/g, "+").replace(/_/g, "/")));
const payload = JSON.parse(atob(parts[1].replace(/-/g, "+").replace(/_/g, "/")));
return { header, payload, signature: parts[2] };
}import base64
import json
def decode_jwt(token):
parts = token.split('.')
# Add padding if needed
payload = parts[1] + '=' * (4 - len(parts[1]) % 4)
decoded = base64.urlsafe_b64decode(payload)
return json.loads(decoded)echo 'eyJzdWIiOiIxMjM0NTY3...' | \
tr '_-' '/+' | \
base64 -d 2>/dev/null | \
python3 -m json.toolRemember: decoding is not the same as verifying. Anyone can decode a JWT. Only someone with the secret key can verify that it is authentic.
JWTs are the standard for securing REST APIs. The client obtains a token after authenticating and includes it in every API request. The server validates the token without maintaining session state, making this approach highly scalable.
JWTs enable SSO across multiple applications. A user logs in once with a central identity provider, receives a JWT, and can use that token to access any application that trusts the same issuer. This is how protocols like OAuth 2.0 and OpenID Connect work under the hood.
In a microservice architecture, services need to verify the identity of callers. JWTs work well here because each service can independently verify a token using the public key, without making a network call to the auth service.
JWTs can be used to securely transmit information between parties. Because they can be signed, the recipient knows the sender is who they claim to be, and that the content has not been changed in transit.
JWTs are powerful but can be dangerous if misused. Follow these practices to avoid common pitfalls.
Set the exp claim to a short duration — 15 minutes for access tokens is a common choice. Use refresh tokens (stored securely, typically in HTTP-only cookies) to obtain new access tokens without requiring the user to log in again.
{
"sub": "user123",
"iat": 1711500000,
"exp": 1711500900
}That token expires after 15 minutes. If it is stolen, the window of exploitation is limited.
The payload is Base64URL-encoded, not encrypted. Anyone who intercepts the token can read its contents. Include only the minimum information needed — a user ID and role, not personal data.
For HMAC-based algorithms, use a key that is at least 256 bits long and generated with a cryptographically secure random number generator. Never use simple strings like "secret" or "my-jwt-key". You can use a Hash Generator to create strong, random keys.
Never skip signature verification. Some libraries have options to decode a token without verifying it — these should only be used for debugging, never in production.
Check not just the signature but also the exp, iss, aud, and nbf claims. A valid signature on an expired token should still result in rejection.
JWTs transmitted over plain HTTP can be intercepted. Always use HTTPS in production. This is non-negotiable.
Prefer RS256 or ES256 over HS256 in production environments where multiple services need to verify tokens. With asymmetric algorithms, only the auth server needs the private key. If using HS256, the same secret must be shared with every service that verifies tokens, increasing the risk of key compromise.
alg: none Attack#Some JWT libraries support an alg: none option, which means the token has no signature. An attacker can modify the payload, set the algorithm to none, remove the signature, and send the token to a server that does not properly validate the algorithm. Always reject tokens with alg: none in production.
If a server is configured to accept RS256 (asymmetric), an attacker might change the algorithm to HS256 (symmetric) and sign the token with the public key (which is often publicly available). If the server does not enforce the expected algorithm, it might accept the forged token. Always enforce the algorithm on the server side and do not let the token dictate which algorithm to use.
If JWTs are stored in localStorage, they are vulnerable to cross-site scripting (XSS) attacks. A malicious script can read the token and send it to an attacker. Use HTTP-only cookies with the Secure, SameSite, and HttpOnly flags set.
Tokens without an exp claim are valid forever. If one is stolen, it grants permanent access. Always set an expiration time, and implement token revocation for critical scenarios (user logout, password change, compromised accounts).
Every claim you add increases the token size, and the token is sent with every request. Large tokens increase bandwidth usage and can even exceed header size limits on some servers. Keep your payload minimal.
JWTs are not always the right choice. Here is a quick comparison:
Choose JWTs when:
Choose sessions when:
Many production systems use a hybrid approach: JWTs for short-lived access tokens combined with server-side refresh token storage for revocation capability.
A well-designed JWT system uses two types of tokens:
Access tokens are short-lived (5 to 15 minutes) and contain the user's identity and permissions. They are sent with every API request.
Refresh tokens are long-lived (days or weeks) and stored securely on the server side. When an access token expires, the client uses the refresh token to obtain a new access token without requiring the user to enter credentials again.
Client Server
| |
|-- Login (credentials) ->|
| |-- Verify credentials
|<-- Access + Refresh ---|
| |
|-- API call + Access -->|
| |-- Verify token
|<-- Response ---|
| |
|-- Access expired -->|
|<-- 401 Unauthorized ---|
| |
|-- Refresh token -->|
| |-- Verify refresh token
|<-- New Access token ---|
This pattern gives you the performance benefits of stateless JWTs (no database lookup on every request) with the security benefit of being able to revoke refresh tokens when needed.
JWTs are a foundational technology for modern web authentication. They are compact, self-contained, and eliminate the need for server-side session storage. But they come with responsibilities — you need to choose the right algorithm, set proper expiration times, validate all claims, and never store sensitive data in the payload.
When you need to inspect or debug a token, use a JWT Decoder to instantly see its contents, verify its structure, and check its expiration status. Understanding what is inside your tokens is the first step toward building secure authentication systems.