KRYZO.

· Security guide · 8 min read

Securing APIs With JWT

A JSON Web Token proves who is calling your API. Here is how the three parts fit together and how to use them safely.

API security diagram showing a JSON Web Token with header, payload and signature

A JSON Web Token, usually shortened to JWT, is a compact string a server hands to a client after a successful login. The client sends it back on later requests so the server can recognise who is calling without checking a session store on every call.

The three parts of a token

A JWT is three base64url chunks joined by dots: a header, a payload and a signature. Anyone can decode and read the first two, so the token is a container, not a secret vault.

Header

Names the signing algorithm and the token type, for example {"alg":"HS256","typ":"JWT"}.

Payload

The claims: user id, roles and an expiry time. This is readable by anyone holding the token.

Signature

Produced from the header and payload with a secret key. It proves the token was not altered.

The most important consequence: the payload is signed, not encrypted. Never put passwords, secrets or private data in it, because a curious client can simply decode it.

How signing and verification work

When the server creates a token it signs the header and payload with a secret key. When a request arrives, the server recomputes the signature and compares. If even one character of the payload changed, the signatures differ and the token is rejected.

const jwt = require("jsonwebtoken");

const token = jwt.sign(
  { sub: user.id, role: user.role },
  process.env.JWT_SECRET,
  { expiresIn: "15m" }
);

Keep the signing secret in an environment variable, never in source code, and rotate it if it is ever exposed.

Sending the token

Clients send the token in the Authorization header using the Bearer scheme. This keeps credentials out of the URL, where they could be logged or shared.

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Verifying on every protected route

Verification is not a one-time step. Every protected endpoint must check the token before doing any work, and it must reject a token that fails verification or has expired.

function requireAuth(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);
    next();
  } catch (err) {
    res.status(401).json({ error: "Invalid or expired token" });
  }
}

Always use the library's verify method rather than decoding the payload yourself. Decoding skips the signature check and trusts data an attacker could forge.

Short expiry and refresh tokens

If a token is stolen, it stays valid until it expires, so a short lifetime limits the damage. A common pattern pairs a short-lived access token, such as fifteen minutes, with a longer-lived refresh token stored securely and used only to issue new access tokens.

Refresh endpoints should be strictly checked, rate limited and able to revoke a token that is no longer trusted.

Storage and transport cautions

Always serve APIs over HTTPS so tokens cannot be read in transit. Storing long-lived tokens in localStorage makes them readable by any script on the page, so a single cross-site scripting bug can leak them. Prefer short-lived tokens held in memory, or server-set cookies marked HttpOnly, Secure and a suitable SameSite value.

Common mistakes

Trusting the payload without verifying, accepting the none algorithm, keeping the secret in the repository and issuing tokens that never expire are all avoidable. Validate the signature, keep expiry short, and treat the token as a claim to confirm rather than a fact to believe.

Back to all blogs