How JWT authentication works
After a successful login the server creates a signed token and hands it to the client. The client sends it in an Authorization: Bearer … header on every request, and the server identifies the user just by verifying the signature, without touching a database or session store. That statelessness makes JWT attractive for scalable APIs.
A JWT has three dot-separated parts: header (algorithm), payload (claims) and signature. The first two are merely Base64URL-encoded and anyone can read them; the signature only prevents tampering.
// payload of a typical access token
{
"sub": "user_42", // who
"iss": "https://api.example.com",
"aud": "example-web",
"iat": 1795000000,
"exp": 1795000900, // 15 minutes later
"role": "editor"
}Access tokens and refresh tokens
If a token is stolen it stays valid until it expires, and a stateless server cannot simply cancel it. So we use two tokens:
| Property | Access token | Refresh token |
|---|---|---|
| Lifetime | 5 to 15 minutes | Days to weeks |
| Used for | Calling the API | Only to obtain a new access token |
| Server storage | None | Its hash is stored in the database |
| Revocation | Via short expiry | Delete the database record |
Refresh tokens should rotate: each use issues a new one and invalidates the old one. If an old token reappears, someone copied it, so revoke the whole token family for that session. Because revocation needs server-side records, you can keep fast lookups in Redis.

Issuing and verifying correctly
An example with the jsonwebtoken library in Node.js. The key point in verification is pinning the algorithm and the issuer and audience explicitly:
const jwt = require("jsonwebtoken");
function issue(user) {
return jwt.sign({ sub: user.id, role: user.role }, process.env.JWT_SECRET, {
algorithm: "HS256", expiresIn: "15m",
issuer: "https://api.example.com", audience: "example-web"
});
}
function verify(token) {
// throws on bad signature, expiry, wrong iss/aud or other algorithm
return jwt.verify(token, process.env.JWT_SECRET, {
algorithms: ["HS256"],
issuer: "https://api.example.com", audience: "example-web"
});
}- Keep the secret in an environment variable, at least 32 random bytes, and never commit it.
- If several services verify tokens, use asymmetric signing (
RS256orES256) instead of a shared secret so only one service can mint tokens. - Hash passwords with
argon2orbcrypt, never plain SHA.
Where to store the token
In a browser there are two main options and each has trade-offs:
- localStorage: easy to implement, but any script running on the page (via an XSS hole or a compromised dependency) can read the token.
- HttpOnly + Secure + SameSite cookie: JavaScript cannot read it, so XSS theft is harder; in return you must defend against CSRF with
SameSite=LaxorStrictand a CSRF token.
res.cookie("refresh_token", token, {
httpOnly: true, secure: true, sameSite: "strict",
path: "/auth/refresh", maxAge: 7 * 24 * 3600 * 1000
});A practical pattern: keep the access token in app memory and the refresh token in an HttpOnly cookie sent only to /auth/refresh. On mobile, use the Keychain or Keystore.
Common mistakes that break security
- Accepting
alg: noneor a token-chosen algorithm. Fix the algorithm on the server and use thealgorithmsoption. - A weak HS256 secret. Short keys fall to brute force.
- A long-lived access token. “30 days” means a month of opportunity for an attacker.
- Ignoring
expandaud. A token for one service must not be accepted by another. - Trusting the role claim blindly. For sensitive operations, confirm access against the database too.
- Logging full tokens. A token in a log is exposed to everyone who can read the log.
See the broader list of API risks in the OWASP API Security Project and read the standard details in the MDN HTTP authentication guide.
Do you actually need JWT?
For a simple monolithic site on one server, a server-side cookie session is simpler, safer and revocable instantly. JWT pays off when several independent services or mobile and web apps share one API and you do not want every request to hit a shared session store.
If your architecture is heading toward separate services, microservices architecture shows what tokens do across those boundaries. Every authentication flow should also be covered by tests; see automated testing in Laravel for an example. And if you need a secure login system or API for your product, get in touch.
Frequently asked questions
Is JWT secure?
The format itself is sound, provided it is implemented properly: a fixed algorithm, a strong key, short lifetimes, exp and aud validation and careful storage. Most holes come from the implementation, not the standard.
Where should I store a JWT?
On the web, an HttpOnly cookie with Secure and SameSite is usually better than localStorage because page JavaScript cannot read it. On mobile, use the iOS Keychain or Android Keystore.
How do I revoke a JWT?
Let short-lived access tokens expire, and store refresh tokens in the database so deleting the record revokes them. For immediate revocation, put the token ID (jti) on a Redis denylist.
What is the difference between JWT and sessions?
With sessions the data lives on the server and the client holds only an ID; with JWT the information is inside a signed token. Sessions are easier to revoke, while JWT is more stateless.

Comments
No comments yet. Be the first to share your thoughts.