The JWT mistake every junior makes

Trusting the token without verifying the signature, or accepting `alg: none`. Two bugs, same root cause.

· LoginWith team

JWTs are three base64-encoded strings separated by dots. The first two are JSON. It’s irresistible to just decode them and read the claims. And it’s exactly the wrong thing to do.

Mistake 1: decoding without verifying

// Looks harmless, is not
const payload = JSON.parse(atob(token.split('.')[1]))
if (payload.role === 'admin') { /* ... */ }

The signature exists to prove the token came from the issuer and hasn’t been tampered with. If you decode without verifying, an attacker can craft any JWT they want — any role, any sub, any claim — and you’ll trust it. The token is no longer a credential; it’s a suggestion.

The fix is to use a library that verifies:

const payload = await jwtVerify(token, publicKey, {
  issuer: EXPECTED_ISSUER,
  audience: EXPECTED_AUDIENCE,
})
// Now payload can be trusted

Mistake 2: accepting alg: none

In 2015, a researcher noticed that some JWT libraries honored the alg: none header, which means “this token is not signed.” If you send such a token, the library happily parses the claims and returns them as valid. CVE. Fixes rolled out, but some libraries still have the behavior lurking in older versions, and some hand-rolled verifiers reintroduce it.

Rule: always pin the algorithm when verifying. If the token says alg: HS256, verify with HS256 and reject anything else — don’t let the token choose the algorithm.

await jwtVerify(token, secret, {
  algorithms: ['HS256'],  // explicit allowlist
})

Mistake 3: confusing HS256 and RS256

HS256 uses a shared secret — the same key signs and verifies. RS256 uses a public/private key pair — private signs, public verifies. If the verifier expects HS256 but the attacker sends an RS256 token signed with the public key (which is, well, public), some flawed libraries will try to verify with HS256 using the public key as the secret. Boom, forgery.

Same fix: pin the algorithm. If your issuer uses RS256, only accept RS256.

Mistake 4: ignoring exp, iss, aud

The claims in the token are signed, but they’re only useful if you actually check them:

  • exp — expiration. If you don’t check it, the token is valid forever.
  • iss — issuer. If you don’t check it, you’ll accept tokens signed by the wrong IdP.
  • aud — audience. If you don’t check it, you’ll accept a token intended for a different service.

All three are easy to miss when you’re writing the verifier yourself. Use a library, pin the algorithm, set issuer and audience explicitly, and let the library reject everything else.

The summary

JWTs feel simple. They’re not. Use a well-maintained library. Configure it strictly. Never decode without verifying. Never accept alg: none. Always pin the algorithm.

Want auth that just works?

Get started with LoginWith