← All posts

14 October 2026 · Ultracore Team

JWT Explained: How JSON Web Tokens Work and the Security Mistakes to Avoid

If you have built or used a modern API, you have probably seen a long string starting with eyJ. That’s a JSON Web Token (JWT), a compact way for one system to tell another “this user is who they say they are, and here’s what they’re allowed to do”.

The three parts of a JWT

A JWT is three Base64url-encoded pieces joined by dots: header.payload.signature.

  • Header: which algorithm signed the token, e.g. {"alg":"HS256","typ":"JWT"}
  • Payload: the claims, the actual data, e.g. user ID, roles and expiry time
  • Signature: proof that the header and payload haven’t been changed since they were signed

Paste any token into our JWT Decoder to see these parts, with dates converted to readable times.

Standard claims

  • sub (subject): who the token is about, usually a user ID
  • iss (issuer): who created it
  • aud (audience): which service it’s meant for
  • exp (expires at), iat (issued at) and nbf (not before): Unix timestamps in seconds

HS256 vs RS256

HS256 (HMAC) uses one shared secret to both sign and verify. It’s simple and fast, but every service that verifies tokens must hold the secret, and any of them could also create tokens.

RS256 and ES256 use a key pair: the auth server signs with a private key, and other services verify with the public key. Services that only verify can’t forge tokens. Choose this when several services or third parties need to check your tokens.

Security mistakes to avoid

1. Decoding instead of verifying

Decoding a token just reads it; anyone can create a token with any payload. Your server must verify the signature before trusting a single claim. Libraries usually have separate decode and verify functions, and using the wrong one is a critical bug.

2. Accepting any algorithm

Attackers can change the header to "alg": "none" (unsigned) or switch RS256 to HS256 and sign with your *public* key as the secret. Always tell your library exactly which algorithms to accept:

const payload = jwt.verify(token, publicKey, {
  algorithms: ['RS256'],
  issuer: 'https://auth.example.com',
  audience: 'orders-api',
});

3. Weak HMAC secrets

If a token is signed with a short or guessable secret like secret123, an attacker can take one token and brute-force the secret offline, then forge any token they like. Use at least 256 random bits, generated by a secure random generator.

4. Not checking expiry and audience

A token for your staging environment, or one that expired last month, should be rejected. Most libraries check exp automatically, but aud and iss usually need to be configured.

5. Long-lived tokens

A JWT stays valid until it expires, even if the user logs out or their account is disabled. Keep access tokens short-lived (5–15 minutes) and use a refresh token, which you *can* revoke on the server, to get new ones.

6. Storing tokens carelessly

Tokens in localStorage can be stolen by any cross-site scripting bug on your site. An HttpOnly, Secure, SameSite cookie keeps the token out of reach of JavaScript, though it then needs CSRF protection. Pick deliberately; don’t default to whatever the tutorial did.

Debugging tokens safely

When a login fails with “invalid token”, the cause is usually an expired exp, a wrong aud, or a secret that differs between services. The JWT Decoder shows the expiry status at a glance and can verify HS256, RS256, PS256 and ES256 signatures with your secret or public key. Everything happens in your browser, so tokens are never sent to a server.

Free toolTry the JWT DecoderRuns in your browser. No sign-up.

Have a project in mind? Let’s talk it through.

A free, no-commitment call to understand what you’re building and how we can help.