JOSE
JSON Web Tokens (JWT)
A worked decode of the RFC 7519 example token, the claims that matter, and the validation mistakes behind algorithm confusion attacks.
A JSON Web Token (JWT, RFC 7519) is a compact, URL-safe way to carry a set of claims (statements such as "issued by X", "about user Y", "valid until Z") protected by a signature, a MAC, or encryption. OpenID Connect ID tokens, RFC 9068 access tokens, DPoP proofs and private_key_jwt client assertions are all JWTs. The format is simple; validating it correctly is where the mistakes are, and RFC 8725 (BCP 225, the JWT Best Current Practices) exists because of them.
Structure
The common form is the JWS Compact Serialization: three base64url-encoded parts separated by periods. Base64url is base64 with - and _ in place of + and / and the = padding removed (RFC 7515 §2).
Encoding is not encryption. Anyone who has a signed JWT can read its header and payload. Never put secrets in a JWS payload; use JWE if the contents must be confidential.
Worked example: the RFC 7519 sample token
This is the example from RFC 7519 §3.1, decoded and checked for this page:
eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9
.
eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFt
cGxlLmNvbS9pc19yb290Ijp0cnVlfQ
.
dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
Line breaks are for display only. Base64url-decoding the first two parts gives these exact bytes (\r\n marks the CRLF line breaks the RFC deliberately includes):
Header: {"typ":"JWT",\r\n "alg":"HS256"}
Payload: {"iss":"joe",\r\n "exp":1300819380,\r\n "http://example.com/is_root":true}
alg: HS256means the third part is an HMAC-SHA256 over the ASCII stringheader.payload(the encoded forms, joined by a period).exp: 1300819380is 2011-03-22 18:43:00 UTC, so this token has long expired. A correct validator rejects it.http://example.com/is_rootis a public claim name: collision-resistant because it is a URI.- The signature verifies with the symmetric key from RFC 7515 Appendix A.1, which is where the RFC took it from. Because the key is public in the RFC, this token proves nothing to anyone.
Note that the header bytes include a CRLF and a space. The signature covers the encoded bytes exactly as sent; a verifier must never decode, re-serialize, and then check the signature over its own re-encoding. Paste the token into the JWT decoder to see the same breakdown.
Registered claims
RFC 7519 §4.1 registers seven claim names. None is mandatory in general; each application (OpenID Connect, RFC 9068, RFC 7523) decides which it requires. Times are NumericDate values: seconds since 1970-01-01T00:00:00Z.
| Claim | Meaning | Processing rule in RFC 7519 |
|---|---|---|
iss | Issuer: who created the token | Case-sensitive string or URI; processing is application-specific. |
sub | Subject: who the token is about | MUST be unique in the context of the issuer, or globally unique. |
aud | Audience: who the token is for | Each recipient MUST identify itself in it; if a present aud does not name the recipient, the JWT MUST be rejected. A string or an array. |
exp | Expiration time | MUST NOT be accepted on or after this time. Small leeway for clock skew MAY be allowed. |
nbf | Not before | MUST NOT be accepted before this time. |
iat | Issued at | Informational; lets recipients judge the token's age. |
jti | JWT ID | Unique identifier; can be used to prevent replay. |
The header has its own registered parameters, defined in RFC 7515 §4.1: alg (algorithm), kid (key ID), typ (type of the whole token), cty (content type, JWT for nested tokens), jku and x5u (URLs to fetch keys or certificates from), jwk and x5c (keys or certificates embedded in the header), x5t and x5t#S256 (certificate thumbprints), and crit.
JWS and JWE
A JWT is either a JWS (RFC 7515) or a JWE (RFC 7516):
| JWS | JWE | |
|---|---|---|
| Protects | Integrity and authenticity | Confidentiality and integrity |
| Compact parts | 3: header . payload . signature | 5: header . encrypted key . IV . ciphertext . authentication tag |
| Payload readable by holders? | Yes | Only by the holder of the decryption key |
| Proves who created it? | Yes (asymmetric) or "someone with the shared key" (MAC) | No, on its own |
| Typical use | ID tokens, access tokens, assertions | Tokens carrying personal data through untrusted parties |
When both are needed, the result is a Nested JWT: OpenID Connect requires that an encrypted ID token be signed first and then encrypted (OIDC Core §2). RFC 8725 §3.3 says every cryptographic operation in a nested JWT MUST be validated with keys and algorithms supplied by the application; some libraries have decrypted the outer JWE and then skipped verifying the inner signature (§2.3).
Algorithms, including "none"
RFC 7518 §3.1 lists the JWS algorithms and how strongly implementations must support them:
alg | Algorithm | Implementation requirement | Key |
|---|---|---|---|
HS256, HS384, HS512 | HMAC with SHA-2 | HS256 Required; others Optional | Shared secret, at least as long as the hash output (256 bits for HS256), MUST |
RS256, RS384, RS512 | RSASSA-PKCS1-v1_5 with SHA-2 | RS256 Recommended | RSA, 2048 bits or larger, MUST |
ES256, ES384, ES512 | ECDSA with P-256 / P-384 / P-521 | ES256 Recommended+ (likely to become Required) | EC key on the named curve |
PS256, PS384, PS512 | RSASSA-PSS with SHA-2 | Optional | RSA, 2048 bits or larger |
none | No signature or MAC | Optional | None |
A JWT with alg none is an "Unsecured JWS" with an empty signature. RFC 7518 §3.6 says implementations MUST NOT accept unsecured JWSs by default, and applications MUST NOT signal acceptance of them globally. RFC 8725 §3.2 allows none only when the JWT is protected end-to-end by other means, such as TLS, and says libraries SHOULD NOT produce or consume it unless the caller explicitly asks. Profiles go further: RFC 9068 access tokens MUST NOT use it, and DPoP proofs MUST NOT use none or any symmetric algorithm.
RFC 8725: the attacks and the rules
Algorithm confusion
The alg header is chosen by whoever made the token, including an attacker. RFC 8725 §2.1 describes two classic results of trusting it:
alg: none. The attacker edits the payload, setsalgtonone, and removes the signature. A library that honors the header "validates" the token without checking anything.- RS256 to HS256. The server expects RSA signatures and is configured with the issuer's RSA public key. The attacker sets
algtoHS256and computes an HMAC using that public key, which is public, as the HMAC secret. A library that picks the verification routine from the header and passes it "the key" verifies the forgery.
Libraries MUST enable the caller to specify a supported set of algorithms and MUST NOT use any other algorithms when performing cryptographic operations.
The same section adds that each key MUST be used with exactly one algorithm, checked at the time of the operation. In practice: configure the verifier with an allow-list such as ["ES256"], and store keys as typed objects (an RSA public key cannot be fed to an HMAC routine) rather than raw byte strings.
The other rules
- Use current algorithms (§3.2): applications MUST only allow cryptographically current algorithms, and MUST be designed for algorithm agility.
- Validate every cryptographic operation (§3.3): reject the whole JWT if any fails, including the inner layer of a nested JWT.
- Use real keys (§3.5): human-memorable passwords MUST NOT be used directly as HMAC keys. A short HS256 secret can be brute-forced offline from any one token (§2.2).
- UTF-8 only (§3.7): MUST.
- Validate issuer and subject (§3.8): when there is an
iss, the application MUST check that the keys used belong to that issuer, and MUST check thatsub(or the issuer-subject pair) is valid for it. A token signed by issuer A's key but claiming"iss": "B"must fail. - Use and validate audience (§3.9): if one issuer issues JWTs for more than one relying party, the JWT MUST contain
audand each recipient MUST reject a token whose audience is missing or not itself. This stops a token for one service being replayed at another (§2.7). - Do not trust received header values (§3.10):
kidis attacker-controlled input to a key lookup, so validate or sanitize it (the RFC warns of SQL and LDAP injection through it). Blindly fetching ajkuorx5uURL enables server-side request forgery and lets the attacker supply their own key; applications SHOULD match such URLs against an allow-list. Better still, ignore these headers and use keys configured from the issuer's metadata. OpenID Connect says ID tokens SHOULD NOT usex5u,x5c,jkuorjwkat all. - Use explicit typing (§3.11, RECOMMENDED for new JWT uses): set
typto a specific media type, such asat+jwt(RFC 9068) ordpop+jwt(RFC 9449), and check it. - Mutually exclusive validation rules (§3.12): if one issuer issues several kinds of JWT, the rules MUST be written so that each kind is rejected by the others' validators, using distinct
typ, required claims, keys, audiences or issuers.
Keys and JWKS
Issuers publish their public keys as a JWK Set (RFC 7517 §5): a JSON object with a keys array. The URL comes from trusted metadata, jwks_uri in RFC 8414 or OpenID Connect Discovery, never from the token. An illustrative set with one P-256 key (the public key used in the RFC 9449 examples):
{
"keys": [
{
"kty": "EC",
"crv": "P-256",
"x": "l8tFrhx-34tV3hRICRDY9zCkDlpBhF42UQUfWVAWBFs",
"y": "9VE4jf_Ok_o64zbTTlcuNJajHmt6v9TDVrU0CdvGRDA",
"use": "sig",
"alg": "ES256",
"kid": "2026-10-a"
}
]
}
The verifier picks the key whose kid matches the token header, confirms its alg is on the allow-list, and verifies. Key rotation works by publishing the new key alongside the old one before signing with it, then removing the old key after tokens signed with it have expired. Common practice is to cache the JWKS and refresh it when an unknown kid appears, with a rate limit so attackers cannot force constant refetching.
Validation checklist
- Reject anything that is not well-formed compact serialization with the expected number of parts.
- Check
typagainst the kind of token you expect (when the profile defines one). - Check
algagainst your allow-list; never acceptnoneunless your profile explicitly allows it. - Select the key from your configured issuer's JWKS by
kid; ignorejku,x5uand embedded keys unless your profile requires them (as DPoP does forjwk). - Verify the signature over the received encoded bytes.
- Check
issexactly matches the expected issuer, and that the key belongs to it. - Check
audcontains your identifier. - Check
expandnbfwith a small leeway; consider rejecting tokens with implausibleiat. - Check the profile's required claims (
sub,nonce,scope,jtifor replay) and only then use the claims.
Use a maintained library for all of this, configured with explicit algorithms, issuer and audience, rather than hand-written parsing. For profile-specific rules see access tokens and ID token validation.
Sources
- RFC 7519: JSON Web Token (JWT), IETF. IETF Trust Legal Provisions (brief quotations with attribution).
- RFC 7515: JSON Web Signature (JWS), IETF. IETF Trust Legal Provisions (brief quotations with attribution).
- RFC 7516: JSON Web Encryption (JWE), IETF. IETF Trust Legal Provisions (brief quotations with attribution).
- RFC 7517: JSON Web Key (JWK), IETF. IETF Trust Legal Provisions (brief quotations with attribution).
- RFC 7518: JSON Web Algorithms (JWA), IETF. IETF Trust Legal Provisions (brief quotations with attribution).
- RFC 8725: JSON Web Token Best Current Practices, IETF. IETF Trust Legal Provisions (brief quotations with attribution).
- RFC 9068: JSON Web Token (JWT) Profile for OAuth 2.0 Access Tokens, IETF. IETF Trust Legal Provisions (brief quotations with attribution).