JWT Authentication Explained (Flow, Code & Patterns)
By AZ Utils Editorial · · 12 min read
Most modern apps no longer keep a server-side record of who is logged in. Instead, they hand the browser a signed token and trust it on every request. That approach — JWT authentication — is what makes APIs, single-page apps and microservices scale so cleanly. This guide walks through exactly how it works, from the login request to a protected API call, with the flow, the code, and the pitfalls.
It is written for developers building or securing authentication, students learning how token auth works, and engineers who want a precise picture of the request lifecycle.
The Core Idea: Trust a Signed Token, Not Server State
Traditional authentication is stateful: when you log in, the server creates a session record, stores it (in memory or a database), and gives your browser a session ID. On every request the server looks up that ID to remember who you are. It works, but it requires shared storage that every server must reach, which complicates scaling.
JWT authentication is stateless. Instead of storing a session, the server issues a JSON Web Token — a signed, self-contained token describing who you are. Your client sends that token with each request, and the server simply verifies the signature to trust it. There is nothing to look up, so any server holding the verification key can authenticate the request independently. That is the property that makes JWTs so popular for distributed systems.
In short: In JWT authentication, the server issues a signed token at login, the client sends it with every request (usually as a Bearer token), and the server verifies the signature to authenticate the user — with no server-side session to store.
The Authentication Flow, Step by Step
Here is the full journey of a typical JWT-authenticated session:
- Login. The user submits credentials (email and password) to a login endpoint over HTTPS.
- Verify credentials. The server checks the credentials against its user store, comparing the password against a securely hashed value.
- Issue a token. On success, the server builds a JWT with claims like the user's ID and an expiry, then signs it with its secret or private key.
- Return the token. The server sends the JWT back to the client, which stores it (in an httpOnly cookie or browser storage).
- Authenticated requests. For each subsequent request to a protected endpoint, the client includes the token, typically in an
Authorization: Bearer <token>header. - Verify and authorize. The server verifies the signature, checks the claims (expiry, audience, issuer), and — if everything is valid — processes the request as that user.
- Expiry and refresh. When the token expires, the client obtains a new one, often using a longer-lived refresh token, and the cycle continues.
The elegant part is step 6: the server proves your identity from the token alone, every time, without consulting a session store.
The Authorization Header and Bearer Tokens
The most common way to send a JWT is the HTTP Authorization header using the Bearer scheme:
GET /api/profile HTTP/1.1
Host: example.com
Authorization: Bearer eyJhbGciOiJIUzI1Ni␣...<header.payload.signature>
"Bearer" means exactly what it sounds like: whoever bears (holds) the token is treated as the authenticated user. That is precisely why protecting the token in transit and at rest is so important — anyone who steals it can impersonate the user until it expires. Always transmit Bearer tokens over HTTPS, never in a URL where they can leak into logs and browser history.
Issuing and Verifying Tokens in Code
In practice you lean on a battle-tested library. Here is the shape of it in Node.js with jsonwebtoken.
Issuing a token at login
import jwt from "jsonwebtoken";
app.post("/login", async (req, res) => {
const user = await findUser(req.body.email);
const ok = user && await verifyPassword(req.body.password, user.passwordHash);
if (!ok) return res.status(401).json({ error: "Invalid credentials" });
const token = jwt.sign(
{ sub: user.id, role: user.role }, // claims (no secrets!)
process.env.JWT_SECRET,
{ expiresIn: "15m", issuer: "example.com", audience: "example-api" }
);
res.json({ token });
});
Protecting routes with middleware
function authenticate(req, res, next) {
const header = req.headers.authorization || "";
const token = header.startsWith("Bearer ") ? header.slice(7) : null;
if (!token) return res.status(401).json({ error: "Missing token" });
try {
req.user = jwt.verify(token, process.env.JWT_SECRET, {
algorithms: ["HS256"], // pin the algorithm
issuer: "example.com",
audience: "example-api",
});
next();
} catch (err) {
return res.status(401).json({ error: "Invalid or expired token" });
}
}
app.get("/api/profile", authenticate, (req, res) => {
res.json({ id: req.user.sub, role: req.user.role });
});
Two details carry most of the security weight: the algorithm is pinned with algorithms: ["HS256"], and the issuer and audience are validated. Skipping either is a common, serious mistake covered in JWT Security Best Practices.
Access Tokens and Refresh Tokens
There is a tension at the heart of JWT auth: short-lived tokens are safer (a stolen token expires quickly) but force users to log in often, while long-lived tokens are convenient but dangerous if stolen, because a self-contained JWT cannot be easily revoked. The standard resolution is a two-token system.
- A short-lived access token (often 5–15 minutes) is sent with every API request. Its brief lifetime limits the damage if it leaks.
- A longer-lived refresh token (days or weeks) is stored more securely and used only to obtain new access tokens from a dedicated refresh endpoint.
When the access token expires, the client quietly exchanges the refresh token for a fresh access token, so the user stays logged in without re-entering credentials. Crucially, refresh tokens can be revoked — typically by storing a reference to them server-side or rotating them on each use — which gives you a way to log a user out for real, restoring some of the control that pure stateless tokens give up.
Stateful vs Stateless, in Depth
It is worth dwelling on the trade-off at the heart of this choice, because it explains most of JWT authentication's strengths and weaknesses. A stateful session keeps the source of truth on the server: the session record exists in storage, and the client holds only an opaque pointer to it. This makes revocation trivial — delete the record and the user is instantly logged out — but it means every request depends on reaching that shared storage, which becomes a scaling and availability concern as traffic and the number of servers grow.
A stateless JWT inverts this. The source of truth travels with the request, inside the signed token, so no shared lookup is needed and any server can authenticate independently. The price is that there is no central record to delete, so revocation is hard — a valid token remains valid until it expires. Almost every JWT best practice, from short lifetimes to refresh tokens to denylists, exists to buy back some of the revocation control that statelessness gives away. Seeing the architecture this way makes the rest of the design decisions feel less like arbitrary rules and more like a coherent response to one fundamental trade-off.
Handling Expiry and Errors Gracefully
A robust client has to handle the moment an access token expires, which it will do constantly if lifetimes are short. The usual pattern is to treat a 401 Unauthorized response as a signal to attempt a silent token refresh: the client calls the refresh endpoint with its refresh token, receives a new access token, and transparently retries the original request. If the refresh itself fails — because the refresh token has expired or been revoked — the client falls back to sending the user to log in again.
It also helps to distinguish two different failures clearly. A 401 Unauthorized means the request lacks valid authentication — no token, an expired token, or a bad signature — and the right response is to authenticate (or refresh). A 403 Forbidden means the user is authenticated but lacks permission for this action — for example, a valid token whose role claim does not grant access. Conflating the two leads to confusing behaviour, such as bouncing a perfectly valid user to the login screen when the real problem was authorization, not authentication.
JWTs in OAuth 2.0 and OpenID Connect
Much of the JWT you encounter in the wild arrives through two related standards. OAuth 2.0 is an authorization framework for granting an application limited access to resources on a user's behalf, and its access tokens are frequently JWTs carrying scopes that describe what the bearer may do. OpenID Connect builds an identity layer on top of OAuth 2.0 and introduces the ID token — a JWT that asserts who the user is, with identity claims like name and email. So when you "sign in with" a major provider, a JWT is very often the thing that ends up representing your authenticated identity and granted permissions. You rarely implement these flows by hand, but recognising that the tokens they produce are ordinary JWTs — verifiable with the provider's published public keys — demystifies a great deal of real-world authentication.
Try Our Free JWT Decoder
While building or debugging auth, you will constantly want to peek inside a token to check its claims and expiry. Our JWT Decoder does it instantly and privately.
- ✅ See the decoded header and payload
- ✅ Human-readable issued-at and expiry times
- ✅ Runs in your browser — tokens never leave your device
Real-World Patterns
- Single-page apps (SPAs). A React or Vue app logs in, receives a token, and attaches it to API calls. Many apps keep the access token in memory and the refresh token in an httpOnly cookie to balance usability and safety.
- Mobile apps. The token is stored in the device's secure storage and sent with each API request — a natural fit for stateless auth.
- Microservices. A gateway issues a JWT, and each downstream service verifies it with a shared public key, so identity flows across services without a central session store.
- Third-party access. OAuth 2.0 and OpenID Connect issue JWTs (ID tokens and access tokens) to represent authenticated users and granted scopes.
Architectural Questions Teams Ask
A few questions come up again and again when teams adopt JWT authentication, and thinking them through up front avoids painful rework. The first is where to keep the access token on the client. Holding it in memory is the safest against theft because it disappears on refresh and is never written to disk, but it means the user is logged out whenever the page reloads unless a refresh token can silently restore the session. Many teams settle on keeping the short-lived access token in memory and the longer-lived refresh token in an httpOnly cookie, which balances usability against exposure.
A second recurring question is how to handle logout in a system that, by design, has no server-side session to delete. The honest answer is that you cannot instantly invalidate a self-contained access token, so logout is implemented by discarding the tokens on the client and revoking the refresh token on the server, after which the short access-token lifetime closes the remaining window. A third question is whether to verify tokens at an API gateway or in each service. Centralising verification at a gateway keeps individual services simpler, while verifying in each service — using a shared public key fetched from a JWKS endpoint — keeps trust decisions close to the resource and avoids a single point of failure. There is no universally correct answer; the right choice depends on how much you value simplicity versus defence in depth, but deciding deliberately beats discovering the constraint later.
Common Mistakes
- Not pinning the algorithm. Letting the token dictate the verification algorithm enables algorithm-confusion attacks, including the infamous
alg: none. - Forgetting to validate claims. Verifying the signature but ignoring
exp,audorissaccepts tokens you should reject. - Making access tokens long-lived. A self-contained token cannot be easily revoked, so a long lifetime means a long window of risk.
- Putting tokens in URLs. They leak into logs, history and
Refererheaders. - Storing tokens where XSS can reach them. Script-accessible storage risks token theft; httpOnly cookies mitigate it.
- No refresh/revocation strategy. Without one, you cannot truly log users out before expiry.
Best Practices
- Use short-lived access tokens plus refresh tokens.
- Pin the verification algorithm and validate
exp,issandaudon every request. - Transmit only over HTTPS, and prefer httpOnly cookies for storage where it fits your architecture.
- Keep claims minimal and non-secret.
- Rotate and revoke refresh tokens to enable real logout.
- Handle expiry gracefully on the client with a transparent refresh.
Frequently Asked Questions
How does JWT authentication work?
At login the server verifies credentials and issues a signed JWT describing the user. The client sends that token with each request, usually in an Authorization: Bearer header, and the server verifies the signature and claims to authenticate the request — with no server-side session.
Where is the JWT sent on each request?
Most commonly in the HTTP Authorization header using the Bearer scheme: Authorization: Bearer <token>. It should always be sent over HTTPS and never placed in a URL.
What is the difference between an access token and a refresh token?
An access token is short-lived and sent with every API request; a refresh token is longer-lived, stored more securely, and used only to obtain new access tokens. Refresh tokens can be revoked, enabling real logout.
How do I log a user out with JWTs?
Because a self-contained JWT cannot be deleted, real logout is handled by using short-lived access tokens and revoking the refresh token, or by maintaining a server-side denylist of tokens.
Is JWT authentication secure?
Yes, when implemented correctly: verify the signature with a pinned algorithm, validate claims, keep tokens short-lived, transmit over HTTPS and store them safely. Most JWT vulnerabilities come from skipping one of these steps.
Should I use JWTs or server sessions?
Use JWTs for stateless, cross-service authentication such as APIs, SPAs and microservices. Use server sessions when you want simple, immediate revocation in a traditional single-server app. See our JWT vs Session Authentication comparison.
Summary
JWT authentication replaces server-side sessions with a signed, self-contained token: the server issues it at login, the client returns it on every request as a Bearer token, and the server verifies the signature and claims to authenticate the user without any lookup. That statelessness is the source of its scalability and the reason it dominates APIs and microservices. The cost is revocation, which the access-token-plus-refresh-token pattern restores. Do the security basics — pin the algorithm, validate the claims, keep tokens short-lived, use HTTPS and store them carefully — and you have a robust, modern authentication system. Keep a JWT decoder handy while you build, and the whole flow becomes easy to reason about.
If you internalise one thing, let it be the shape of the round trip: authenticate once, receive a signed token, and present that token as proof on every subsequent request, with the server trusting the signature rather than a stored session. Everything else — Bearer headers, access and refresh tokens, expiry handling, gateway versus per-service verification — is detail layered on top of that single idea. Hold the core flow clearly in mind and the rest of the design decisions fall into place naturally.
👉 Inspect your tokens with our free JWT Decoder →
Related Resources
- What Is a JWT? — the fundamentals
- Common JWT Claims — the claims you validate
- JWT Security Best Practices — secure your implementation
- JWT vs Session Authentication — the trade-offs
- JWT Decoder — the tool