
Anatomy of a JWT: Header, Payload, and Signature
A technical breakdown of JSON Web Tokens (JWT) structure, explaining the header, payload, and signature components as defined in RFC 7519.
A JSON Web Token (JWT) is often misunderstood as a secure vault for user data. It is not. A JWT is a compact, URL-safe means of representing claims to be transferred between two parties. The security of a JWT does not come from hiding the data—it comes from the cryptographic signature that guarantees the data has not been altered since issuance. To understand JWTs, we must strip away the abstraction and look at the raw bytes, the encoding layers, and the signature verification mechanism defined in RFC 7519 and its companion RFCs.
The Header: Declaring the Algorithm
The first component of a JWT is the Header. This is a JSON object that describes the cryptographic operations applied to the token. The two most critical fields are alg and typ.
{
"alg": "HS256",
"typ": "JWT"
}This structure is standardized by RFC 7515 (JSON Web Signature) to ensure interoperability across different systems.
The alg field specifies the signing algorithm. For HMAC-based tokens, this might be HS256 (HMAC using SHA-256). For asymmetric tokens, it might be RS256 (RSA Signature with SHA-256). This field is critical because it tells the verifier how to validate the signature. If an attacker can manipulate the alg field, they may bypass signature verification entirely. For example, if the server accepts alg: none, it treats the token as unsigned, allowing anyone to forge claims. This vulnerability allows attackers to bypass the jwt signature verification entirely. This vulnerability is known as the "alg: none" attack.
The typ field is typically set to JWT to indicate the media type. While often ignored by validators, it serves as a hint to parsers about the expected format.
The Payload: Claims and Context
The second component is the Payload, which contains the claims. Claims are statements about an entity (typically, the user) and additional data. The JSON object in the payload should be small; JWTs are not designed to carry large datasets.
{
"sub": "1234567890",
"name": "John Doe",
"admin": true,
"iat": 1516239022
}RFC 7519 defines several "registered claim names" that have special meaning:
sub(Subject): The principal that is the subject of the JWT. Usually a user ID.iss(Issuer): The principal that issued the JWT.aud(Audience): The recipients that the JWT is intended for. A server should reject tokens whereauddoes not match its client ID.exp(Expiration Time): Defines the expiration time on or after which the JWT must not be accepted.nbf(Not Before): Defines the time before which the JWT must not be accepted.iat(Issued At): Identifies the time at which the JWT was issued.
These definitions are mandated by rfc 7519 to ensure consistent interpretation.
While these fields have standardized names, the payload is otherwise free-form. You can add custom claims like role, permissions, or tenant_id. However, because the payload is only Base64URL encoded (not encrypted), anyone with access to the token can read its contents. Never store secrets (like passwords or credit card numbers) in a JWT.
Base64url Encoding: From JSON to URL-Safe Strings
Before the Header and Payload are combined, they are serialized to JSON and then encoded using Base64url. This is a variation of Base64 encoding that is safe for use in URLs, cookies, and query parameters.
Standard Base64 uses characters like + and /, which have special meanings in URLs. It also uses = for padding. Base64url replaces these characters to ensure the token can be passed safely in HTTP headers or URL parameters without encoding issues:
+becomes-/becomes_=padding is omitted (since it's not needed for decoding in this context)
This specific base64url encoding scheme prevents URL parsing errors.
This encoding is applied independently to the Header and the Payload. The resulting strings are concatenated with a period (.) separator.
Base64UrlEncode(Header) + "." + Base64UrlEncode(Payload)
For example, the header { "alg": "HS256", "typ": "JWT" } becomes eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
The Signature: Ensuring Integrity
As defined in RFC 7515 (JSON Web Signature), the third component is the Signature. This is the part that ensures the token hasn't been tampered with. The signature is created by taking the encoded Header, a period, and the encoded Payload, and then applying the algorithm specified in the Header.
For an HMAC-based token (like HS256), the process is:
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
For an RSA-based token (like RS256), the process uses the private key:
RSA-SHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
privateKey
)
The result is then Base64URL encoded and appended to the string with another period.
The final JWT looks like this:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
When a server receives this token, it extracts the Header, Payload, and Signature. It re-computes the signature using the same algorithm and secret/key. If the computed signature matches the provided signature, the token is valid. If even one character in the Header or Payload is changed, the signature will no longer match, and the token will be rejected.
This mechanism ensures integrity but not confidentiality. Anyone can decode the Header and Payload to read the claims. Only the signature prevents modification.
Common Pitfalls and Best Practices
Understanding the anatomy of web tokens, specifically JWTs, reveals several common security pitfalls:
-
Algorithm Confusion: If a server supports both symmetric (HMAC) and asymmetric (RSA) algorithms, an attacker might change the
algheader fromRS256toHS256and sign the token with the public key (which is known). Since HMAC uses a shared secret, and the public key is often public, this can lead to signature forgery. Always restrict the allowed algorithms on the server side. -
Clock Skew: The
expclaim relies on the server's clock. If the server's clock is out of sync with the client's, valid tokens might be rejected. Implement a small tolerance window (e.g., ±30 seconds) when validatingexpandnbf. -
Opaque Tokens: Treat JWTs as opaque strings when passing them between services. Do not assume the payload is trustworthy until the signature is verified. Once verified, you can trust the claims. This applies to all web tokens, not just JWTs.
-
Size Limits: JWTs can become large if many claims are added. This increases the size of HTTP headers, which can cause issues with some proxies and browsers that have header size limits. Keep the payload minimal.
Conclusion
By understanding the precise structure and mechanics of JWTs, developers can implement authentication systems that are both secure and interoperable. Remember: the signature is the only thing that matters for security. The rest is just data.
Related posts
Reactive Security: WebFlux & JwtAuthenticationToken
Explore WebFlux security patterns using ReactiveSecurityContextHolder and JwtAuthenticationToken for non-blocking authentication.
OAuth 2.0 vs JWT: Understanding the Relationship
An examination of the relationship between OAuth 2.0 and JSON Web Tokens, covering opaque tokens, token format selection, and JWT best practices.
Magic Links: Design, Threats, and Session Binding
An examination of magic link authentication design, security threats, and session binding techniques for backend developers.