JSON Web Tokens (JWTs, pronounced "jot") have become the default choice for stateless authentication in REST APIs and single-page applications. They allow a server to issue a self-contained token that clients can present on every request without the server needing to query a session store. This is genuinely useful architecture — but JWTs have a history of subtle security vulnerabilities that every developer working with them should understand.
The Structure of a JWT
A JWT is three Base64url-encoded segments separated by dots: header.payload.signature.
Header: A JSON object that declares the token type ("JWT") and the signing algorithm, for example {"alg": "HS256", "typ": "JWT"}. The algorithm field is critical — as you will see shortly, it has been the source of major vulnerabilities.
Payload: A JSON object containing claims. "Claims" are statements about the entity (typically the user) and additional metadata. There are registered claim names with standard semantics: iss (issuer), sub (subject, usually a user ID), aud (audience), exp (expiration time as a UNIX timestamp), and iat (issued at). Custom claims can be added for application-specific data like roles or permissions.
Signature: Computed by taking the encoded header, a period, the encoded payload, and signing the combined string with the algorithm and key specified in the header. For HMAC-SHA256 (HS256), this is a symmetric operation using a shared secret. For RS256, it uses a private RSA key, with verification done by the corresponding public key.
Symmetric vs. Asymmetric Signing
HS256 (HMAC-SHA256) uses a single shared secret for both signing and verification. This is simpler to set up but requires every service that validates tokens to have access to the secret — which creates key distribution challenges in distributed systems.
RS256 (RSA with SHA-256) and ES256 (ECDSA with SHA-256) use asymmetric key pairs. The issuing service signs with a private key; any service needing to verify a token only needs the public key. This is easier to distribute safely and enables scenarios like a third-party OAuth provider issuing tokens verified by your own service.
For most single-service deployments, HS256 with a long, random secret is perfectly adequate. For microservices or third-party integrations, RS256 or ES256 is more appropriate.
The "Algorithm None" Vulnerability
In 2015, researchers discovered that many JWT libraries accepted the algorithm value none in the header, which means the token carries no signature at all. An attacker could forge any claims they wanted by setting {"alg": "none"} and omitting the signature segment. The vulnerability was trivially exploitable against any library that did not explicitly reject the none algorithm.
The lesson: when verifying JWTs, always specify the expected algorithm explicitly. Never accept the algorithm declared in the token header as authoritative — an attacker controls that field.
The Algorithm Confusion Attack
A related attack targets systems that support both HS256 and RS256. If the server is configured with an RSA public key and expects RS256, an attacker can take that public key (which is, by definition, public) and use it as the secret in an HS256 signature. If the library blindly trusts the algorithm in the header, it might validate the forged token using the RSA public key as an HMAC secret.
Defense: pin the accepted algorithm to exactly what you expect, and reject tokens whose header declares a different one.
Expiration and Revocation
The exp claim sets a hard expiration time. After this timestamp, a well-implemented validator will reject the token. Setting short expiration times (15–60 minutes) limits the damage window if a token is stolen.
The fundamental limitation of JWTs is that they cannot be revoked before expiration without server-side state. This is a deliberate trade-off: the whole point of a JWT is that verification requires no database query. If you need instant revocation (for logout, account suspension, or compromised-credentials scenarios), you must either use short-lived tokens plus a refresh token mechanism, or maintain a token blocklist — which reintroduces server state.
What Should Not Go in a JWT Payload
The JWT payload is signed, not encrypted. Anyone who holds the token can decode and read the payload — it is just Base64url-encoded. Never include sensitive data like passwords, credit card numbers, or personally identifying information that should not be visible to the client. The signature only guarantees that the payload was not tampered with, not that it is confidential.
If you need a confidential token payload, use JWE (JSON Web Encryption) instead of (or wrapped around) a standard JWT.
Decoding JWTs for Debugging
During development, being able to quickly decode a JWT to inspect its claims and check expiration is invaluable. A browser-based decoder processes the token entirely locally — no network request, no risk of the token being logged on a remote server. This is especially important for tokens that carry authorization claims, since exposing them to a third-party service could be a security issue in itself.