Before JWT (JSON Web Tokens), authentication typically relied on server-side sessions — the server stored who was logged in in memory or a database, and the browser held a session ID in a cookie. That works, but it doesn't scale cleanly across multiple servers. JWT solved this by making the token itself carry the proof of identity, with nothing to look up on the server.
A JWT is three Base64-encoded parts separated by dots: a header (algorithm used), a payload (the actual claims, like user ID and role), and a signature. The signature is generated using a secret key known only to the server — this is what proves the token hasn't been tampered with.
header.payload.signature
Anyone can decode and read the payload of a JWT (it's not encrypted, just encoded) — but only the server can verify the signature, which is why you should never put sensitive data like passwords directly in the payload.
1. The user submits their email and password. 2. The server verifies the credentials against the database. 3. If valid, the server signs a new JWT containing the user's ID and issues it back to the client. 4. The client stores the token (commonly in an HTTP-only cookie or, less securely, localStorage) and sends it with every subsequent request, usually in the `Authorization: Bearer <token>` header. 5. The server verifies the signature on each request — no database lookup needed to confirm the user is who they claim to be.
const jwt = require('jsonwebtoken');
// On login const token = jwt.sign( { userId: user.id }, process.env.JWT_SECRET, { expiresIn: '7d' } );
// On protected routes const payload = jwt.verify(token, process.env.JWT_SECRET);
The `expiresIn` option is important — a JWT with no expiry is a permanent credential if it's ever leaked. Most production systems also use a short-lived access token paired with a longer-lived refresh token to limit that exposure window.
Storing JWTs in plain localStorage exposes them to any XSS vulnerability on your site — an HTTP-only cookie is safer since JavaScript can't read it. Never sign a JWT with a weak or hardcoded secret. And remember that a JWT can't be "revoked" the way a database session can, since verification doesn't check a database — this is why most real systems keep tokens short-lived and use a refresh-token pattern for anything that needs true logout/revocation support.
JWT solved a real scaling problem — stateless authentication that doesn't require a shared session store across servers. But that statelessness is also its main limitation: understand the trade-off around token expiry and revocation before choosing it for a system with strict security requirements.
Not inherently — they solve different problems. Sessions are easier to revoke instantly since the server controls the session store. JWTs scale better across multiple servers but are harder to revoke before they expire, so the right choice depends on your architecture.
Yes, if it's exposed through XSS or intercepted over an insecure connection. Always use HTTPS, store tokens in HTTP-only cookies where possible, and keep expiry times short to limit the damage if a token is compromised.
An access token is short-lived (minutes to hours) and used to authenticate API requests. A refresh token is longer-lived and used only to obtain a new access token once the old one expires, without forcing the user to log in again.