Backend

Secure Authentication with JWT

JWT is popular because it is simple, and that same simplicity makes insecure implementations very common. This article shows how to design JWT authentication that is convenient and still resistant to the usual mistakes.

  • By TETA Team
  • 8 min read
  • Updated:
Secure Authentication with JWT

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.

json
// 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:

Comparing the two token types
PropertyAccess tokenRefresh token
Lifetime5 to 15 minutesDays to weeks
Used forCalling the APIOnly to obtain a new access token
Server storageNoneIts hash is stored in the database
RevocationVia short expiryDelete 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.

Colourful abstract illustration of a glowing digital key and two tokens passing between a user and a server
A short-lived token limits the damage of theft, and rotating refresh tokens make theft detectable.

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:

js
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 (RS256 or ES256) instead of a shared secret so only one service can mint tokens.
  • Hash passwords with argon2 or bcrypt, 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=Lax or Strict and a CSRF token.
js
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: none or a token-chosen algorithm. Fix the algorithm on the server and use the algorithms option.
  • 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 exp and aud. 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.

    Comments are published after review.