Skip to content
security

JWT Explained: JSON Web Tokens, JWT Authentication, and the Pitfalls

Updated 2026-05-15 · 12 min read · By @guptadeepak

Key takeaways

  • JWT is a token format, not a session protocol. Most JWT bugs come from treating it as a drop-in session cookie.
  • The alg field in the header is the vulnerability surface. Pin the expected algorithm at the verifier; never trust the token to declare its own.
  • Confused-deputy bugs (HS256-vs-RS256 mix-up, jku/x5u SSRF, kid path injection) are the JWT vulnerability class that keeps producing CVEs.
  • Stateless JWTs cannot be revoked. If revocation matters in your threat model, use opaque tokens or a hybrid scheme.
  • PASETO and Biscuit exist as JWT alternatives precisely because JWT's flexibility is its weakness; pick them when the JWT library risk outweighs the JWT ecosystem advantage.
  • JWT for session cookies in browser apps is fine when the lifetime is short, the audience is constrained, and you have a revocation story for forced logout.

What a JWT actually is

The structure, with each part decoded:

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImsxIn0    <- header
.
eyJzdWIiOiJ1c2VyXzEyMyIsImlzcyI6Imh0dHBzOi8vaWRwLmV4YW1wbGUuY29tIiwiYXVkIjoiYXBpLmV4YW1wbGUuY29tIiwiZXhwIjoxNzMxNjA5NjAwfQ    <- payload
.
<256-bit RSA signature, base64url>    <- signature

Header: {"alg": "RS256", "typ": "JWT", "kid": "k1"}.

Payload: {"sub": "user_123", "iss": "https://idp.example.com", "aud": "api.example.com", "exp": 1731609600}.

The signature is computed over the ASCII string <base64url(header)>.<base64url(payload)> using the algorithm in the header and the key the issuer holds. Verification recomputes the signature and compares.

JWT is one of a family of specs: JWS (RFC 7515, the signing format), JWE (RFC 7516, encrypted variant), JWA (RFC 7518, the algorithms), and JWK (RFC 7517, the key format). Best practices live in RFC 8725.

JWT authentication in practice

The phrase "JWT authentication" usually describes a Bearer-token flow: the user logs in once (password, OIDC, SAML, passkey — JWT does not care how), the server issues a signed JWT, and the client attaches it to every subsequent request as Authorization: Bearer <jwt>. The server validates signature + claims on each request and treats the sub claim as the authenticated principal. No server-side session row, no database lookup — that statelessness is what makes JWTs scale, and what makes them hard to revoke.

In OAuth 2.0 and OIDC the same pattern shows up in two distinct token roles. The ID Token is always a JWT and proves who the user is to the client; the spec hard-codes the format. The access token is the credential the client sends to APIs — it is frequently a JWT (and at this point usually is), but the OAuth 2.0 spec deliberately leaves the format opaque so issuers can use opaque tokens instead. If you see "JWT authentication" in a vendor doc, they almost always mean "we use JWT-format access tokens." The validation rules below apply identically either way.

Claims: what goes in the payload

The standard claims (iss, sub, aud, exp, nbf, iat, jti) are defined in RFC 7519. Anything else is custom and per-issuer. The validator should always check the standard claims; custom claims are application-specific.

  • iss — issuer. The OP / API gateway / whatever produced the token. Pin per connection.
  • sub — subject. The user (or service principal) the token represents.
  • aud — audience. Who the token is for. Must match the verifier's identity exactly; mismatched audiences are how access tokens get accepted by the wrong resource.
  • exp — expiration time, seconds since epoch. Hard deadline.
  • nbf — not-before time. Token is invalid before this time.
  • iat — issued-at time.
  • jti — unique token ID. Used for revocation denylists or replay prevention.

Custom claims are anything else: email, roles, tenant_id, permissions, etc. Keep them small; every JWT goes on every request that uses it.

Algorithms: the vulnerability surface

JWA defines the algorithm set. The relevant ones:

  • HS256, HS384, HS512 — HMAC with SHA-2. Symmetric: the same secret signs and verifies. Use only when issuer and verifier are the same process or share a secret over a trusted channel.
  • RS256, RS384, RS512 — RSA with SHA-2. Asymmetric: private key signs, public key verifies. The standard for OIDC ID Tokens.
  • ES256, ES384, ES512 — ECDSA. Smaller signatures than RSA at comparable security. Increasingly preferred for new deployments.
  • PS256, PS384, PS512 — RSASSA-PSS. RSA with probabilistic padding. Stronger guarantees than RS*; required by some compliance regimes.
  • EdDSA (Ed25519) — newer, fast, simple. Supported by modern libraries; not yet universal.
  • none — no signature. Defined by the spec; rejected by every safe library.

The validator must pin the expected algorithm. Accepting whatever the token declares is the root of the worst JWT bugs.

The vulnerability classes

The same handful of bugs recur across implementations:

alg:none acceptance. The library checks the signature using the algorithm in the header. If the header says alg: none, no signature is checked, and the token is treated as valid. Modern libraries reject this by default. The verifier should explicitly pin allowed algorithms anyway, in case a library's default flips.

HS256-vs-RS256 confused deputy. The library accepts both HMAC and RSA, and key material is loaded into both code paths. An attacker takes a legitimate RS256 token, changes alg to HS256, and signs the new payload using the RSA public key as the HMAC secret. The verifier validates because the HMAC happens to use the same byte string. Defense: per-key allowed-algorithm pinning. Never let a key intended for one algorithm be used for another.

kid header injection. The kid header is a string identifier for which key to use. If the implementation uses kid as a filesystem path — concatenating it into something like keys/<kid>.pem without escaping — the attacker sets kid to ../../../etc/passwd or a path that resolves to an attacker-controlled file. If kid feeds a SQL query, it becomes SQL injection. Defense: treat kid as an opaque identifier; map it through a whitelist to actual keys.

jku/x5u SSRF. The jku (JWK Set URL) and x5u (X.509 URL) headers tell the verifier where to fetch the signing key from. A sloppy implementation fetches from whatever URL the header specifies and trusts the result. The attacker points jku at their own JWK server, returns a JWK whose public key matches a private key the attacker holds, and signs whatever they want. Defense: pin jku/x5u to an allowlist of known issuer URLs.

Missing claim validation. The signature is valid, but the verifier doesn't check aud, iss, or exp. A token issued for one audience is accepted by another (audience confusion). A token from an unknown issuer is accepted (issuer confusion). An expired token is accepted indefinitely. Defense: validate every claim that matters every time.

Replay. A captured token is replayed within its expiration window. JWT cannot prevent this on its own. Mitigations: short exp, jti tracking with denylist for sensitive operations, sender-constrained tokens via DPoP or mTLS.

Revocation: the hard tradeoff

A pure stateless JWT has no revocation path. The signature proves issuance, and that's the end of validation; the verifier never calls back to the issuer. This is the property that makes JWTs scale — but it means a compromised token is valid until exp.

The four mitigations in practice:

  1. Short expirations. Access tokens at 5-15 minutes, refresh tokens (which can be revoked at the issuer) for longer-term renewal. This is what OAuth 2.0 / OIDC defaults look like.
  2. Denylist of revoked JTIs. Give up some statelessness. The verifier checks every JTI against a small revocation set. Works when revocations are rare and the set fits in cache.
  3. Hybrid (opaque session, JWT downstream). The user-facing session is an opaque cookie checked against the issuer's session store on every request. The session controller mints short-lived JWTs for downstream services. Best of both: immediate revocation at the front door, JWT performance for service-to-service.
  4. Opaque tokens end-to-end. Skip JWT for the use case where revocation latency is unacceptable. Validation goes to the issuer every request; revocation is immediate.

The choice is covered in detail in Session Management: JWTs vs Opaque Tokens. The summary: pick the mechanism that matches your revocation tolerance and your validation-latency budget.

When not to use JWT

JWT is rarely the wrong choice for OIDC ID Tokens (it is the format the spec mandates). It is often the wrong choice for application session cookies in browser apps, and sometimes the wrong choice for API access tokens. The signals that JWT is fighting you:

  • You need immediate revocation. JWTs make this expensive.
  • You need to update token contents after issuance. JWTs are immutable; you have to mint a new one.
  • The audience is one backend, and the validation roundtrip cost is negligible. Opaque tokens are simpler.
  • The library and language ecosystem for JWT in your stack is immature. PASETO (covered separately) is a JWT-shaped alternative without the algorithm-confusion surface.
  • The token is going to a browser cookie and lifetime needs to be hours. A standard session cookie is simpler and the browser already does most of the cookie-handling work.

Implementation guidance

  1. Use a maintained library. jose (Node, modern), pyjwt or authlib (Python), microsoft.identitymodel (.NET), nimbus-jose-jwt (JVM). Avoid hand-rolled JWT code.
  2. Pin the algorithm at the verifier. Configure exactly which algorithms are accepted; reject anything else, including none.
  3. Pin keys to algorithms. If a key was generated as an RSA signing key, do not let it be used as an HMAC secret. Confused-deputy bugs hide here.
  4. Validate every claim that matters every time. iss, aud, exp, nbf, iat, and nonce for OIDC ID Tokens. Use the library's built-in validators rather than hand-rolling.
  5. Treat the header as untrusted input. kid, jku, x5u are attacker-controlled. Pin allowed values; never use them in filesystem paths or unparameterized queries.
  6. Keep payloads small. Every header sent to your backend that includes the token pays for the size. 1 KB is the rough soft limit.
  7. Plan for revocation from day one. Short exp + refresh, or hybrid opaque-then-JWT. Retrofitting revocation onto a stateless JWT scheme is painful.
  8. Use RS256 or ES256 by default, not HS256, for any token that crosses a service boundary. The asymmetric model means the verifier never needs the signing key, which simplifies key distribution and rotation.

Related vendors

FAQ

What is JWT authentication?
JWT authentication is the pattern where a server issues a signed JSON Web Token after the user (or service) proves identity, and the client then sends that JWT on subsequent requests — usually as a Bearer token in the Authorization header — to prove who it is without re-authenticating. The server validates the signature, the issuer, the audience, and the expiration on every request. JWT authentication is what OAuth 2.0 access tokens, OIDC ID Tokens, and most modern API auth schemes use under the hood. It is stateless: the server holds no session row, the token itself is the proof. That same statelessness is why JWTs are hard to revoke.
JWT vs JWT token — is there a difference?
No. JWT *is* the token (the T in JWT stands for Token), so 'JWT token' is redundant but extremely common in practice. People say it the same way they say 'PIN number' or 'ATM machine'. Treat them as identical: a compact, signed, base64url-encoded credential in the format `header.payload.signature`.
What is a JWT and what's actually in one?
A JWT (JSON Web Token, RFC 7519) is a string of three base64url-encoded parts separated by dots: header.payload.signature. The header declares the signing algorithm and key identifier; the payload contains the claims (sub, iss, aud, exp, etc., plus any custom data); the signature is computed over the header and payload using the declared algorithm. Anyone can read the contents (it is base64, not encryption); only the holder of the signing key can produce a valid signature.
What is the alg:none vulnerability?
RFC 7519 allows an unsigned JWT with alg:none. Several early JWT libraries accepted alg:none even when the verifier was configured for a real algorithm — an attacker could strip the signature, set alg:none, and the verifier would treat the token as valid. Every modern library defaults to rejecting alg:none unless explicitly allowed, but the lesson is general: pin the algorithm at the verifier, do not let the token declare its own.
What is the HS256-vs-RS256 confused-deputy bug?
A library configured to accept either symmetric (HMAC) or asymmetric (RSA) algorithms can be tricked: the legitimate signing key is the RSA private key, and the corresponding RSA public key is published. An attacker takes a token signed with the RSA private key, swaps alg to HS256, and signs the new payload using the RSA public key as the HMAC secret. A verifier that re-uses the same key material for both algorithm families validates the attacker's token. Defense: per-key allowed-algorithm pinning, or never load asymmetric public keys into the HMAC code path.
Can JWTs be revoked?
Stateless JWTs, no. The signature proves issuance, but there is no per-token database lookup to consult, so a stolen token is valid until it expires. The mitigations: short expirations (5-15 minutes for access tokens) with longer refresh tokens that can be revoked at the issuer; a denylist of explicitly-revoked JTIs (gives up some statelessness); or use opaque tokens whose validity check goes to the issuer every request. The choice depends on revocation latency tolerance.
JWT vs opaque tokens — when should I pick which?
JWTs win when validation latency matters and revocation is fine to delay — APIs called from many services, mobile clients, distributed systems where calling back to the issuer per request is expensive. Opaque tokens win when revocation has to be immediate, when you need to update token contents after issuance, or when the audience is a single backend that can cheaply check the issuer. The hybrid pattern is common: opaque session tokens in the browser, short-lived JWTs for service-to-service downstream.
What about kid header injection and jku/x5u SSRF?
The kid (key ID) header tells the verifier which key to use; if a sloppy implementation uses kid as a filesystem path or database query without escaping, it becomes a path traversal or SQL injection vector. The jku (JWK Set URL) and x5u (X.509 URL) headers tell the verifier where to fetch the signing key from; if the verifier blindly fetches from any URL the header specifies, the attacker points it at their own JWK server and signs whatever they want. Defense: do not use kid for paths or queries; pin jku/x5u to an allowlist of known issuers.

Sources

  • RFC 7519 — JSON Web Token (JWT)
  • RFC 7515 — JSON Web Signature (JWS)
  • RFC 7516 — JSON Web Encryption (JWE)
  • RFC 7518 — JSON Web Algorithms (JWA)
  • RFC 8725 — JSON Web Token Best Current Practices
  • Critical vulnerabilities in JSON Web Token libraries (Auth0, 2015) — the canonical alg:none / HS256 RSA write-up
Last reviewed 2026-05-15.