A JWT (JSON Web Token) is a compact, self-contained token used to verify a user's identity without the server needing to look up a session on every request. It consists of three Base64URL-encoded parts, a header, a payload, and a signature, separated by dots. The signature lets a server confirm the token hasn't been tampered with, without storing any session data itself.
Introduction
Log into almost any modern app, and somewhere behind the scenes, a JWT is probably doing the work of proving you are who you say you are on every single request that follows. It's one of those pieces of infrastructure most users never see and most junior developers copy-paste into a project long before they actually understand what's inside it.
That gap matters, because JWTs are also one of the most commonly misused security mechanisms in web development. Storing them in the wrong place, skipping expiration checks, or misunderstanding what the signature actually protects are extremely common mistakes, and each one quietly weakens an authentication system that looks fine in a demo.
This guide breaks JWTs down properly: what's actually inside one, how the signing and verification process works, where they genuinely fit compared to traditional session-based authentication, and the mistakes worth avoiding before you rely on one in production.
What Is a JWT, Structurally
A JWT is a single string made up of three parts, separated by periods: header.payload.signature. Each of the first two parts is a JSON object that has been Base64URL-encoded into a compact string, and the third part is a cryptographic signature covering the first two.
The header identifies the signing algorithm being used and the token type, typically something like:
{
"alg": "HS256",
"typ": "JWT"
}
The payload contains the actual claims, meaning the data the token is asserting: who the user is, when the token was issued, when it expires, and any other application-specific information.
{
"sub": "user_1024",
"role": "editor",
"iat": 1753660800,
"exp": 1753664400
}
The signature is generated by taking the encoded header and payload, combining them with a secret key (or private key, depending on the algorithm), and running the result through the signing algorithm specified in the header. This signature is what lets a server later verify the token hasn't been altered since it was issued.
A frequent point of confusion worth clearing up directly: the payload is encoded, not encrypted. Anyone who intercepts a JWT can decode the header and payload instantly and read the claims inside, without needing the secret key at all. The secret key is only required to verify the signature, not to read the contents. This is exactly the same distinction we cover in Base64 vs Encryption — Base64URL encoding, which JWTs use, is a reversible encoding scheme, not a security mechanism on its own. If you want to see this for yourself, decoding a real JWT's header and payload with a Base64 decoder shows the raw JSON immediately, which is a genuinely useful way to understand what a token actually contains before relying on it in your own code. ToolNexIn's Base64 Encoder and Decoder handles exactly this kind of quick inspection directly in the browser.
Warning: Never place sensitive information, like a password, a full credit card number, or private personal data, inside a JWT payload. Anyone holding the token can read it without any special tools.
How Token-Based Authentication Actually Works, Step by Step
Step 1: The user logs in. They submit a username and password (or another credential) to an authentication endpoint.
Step 2: The server verifies the credentials and issues a JWT. If the credentials are correct, the server builds a payload containing the user's identity and relevant claims, signs it with a secret or private key, and returns the resulting token to the client.
Step 3: The client stores the token and sends it with subsequent requests. Typically as an Authorization: Bearer <token> header on every API call that requires authentication.
Step 4: The server verifies the signature on each request. Rather than looking up a session in a database, the server recalculates the expected signature using its secret key and compares it against the signature attached to the incoming token. If they match, and the token hasn't expired, the request is treated as authenticated.
Step 5: The server reads the claims directly from the payload, without any additional database lookup, to determine who the user is and what they're authorized to do.
This last step is the core efficiency JWTs offer: the server doesn't need to store or look up session state anywhere, because the token itself carries everything needed to verify and identify the request. That property is also exactly why the "exp" (expiration) claim matters so much, since it's a Unix timestamp marking exactly when the token should stop being accepted; we cover how these timestamps work in more general terms in What Is a Unix Timestamp, which is worth understanding if you're debugging why a token expired earlier or later than expected.
JWT vs Session-Based Authentication: A Direct Comparison
| Factor | JWT (Token-Based) | Session-Based |
|---|---|---|
| Where state lives | Entirely inside the token itself | Server-side session store (database, Redis, etc.) |
| Scalability across servers | Easy — any server with the shared secret can verify | Requires a shared session store across servers |
| Revoking access immediately | Difficult — token remains valid until it expires | Easy — delete the session server-side instantly |
| Payload visibility | Readable by anyone holding the token (not encrypted) | Not exposed to the client at all |
| Typical use case | APIs, microservices, mobile apps, single-page apps | Traditional server-rendered web applications |
| Storage on the client | Local storage, session storage, or a cookie | Usually a session ID stored in a cookie |
Neither approach is universally better. Session-based authentication gives you immediate, server-side control over revoking access, which JWTs sacrifice in exchange for statelessness and easier horizontal scaling. Many production systems use a hybrid: short-lived JWTs for the token itself, paired with a longer-lived, revocable refresh token tracked server-side, combining the scalability of JWTs with the ability to cut off access when needed.
Signing Algorithms: HMAC vs RSA vs ECDSA
JWTs support several signing algorithm families, and picking the right one depends on your system's architecture.
HMAC (commonly HS256) uses a single shared secret key for both signing and verifying. It's fast and simple, but every service that needs to verify tokens must have access to that same secret, which becomes a real operational risk if one service is compromised or the secret is accidentally exposed in a config file or repository. Generating a genuinely strong, unpredictable secret matters enormously here — a weak or guessable HMAC secret undermines the entire signature scheme regardless of which algorithm you chose. ToolNexIn's Password Generator is a quick way to produce a sufficiently long, random secret rather than relying on something memorable (and therefore guessable).
RSA (commonly RS256) uses a public/private key pair. The private key signs tokens and stays secret on the authentication server; the public key verifies tokens and can be distributed freely to any service that needs to check a token's validity, without ever exposing the ability to forge one. This is generally the safer choice in a microservices architecture, since services that only need to verify tokens never need access to anything that could be used to create fake ones.
ECDSA (commonly ES256) offers similar public/private key separation to RSA, with smaller key and signature sizes for equivalent security, which matters for systems processing very high volumes of tokens where every byte of overhead adds up.
Tip: If multiple independent services need to verify tokens but only one service should ever issue them, RSA or ECDSA is almost always the safer architectural choice over a shared HMAC secret.
Use Case 1: A Single-Page App Authenticating Against an API
A React or Vue front end calling a separate backend API is one of the most common JWT use cases. After login, the front end stores the returned JWT and attaches it as a Bearer token to every subsequent API call. The backend verifies the signature and reads the user's identity directly from the payload on each request, with no server-side session lookup required.
The practical risk here is where the token gets stored on the client. Storing it in localStorage is common but exposes it to any successful cross-site scripting (XSS) attack, since JavaScript running on the page can read localStorage freely. A more defensive pattern stores the token in an HttpOnly cookie instead, which JavaScript cannot access directly, trading a small amount of implementation complexity for meaningfully better protection against token theft.
Use Case 2: Microservices Verifying Identity Without a Shared Session Store
In a microservices architecture, dozens of independent services might need to confirm a request is authenticated, without all of them sharing a single centralized session database, which would itself become both a bottleneck and a single point of failure. JWTs solve this cleanly: one authentication service issues signed tokens, and every other service verifies the signature independently using a shared public key (with RSA or ECDSA), with no network call back to the authentication service required for routine verification.
This is precisely the scalability property that makes JWTs attractive in distributed systems, and it's also why choosing an asymmetric signing algorithm over a single shared HMAC secret matters more as the number of independent services grows.
Use Case 3: Third-Party API Access With Short-Lived Tokens
When a third-party application needs temporary access to a user's data (a common OAuth-style pattern), issuing a short-lived JWT scoped to exactly the permissions needed, rather than a long-lived credential, limits the damage if that token is ever leaked or intercepted. A token that expires in fifteen minutes and grants only read access to one specific resource is a fundamentally safer thing to hand to a third party than a broad, long-lived credential.
If you're building or debugging a system like this and need to inspect the exact structure of the claims a token carries during development, running the decoded payload through a JSON formatter makes a dense, unformatted JSON object dramatically easier to read and verify at a glance; we cover exactly this workflow in JSON Formatter Explained.
Common JWT Mistakes and Security Pitfalls
Storing sensitive data in the payload. Since the payload is only encoded, not encrypted, anything placed inside it is readable by anyone holding the token. Passwords, full financial details, and other sensitive personal data should never appear in a JWT payload.
Skipping expiration entirely. A token without a reasonable "exp" claim remains valid indefinitely, which defeats much of the security benefit token-based authentication is meant to provide. Short expiration times, paired with a refresh token mechanism, are the standard safer pattern.
Accepting the "none" algorithm. A well-documented historical JWT vulnerability involves an attacker modifying the header to specify "alg: none" and stripping the signature entirely; poorly implemented verification libraries have, in the past, accepted this as valid. Modern, well-maintained JWT libraries reject this by default, but it's worth confirming explicitly rather than assuming.
Using a weak or reused HMAC secret. A short, guessable, or reused-across-environments secret undermines the entire signature scheme, since anyone who obtains or guesses it can forge valid tokens.
Storing tokens insecurely on the client. Placing a JWT in localStorage without considering XSS exposure is a common and avoidable mistake, particularly for applications handling sensitive user data.
Assuming a JWT can be instantly revoked. Because the server doesn't track token state, a compromised JWT remains valid until it naturally expires, unless you've built a separate revocation mechanism (like a blocklist) alongside it. This is a real limitation worth designing around explicitly, not discovering during an incident.
Best Practices for Using JWTs Safely
- Keep expiration times short, and use a separate, revocable refresh token for maintaining longer sessions.
- Prefer RSA or ECDSA over a shared HMAC secret whenever multiple independent services need to verify tokens.
- Never store sensitive personal data directly in the payload.
- Store tokens in HttpOnly cookies where practical, rather than
localStorage, to reduce exposure to XSS attacks. - Validate the signing algorithm explicitly on verification rather than trusting whatever algorithm the token claims to use.
- Build a token revocation strategy (a blocklist or short-lived tokens with frequent refresh) rather than assuming expiration alone is sufficient.
- Generate signing secrets with genuine randomness and adequate length rather than a memorable phrase.
Key Takeaways
- A JWT is three Base64URL-encoded parts — header, payload, signature — not an encrypted container; anyone holding the token can read its contents.
- Token-based authentication trades the server-side control of session-based authentication for statelessness and easier scaling across multiple servers.
- HMAC uses one shared secret for signing and verifying; RSA and ECDSA use a public/private key pair, which is generally safer in multi-service architectures.
- Short expiration times, paired with a separate refresh token mechanism, address JWTs' biggest structural weakness: the inability to revoke a token instantly once it's issued.
- Never place sensitive data directly in a JWT payload, since encoding is reversible and not a substitute for encryption.
Conclusion
JWTs solve a real, specific problem: proving identity across requests and services without the overhead of a centralized session store. That efficiency comes with tradeoffs that are easy to overlook when a token-based login flow first works in a demo — the payload is readable by anyone with the token, revocation isn't instant, and the choice between a shared secret and a public/private key pair has real security implications as a system grows. Understanding what's actually inside a JWT, and why the signature protects integrity rather than confidentiality, is what separates a genuinely secure implementation from one that simply looks secure until it's tested.
Frequently Asked Questions
Is a JWT encrypted? No. A JWT's header and payload are Base64URL-encoded, which is reversible by anyone, not encrypted. Only the signature protects against tampering; it does not hide the contents.
Can a JWT be decoded without the secret key? Yes. The header and payload can be decoded by anyone instantly, since Base64URL encoding requires no key at all. The secret key is only needed to verify or generate a valid signature.
What happens when a JWT expires? The server rejects it once the current time passes the "exp" claim's timestamp. The client typically then uses a separate refresh token to obtain a new JWT without requiring the user to log in again.
Can you revoke a JWT before it expires? Not directly, since the server doesn't track token state. Common workarounds include maintaining a short blocklist of revoked tokens or keeping JWT expiration times short enough that the exposure window stays small.
What's the difference between JWT and OAuth? OAuth is an authorization framework describing how access is granted and delegated between systems; a JWT is commonly the token format OAuth implementations use to represent that access. They solve related but distinct problems.
Is HS256 or RS256 better for JWTs? RS256 (or ECDSA) is generally safer in systems where multiple independent services need to verify tokens, since only one service needs the private key. HS256 is simpler for single-service setups where only one system both issues and verifies tokens.
Where should a JWT be stored on the client? An HttpOnly cookie is generally safer than localStorage, since JavaScript cannot read HttpOnly cookies directly, reducing exposure if the application is ever compromised through cross-site scripting.
Why shouldn't sensitive data go inside a JWT payload? Because the payload is only encoded, not encrypted, anyone who obtains the token, through a network capture, browser storage access, or a leaked log, can read every claim inside it immediately.
Final Call-to-Action
If you're building or debugging a JWT-based authentication flow, start by decoding a real token with ToolNexIn's Base64 Encoder and Decoder to see exactly what's inside the payload, then run it through the JSON Formatter to check the claims are structured the way you expect. And if you're generating a signing secret for an HMAC-based setup, use the Password Generator to produce something genuinely random rather than a phrase you'll be tempted to reuse elsewhere.
Internal Link Suggestions
Tool Recommendations
- Base64 Encoder and Decoder — embedded where the article explains JWT structure, used to decode a real token's header and payload for hands-on inspection.
- JSON Formatter — embedded in the third-party API use case, used to pretty-print decoded JWT claims for easier debugging.
- Password Generator — embedded in the HMAC signing algorithm section and the closing CTA, used to generate a genuinely strong, random signing secret.
