JWT Explained: Claims, Signatures, and Where to Store Them
Understand JWT structure, signing algorithms, and token storage so you can avoid XSS and CSRF exposure when securing your API against attackers.
Netbay Developer Relations
Netbay Engineering
On this page
JSON Web Tokens are everywhere in modern APIs, yet engineers routinely misuse them. The core idea is simple: a compact, signed JSON object that a server can trust without a database lookup. The details — which claims to include, which algorithm to sign with, and where the token lives on the client — decide whether that trust holds up in practice.
Anatomy of a token
A JWT is three dot-separated, base64url-encoded segments. The header declares the algorithm, the payload carries claims, and the signature proves the header and payload were not tampered with.
- **Header**: 'alg: HS256, typ: JWT'. The alg field is what a server must validate strictly.
- **Payload**: claims such as 'sub' (subject), 'iss' (issuer), 'aud' (audience), 'exp' (expiry), 'iat' (issued at).
- **Signature**: HMAC over 'base64url(header).base64url(payload)' for symmetric keys, or an RSA/ECDSA signature for asymmetric ones.
Choosing an algorithm safely
The classic JWT failure is the algorithm confusion attack, where an attacker re-signs a token with 'alg: none' or switches from an asymmetric to a symmetric key type. Mitigate it at the library level by allowing an explicit allow-list. Never derive the verification key from untrusted token data, and always treat 'exp' as mandatory.
For most Netbay workloads, a signed short-lived token (15 minutes) plus a rotating refresh flow is the right shape. Put as little sensitive data in the payload as possible — remember that without encryption a JWT is base64, not private. Anyone who captures it can read the claims.
Claims should be defensive by default. Keep the payload to identifiers your downstream services actually need, and nothing else: a subject id, an audience, an issuer, and the time fields are usually enough. Avoid stuffing in email addresses or roles that read from a database, because every claim you embed either leaks information or goes stale. If you do need roles, prefer compact group ids that your services resolve server-side, so you never have to re-issue tokens just to change a permission.
Token lifetime is a direct trade-off. A shorter expiry shrinks the window in which a stolen token is useful but forces more frequent refreshes and more trips to your token endpoint. Fifteen minutes is a common balance for user-facing API tokens, while internal service tokens often live longer precisely because the underlying key rotation already bounds the risk. Whatever you choose, make the expiry explicit and refuse tokens that carry no expiry at all — a permanently valid token is a standing credential you will eventually regret.
Where to store the token on the client
This is where security is won or lost. The two common homes are 'localStorage' and an 'HttpOnly' cookie.
- **localStorage** is accessible to any JavaScript on the origin, so one XSS payload empties your token store. Convenient for SPAs, dangerous under scripting.
- **An 'HttpOnly; Secure; SameSite=Lax' cookie** keeps the token invisible to JavaScript, shrinking the XSS blast radius, while 'SameSite' limits CSRF exposure.
For a browser API client, prefer the HttpOnly cookie route or move auth state to a trusted backend and store only ephemeral data on the client.
A tiny Node verification example
const crypto = require("crypto");
function base64url(buf) {
return Buffer.from(buf).toString("base64url");
}
function verify(token, secret) {
const [h, p, s] = token.split(".");
const header = JSON.parse(Buffer.from(h, "base64url").toString());
if (header.alg !== "HS256") throw new Error("unexpected alg");
const expected = crypto.createHmac("sha256", secret)
.update(h + "." + p).digest();
const provided = Buffer.from(s, "base64url");
if (!crypto.timingSafeEqual(expected, provided)) {
throw new Error("bad signature");
}
const payload = JSON.parse(Buffer.from(p, "base64url").toString());
if (payload.exp < Math.floor(Date.now() / 1000)) {
throw new Error("expired");
}
return payload;
}Setting a secure cookie
res.setHeader("Set-Cookie", [
"session=" + token,
"HttpOnly",
"Secure",
"SameSite=Lax",
"Path=/",
"Max-Age=900"
].join("; "));Takeaway
JWTs are a convenient stateless credential, but treat the payload as public data, pin the algorithm, enforce expiry, and store tokens where page scripts cannot read them. Train this habit on a real endpoint you control — a Netbay VPS at netbayhosts.in lets you stand up a test API in minutes.
Keep reading
Follow along on a real VPS
Deploy Linux in under 60 seconds
These guides are written against Ubuntu, Debian, and RHEL-family images — the same ones on NetBay.
Deploy an instance