JWT and the JOSE Family
|
This section documents OAuth 2.0 (RFC 6749) as amended by the OAuth 2.0 Security Best Current Practice (RFC 9700 / BCP 240), the OAuth 1.0 Protocol (RFC 5849) for historical context, and OpenID Connect Core 1.0, as published at the IETF Datatracker and the OpenID Foundation specifications — and, on the Spring pages, Spring Boot 4.1.x and Spring Security 7.1.x as published at the Spring Security reference documentation — which are the references these pages are written and verified against. OAuth 2.1 is still an Internet-Draft ( This content was generated with the assistance of AI and should be verified against those official specifications before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
A JSON Web Token (JWT) is the format most access tokens, ID tokens and countless other bearer credentials are built from. It does not exist in isolation — it is the top of a small family of specifications, collectively known as JOSE (JSON Object Signing and Encryption), that define how to sign, encrypt and describe keys for arbitrary JSON payloads. This page covers how those specifications fit together, the algorithm choices and classic attacks, key management, and the validation checklist a resource server must implement before it can trust any of it.
The JOSE family and how it fits together
| Spec | Name | Role |
|---|---|---|
RFC 7515 |
JWS — JSON Web Signature |
Defines how to sign a JSON payload and represent the result (compact or JSON serialisation) |
RFC 7516 |
JWE — JSON Web Encryption |
Defines how to encrypt a JSON payload the same way JWS signs one |
RFC 7517 |
JWK — JSON Web Key |
A JSON representation of a cryptographic key (and JWK Set, a JSON array of keys) — how a signer publishes the public keys a verifier needs |
RFC 7518 |
JWA — JSON Web Algorithms |
The registry of algorithm identifiers ( |
RFC 7519 |
JWT — JSON Web Token |
A profile of JWS (usually) or JWE that adds a standard set of claims ( |
A JWT is, in the overwhelmingly common case, a JWS-signed JSON claims set — signed so the recipient can verify who issued it and that it has not been tampered with, but not encrypted, so its claims are readable by anyone who has the token (which is why access tokens are opaque to the client by contract, not by unreadability — see Access and Refresh Tokens). A JWT can also be JWE-encrypted when the claims themselves are sensitive to whoever might intercept the token in transit or at rest — covered under nested/encrypted JWTs below.
Compact serialisation and the three parts
The compact serialisation — the form seen on the wire almost everywhere — concatenates three base64url-encoded
segments with .:
<Header>.<Payload>.<Signature>
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImFiYzEyMyJ9.eyJpc3MiOiJodHRwczovL2FzLmV4YW1wbGUuY29tIiwic3ViIjoiYWxpY2UiLCJhdWQiOiJodHRwczovL2FwaS5leGFtcGxlLmNvbSIsImV4cCI6MTc1ODAwMDAwMCwiaWF0IjoxNzU3OTk2NDAwLCJzY29wZSI6Im9yZGVyczpyZWFkIn0.c2lnbmF0dXJlLWJ5dGVzLWhlcmU
-
Header (JOSE header) — a JSON object naming the algorithm (
alg) and key (kid), decoded:{ "alg": "RS256", "typ": "JWT", "kid": "abc123" } -
Payload — the claims set, decoded:
{ "iss": "https://as.example.com", "sub": "alice", "aud": "https://api.example.com", "exp": 1758000000, "iat": 1757996400, "scope": "orders:read" } -
Signature — raw bytes (base64url-encoded for transport) computed over
Header.Payloadwith the algorithm and key named in the header; this is what a verifier recomputes and compares to detect tampering.
Base64url encoding means the header and payload are trivially decodable by anyone — a JWT’s confidentiality comes only from JWE encryption (rare) or from transport security (TLS); its integrity comes from the signature, verified by whoever holds the right key.
Registered, public and private claims
RFC 7519 §4 divides claim names into three categories:
| Category | Meaning |
|---|---|
Registered |
Names defined by the IETF in the JWT/OIDC registries — |
Public |
Names collision-resistant by convention — either registered in the IANA JSON Web Token Claims registry, or namespaced as a URI, so unrelated parties can mint claims without stepping on each other |
Private |
Names agreed bilaterally between the issuer and a specific consumer, with no collision-resistance guarantee
outside that agreement — e.g. an internal |
A resource server should never assume a private claim from one issuer means the same thing as an identically-named claim from another.
Signing algorithms and how to choose
| Algorithm | Family | When to choose it |
|---|---|---|
|
RSA-SHA256 (asymmetric) |
The historical default; wide library support; larger keys and slower verification than EC. Reasonable when interoperability with older stacks matters. |
|
ECDSA over P-256 (asymmetric) |
Smaller keys and faster verification than RSA at an equivalent security level; the modern default for new deployments with good library support. |
|
Ed25519 (asymmetric) |
The most modern choice — deterministic signatures (no per-signature random nonce to get wrong), resistant to the implementation pitfalls that have historically hit ECDSA nonce generation. Prefer it where the whole toolchain (issuer and every verifier) supports it. |
|
HMAC-SHA256 (symmetric) |
The signer and every verifier share the same secret — appropriate only when there is exactly one verifier and it is fully trusted with the signing key. Never appropriate for a multi-resource-server deployment, and never for a public client — see algorithm confusion below. |
Prefer an asymmetric algorithm (ES256, EdDSA, or RS256) for anything a resource server verifies without
also being able to sign: it lets the authorization server keep the private key to itself and publish only the
public half via JWKS.
The classic attacks and RFC 8725’s countermeasures
RFC 8725 (BCP 225), JSON Web Token Best Current Practices, catalogues the JWT implementation mistakes that
have recurred across libraries and deployments for years. draft-ietf-oauth-rfc8725bis is currently in the RFC
Editor queue, refreshing this guidance without changing its substance.
| Attack | How it works | Countermeasure (RFC 8725) |
|---|---|---|
|
Some early JWT libraries honoured a header claiming the "none" algorithm, accepting the token with no
signature verification at all if the attacker simply set |
§3.1: a verifier MUST have an explicit allow-list of acceptable algorithms and MUST reject |
HMAC/RSA algorithm confusion |
A verifier configured with an RSA public key, if it blindly trusts the token’s |
§3.1 (same allow-list rule, scoped per-key) — a key configured for RSA verification must never be usable to
satisfy an |
Unverified |
Some implementations use the header’s |
§3.9: treat |
Missing |
A verifier that checks only the signature accepts a token that is genuinely signed by a trusted issuer but was never meant for this audience, has already expired, or came from a different, untrusted issuer entirely |
§3.10 / §3.11: |
|
|
JWKS, kid, key rotation and caching
Verifiers do not hard-code an issuer’s public key. Instead, the authorization server publishes a JWKS
(JSON Web Key Set) document — an array of JWK objects, each tagged with a kid — at a well-known URL
(typically discovered per RFC 8414, see
Discovery, Metadata and Client
Registration). A verifier:
-
Fetches and caches the JWKS, keyed by
kid. -
On each token, reads the header’s
kid, looks it up in the cached key set (never dereferencingkidas a URL itself). -
If the
kidis not found in the cache, refreshes the JWKS once (bounded, rate-limited) before failing — this is what makes key rotation transparent: the authorization server publishes the new key alongside the old one for an overlap window, signs new tokens with the new key, and verifiers pick it up on the next cache miss without any coordinated deployment. -
Respects the JWKS response’s own caching headers, but caps the cache lifetime locally too, so a compromised key can be dropped from rotation within a bounded time even if a verifier’s cache would otherwise hold it longer.
Nested and encrypted JWTs
A nested JWT is a JWT whose payload is itself another JWT (cty: JWT in the outer header) — typically a
signed JWT wrapped inside an encrypted one (JWS inside JWE), so the recipient first decrypts, then verifies
the signature of what falls out. Encryption is worth the added complexity only when the claims themselves are
sensitive to a party who can see the token in transit or at rest but should not be able to read it — for
example, a JWT routed through an intermediary that must forward it without being trusted with its contents.
Where the transport is already end-to-end TLS and the only concern is that a legitimate resource server can
authenticate the issuer, signing alone (plain JWS) is standard and sufficient — most access tokens and ID
tokens in ordinary deployments are signed, not encrypted.
RFC 9068: the at+jwt profile
Before RFC 9068, nothing standardised what claims a JWT-formatted access token should carry, so resource
servers across different stacks disagreed on how to validate one. RFC 9068 fixes this by defining the
application/at+jwt media type (asserted in the JOSE header’s typ) and a required claim set:
| Claim | Requirement under RFC 9068 |
|---|---|
|
Required — the authorization server’s issuer identifier |
|
Required |
|
Required — the resource server(s) this token is valid for |
|
Required — the resource owner (or client, for |
|
Required — the client the token was issued to |
|
Required |
|
Required — a unique token identifier, useful for revocation lists and replay logs |
|
Optional, but conventional — space-separated scope values, per RFC 6749 §3.3 |
A resource server that knows it is talking to an RFC 9068-compliant authorization server can validate against this fixed claim set instead of guessing at issuer-specific conventions.
SD-JWT in one paragraph
Selective Disclosure for JWTs (SD-JWT, RFC 9901) lets an issuer sign a JWT whose individual claims are each
independently hash-committed, and hand the holder a bundle of "disclosures" alongside it; the holder then
reveals to a given verifier only the specific disclosures needed for that interaction, while the verifier can
still confirm every revealed claim was part of the original signed set. This solves a problem plain JWTs cannot:
a plain JWT is all-or-nothing (the verifier sees every claim in the payload), whereas SD-JWT lets, for example, a
digital-credential holder prove "I am over 18" from a signed birth-date claim without disclosing the birth date
itself. draft-ietf-oauth-sd-jwt-vc builds a verifiable-credential format on top of it; neither is in
mainstream OAuth access-token use yet, but both are worth knowing about when the deployment involves digital
identity credentials rather than plain API access.
Validation checklist
Every resource server that accepts JWT-formatted tokens must implement all of the following before trusting a single claim:
| Step | What to check |
|---|---|
1. Algorithm allow-list |
|
2. Signature |
The signature verifies against the key identified by |
3. |
Matches the expected issuer for this deployment exactly (scheme, host, path) |
4. |
Contains this resource server’s own identifier — reject if it names only other audiences |
5. |
The current time is before |
6. |
Sanity-checked against an acceptable age window where the deployment cares about very old, still-unexpired tokens |
7. Required claims present |
|
8. Scope / authorization |
The token’s |
The honest limitation
Every one of those checks runs against the token as presented — and a signed JWT that passes all eight steps
is still valid until exp, full stop. A JWT cannot be revoked before it expires. If a token needs to stop
working the moment a session ends, a device is reported stolen, or a user’s access is pulled, a resource server
validating JWTs locally has no way to learn that within the token’s remaining lifetime unless something else
is added — a short exp plus a refresh cycle (see
Access and Refresh Tokens), a deny-list the resource server
also checks, or abandoning by-value tokens for the opaque/introspection model entirely. That trade-off, and the
mechanics of introspection and revocation, are the subject of the next page:
Opaque Tokens, Introspection and
Revocation.
Spring Security implements this exact validation chain — NimbusJwtDecoder, the OAuth2TokenValidator<Jwt>
pipeline (JwtTimestampValidator, JwtIssuerValidator, audience checks) and JwtAuthenticationConverter — out of the box; see
Spring Security — Building the SecurityContext from a JWT rather than reimplementing any of it by hand.