In May 2015 the IETF published RFC 7519, which defines the format of JSON Web Tokens (JWT). Together with RFC 7515โ7518, released the same month, it closes the work of the JOSE (JavaScript Object Signing and Encryption) group and gives a normative reference to a format that libraries had already been implementing for some time off the Internet-Draft series.
Context
A JWT carries a set of statements โ the claims โ in a compact, URL-safe form. The three parts โ header, payload, signature โ are base64url-encoded and separated by a dot:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjMiLCJleHAiOjE0MzQwMzM2MDB9.<signature>
The header declares the algorithm (alg) and the type (typ). The payload holds the claims. The signature covers the first two segments concatenated. A signed JWT is a JWS structure (RFC 7515); an encrypted JWT is a JWE structure (RFC 7516). The former protects integrity but leaves the payload readable โ anyone can base64url-decode it โ while the latter protects confidentiality as well.
The family breaks down like this:
RFC 7515โ JWS, signatures and MACsRFC 7516โ JWE, encryptionRFC 7517โ JWK, key representationRFC 7518โ JWA, algorithms and identifiersRFC 7519โ JWT, the claims container
The editors of RFC 7519 are Michael B. Jones, John Bradley and Nat Sakimura.
Registered claims
Section 4.1 of the standard reserves a small set of names with fixed semantics. They are all optional, but their meaning is normative:
issโ who issued the tokensubโ the subject it refers toaudโ the intended recipientexpโ expiry instant (seconds since epoch)nbfโ not valid before this instantiatโ issuance instantjtiโ a unique token identifier
Having them in the standard, rather than in conventions that differed from library to library, is the most concrete benefit: exp means the same thing everywhere, and a conformant validator must reject an expired token. Every other payload field stays free.
Algorithms
RFC 7518 defines the algorithm identifiers used in the header. The three main groups for signing:
HS256/HS384/HS512โ HMAC with a shared symmetric keyRS256/RS384/RS512โ RSA asymmetric signaturesES256/ES384/ES512โ ECDSA over elliptic curvesPS256/PS384/PS512โ RSA-PSS
And then none, defined for unsecured JWTs: no signature, an empty final segment. It serves cases where integrity is guaranteed by another layer of the transport. It is also the first mistake an implementer makes, as we will see.
The critical point
The standard describes a sound format; its implementations, in early 2015, were not. On 31 March Tim McLean published a review of the code of the most widely used libraries โ node-jsonwebtoken, pyjwt, php-jwt, namshi/jose and others โ documenting two classes of flaw present in their default behaviour.
The first concerns alg: none. Several libraries accepted as valid a token with the header {"alg":"none"} and an empty signature segment, and the verification routine returned true. Anyone could then build a token with whatever payload they wanted โ arbitrary sub, exp far in the future โ and have it pass as authentic. The cause is an API choice: the verify function read the algorithm from the token header, that is, from a field the attacker controls.
The second is the confusion between asymmetric and symmetric algorithms. A library that handled both HMAC and RSA would verify an HS256 token with whatever key it was handed. If the application expected RS256 and passed its own RSA public key to verify, an attacker could sign an HS256 token using that same public key as the HMAC secret. A public key is public by definition: the attacker holds it. Verification succeeded because the library treated the bytes of the RSA key as a symmetric secret.
The two flaws share the same root: letting the token declare how it should be verified. The fix is to move the decision to the application and take it away from the token. verify must be given the list of expected algorithms and reject everything else, none included.
Implications
For anyone issuing and verifying JWTs from here on:
- Pin verification to an algorithm, or a closed list, decided by the code and never by the token header.
- Reject
noneexplicitly, unless the flow calls for it for a specific reason. - Always validate
expand, when present,nbf; checkissandaudagainst the expected values. - Keep
expshort. A signed JWT cannot be revoked before expiry without reintroducing server-side state (a revocation list, invalidatedjtiidentifiers): the stateless nature is the formatโs advantage and also its operational limit. - Do not mistake the signature for confidentiality: a JWS is readable. Personal data in the payload calls for a JWE, or it stays out of the token.
Limits
RFC 7519 standardises the container, not the use. Which claims to make mandatory for a given case, how keys are rotated, where the token is stored on the client โ a JWT in localStorage is exposed to hostile scripts differently from a session cookie โ remain application decisions. The standard makes the format interoperable and gives a common vocabulary; the two classes of bug from March 2015 show that the fragile part lies elsewhere, in the verification API that libraries offer.
- https://www.rfc-editor.org/rfc/rfc7519.html
- https://www.rfc-editor.org/rfc/rfc7515.html
- https://www.rfc-editor.org/rfc/rfc7518.html
- https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/
- https://www.noze.it/en/insights/jwt-rfc-7519/
Cover image: Schematic diagram of a digital signature: on the left the sender hashes the message and signs it with the private key; on the rightโฆ โ diagram by Acdx, CC BY-SA 3.0 โ https://commons.wikimedia.org/wiki/File:Digital_Signature_diagram.svg