JSON Web Tokens: Header, Claims, and Signature
Table of Contents
A JWT is a set of claims carried inside a JOSE container. In the common
case that container is a JWS in compact serialization RFC 7519,
RFC 7515: three base64url segments joined by dots,
header.payload.signature. An encrypted JWT (JWE, RFC 7516) has five
segments instead. The algorithms are registered separately in
RFC 7518 (JWA).
The 2018 version of this page was a header block and three empty headings. The header is kept below as the snapshot; everything else was filled in later and has been corrected.
1. JSON Web Token
earlier example token carried jwt.io's "John Doe" signature on an "Alice" payload; it verified under no key. Replaced with a token generated and checked below.
1.1. Header
The JOSE header names the algorithm. alg is the only required member for a
JWS; typ is optional, and RFC 7519 §5.1 recommends "JWT" when present.
This was the 2018 default:
{
"typ": "JWT",
"alg": "HS256"
}
The registered values that matter in practice:
alg |
Construction | Key |
|---|---|---|
HS256 |
HMAC with SHA-256 (a MAC) |
shared secret, ≥256 bits |
RS256 |
RSASSA-PKCS1-v1_5 with SHA-256 |
RSA key pair |
ES256 |
ECDSA, P-256, SHA-256 |
EC key pair |
EdDSA |
Ed25519 (RFC 8037) | Ed25519 key pair |
none |
unsecured; empty signature | none |
1.2. Claims Set (Payload)
The payload is a JSON object of claims. Base64url is an encoding, not encryption: anyone holding the token reads the claims.
{
"sub": "1234567890",
"name": "Alice",
"email": "alice@example.com",
"iat": 1516239022,
"exp": 1516242622,
"roles": ["user", "admin"]
}
name, email and roles are private claims. The registered ones
(RFC 7519 §4.1) are all optional; profiles decide which are required.
Times are NumericDate, in seconds since the epoch.
1.2.1. Registered Claims
| Claim | Description |
|---|---|
iss |
Issuer |
sub |
Subject |
aud |
Audience; a verifier must reject a token not for it |
exp |
Expiration time |
nbf |
Not before |
iat |
Issued at |
jti |
JWT ID; unique per token, used for replay/revocation |
1.3. Signature
The JWS signing input is the two encoded segments joined by a dot, without base64 padding:
HMAC-SHA256(
key,
ASCII(BASE64URL(header) || "." || BASE64URL(payload))
)
For HS256 the "signature" is a MAC. Every party that can verify a token can
also mint one, so a shared secret only fits when issuer and verifier are the
same service. RFC 7518 §3.2 sets the key floor: "A key of the same size as
the hash output (for instance, 256 bits for "HS256") or larger MUST be used."
2. Worked example
block run locally; output pasted verbatim
Sign, decode, and verify with nothing but node:crypto. The last line is the
2015 failure mode in miniature: a token with alg: none and an empty
signature, which a verifier that trusts the header would accept.
const { createHmac, timingSafeEqual } = require('node:crypto');
const b64u = (obj) => Buffer.from(JSON.stringify(obj)).toString('base64url');
const hs256 = (input, key) => createHmac('sha256', key).update(input).digest('base64url');
const key = 'k'.repeat(32); // 256 bits
const header = { alg: 'HS256', typ: 'JWT' };
const claims = { sub: '1234567890', name: 'Alice', iat: 1516239022 };
const signingInput = `${b64u(header)}.${b64u(claims)}`;
const token = `${signingInput}.${hs256(signingInput, key)}`;
console.log(token);
// Decode: no key needed. The payload is encoded, not encrypted.
const [h, p] = token.split('.');
console.log(JSON.parse(Buffer.from(h, 'base64url')), JSON.parse(Buffer.from(p, 'base64url')));
// Verify with the algorithm pinned by the verifier, not read from the header.
const verify = (tok) => {
const [h, p, s] = tok.split('.');
if (JSON.parse(Buffer.from(h, 'base64url')).alg !== 'HS256') return false;
const expected = Buffer.from(hs256(`${h}.${p}`, key));
const given = Buffer.from(s ?? '');
return expected.length === given.length && timingSafeEqual(expected, given);
};
// The 2015 failure: a verifier that trusts the header's alg accepts this.
const forged = `${b64u({ alg: 'none', typ: 'JWT' })}.${b64u({ ...claims, name: 'Mallory' })}.`;
console.log({ genuine: verify(token), forged: verify(forged) });
The first two segments match any HS256 token with these claims; the third depends on the key. Paste the token into a debugger with the wrong key and it still decodes, which is the point.
3. Implementation
both snippets run: PyJWT 2.15.1 (uv), jsonwebtoken 9.0.3 (node v26.2.0); both print user123
Both libraries accept an allow-list of algorithms. Pass it every time.
3.1. Python
from datetime import datetime, timedelta, timezone
import jwt # PyJWT
secret = "k" * 32 # 256-bit minimum for HS256 (RFC 7518 §3.2)
token = jwt.encode(
{"sub": "user123", "exp": datetime.now(timezone.utc) + timedelta(minutes=15)},
secret,
algorithm="HS256",
)
claims = jwt.decode(token, secret, algorithms=["HS256"])
datetime.utcnow() is deprecated as of Python 3.12; the aware form above
replaces it. PyJWT 2.x makes algorithms mandatory on decode.
3.2. JavaScript
const jwt = require('jsonwebtoken');
const secret = 'k'.repeat(32);
const token = jwt.sign({ sub: 'user123' }, secret, { algorithm: 'HS256', expiresIn: '15m' });
const claims = jwt.verify(token, secret, { algorithms: ['HS256'] });
4. Security Considerations
The short list, each item traceable to RFC 8725:
- Pin the accepted algorithms on the verifier; never take
algfrom the token. - Validate
exp,nbf,issandaudas well as the signature. - HS256 keys of at least 256 bits drawn from a CSPRNG.
- Asymmetric algorithms (
ES256,EdDSA,RS256) when issuer and verifier differ. - Nothing secret in the payload unless the token is a JWE.
- Use
typ(for exampleat+jwt, RFC 9068) so one kind of JWT cannot be replayed as another.
5. Since then
As of 2026-10, JWT is still the default wire format for OAuth access tokens, OIDC ID tokens, and most workload credentials. What changed is how much of it a verifier is expected to distrust.
5.1. Algorithm confusion and alg: none
RFC 8725 §2.1 and §3.1 checked; CVE-2015-9235 = npm jsonwebtoken < 4.2.2 per GHSA-c7hr-j4mj-j2w6
Letting the token choose its own verification algorithm was the original
design mistake. Two attacks follow. Set alg to none and strip the
signature; libraries that honoured the header skipped verification. Or take
a service that verifies RS256, switch the header to HS256, and MAC the
token with the server's public key; a library that passes "the key" to
whichever algorithm the header names will accept it. Both were disclosed
together in 2015 and hit most major libraries, among them node
jsonwebtoken before 4.2.2 (CVE-2015-9235). Variants kept turning up for
years afterwards.
5.2. RFC 8725, the JWT BCP
RFC 8725 (BCP 225, February 2020; Sheffer, Hardt, Jones) is the document the 2018 page needed. It requires that "the library MUST ensure that the 'alg' or 'enc' header specifies the same algorithm that is used for the cryptographic operation", and adds audience validation, explicit typing, and mutually exclusive validation rules for different kinds of JWT issued by the same party. If one link from this page gets read, it is this one.
5.3. PASETO
PASETO (Arciszewski, 2018) removes the choice outright: a version
(v3, v4) fixes one cipher suite per purpose (local is symmetric AEAD,
public is a signature) and there is no alg header to forge. It solved
the agility problem and lost on adoption. Its IETF draft expired in 2022
without becoming an RFC, and the OAuth/OIDC ecosystem stayed on JWT with the
RFC 8725 rules bolted on.
5.4. Short-lived tokens versus sessions
A self-contained token cannot be revoked without reintroducing the server
state it was supposed to remove. The working answer is not "JWT everywhere":
browser sessions stay as opaque cookies, and JWT access tokens live for
minutes, backed by a revocable refresh token or introspection
(RFC 7662). A jti deny-list until expiry, as in the Laravel JWT Auth
design in Token-Based Authentication in Web Frameworks, is the same
trade-off made explicit.
5.5. Sender-constrained tokens: mTLS and DPoP
RFC 9449 (2023, Fett et al.) binds the token to the JWK SHA-256 thumbprint of the client key via cnf
A bearer token works for whoever holds it. Sender-constrained tokens carry a
cnf claim tying them to a key the client must prove it holds on each
request: a TLS client certificate (RFC 8705, 2020) or an
application-layer proof JWT (RFC 9449, DPoP, 2023). The OAuth 2.0 Security
BCP (RFC 9700, 2025) recommends sender-constraining over plain bearer
tokens. JWT is now as often the proof as the credential.
5.6. Selective disclosure
SD-JWT (RFC 9901, November 2025) lets a holder reveal a subset of signed claims by replacing each with a salted digest. It is the JWT-shaped answer to the "everything in the payload is readable" problem; SD-JWT VC is one of the two credential formats in the EU digital identity wallet architecture, alongside ISO mdoc.
5.7. Workload and agent identity
The newest use is identity for software rather than people. SPIFFE issues
JWT-SVIDs to workloads; the IETF WIMSE working group is standardising
JOSE-based tokens and token exchange between workloads, building on
OAuth Token Exchange (RFC 8693) and its act claim for delegation.
AI agents inherit all of it: short-lived, audience-bound, sender-constrained
tokens in place of long-lived API keys, with the delegation chain recorded in
the token. The landscape is in
Bot and Agent Trust Management Software: The 2026 Landscape; the
attribution side, where the unsigned analogue is a git trailer, is in
Agent Identity and Attestation.
What held up from 2018: the three-part structure, HS256 for single-service tokens, and the debugger at jwt.io. What did not: trusting the header, long-lived bearer tokens, and the idea that stateless meant no state.
6. Tools
7. Related
- Token-Based Authentication in Web Frameworks – JWT Auth (Laravel) versus Devise Token Auth (Rails): token lifecycle, revocation, storage
- Bot and Agent Trust Management Software: The 2026 Landscape – short-lived agent tokens, Cross App Access, non-human identity
- Agent Identity and Attestation – attribution and delegation for agents; cites Agentic JWT as a formal analogue
- Blockchain for Identity and Lending – self-sovereign identity, the verifiable-credential branch of the same problem