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 (draft-ietf-oauth-v2-1-16, 3 September 2026) and is flagged as such everywhere it appears on these pages. It is a working-group consolidation in progress, not a published standard; nothing here should be read as saying otherwise.

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 (RS256, ES256, HS256, …​) usable in JWS and JWE headers

RFC 7519

JWT — JSON Web Token

A profile of JWS (usually) or JWE that adds a standard set of claims (iss, sub, exp, …​) for representing "a set of claims as a JSON object", most often used as a bearer credential

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.Payload with 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.

A decoded JWT: the base64url-encoded

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 — iss, sub, aud, exp, nbf, iat, jti (JWT) plus nonce, auth_time, acr, amr, azp (OIDC) — interoperable by construction; every implementation understands them the same way

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 department or tenant_id claim meaningful only inside one organization

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

RS256

RSA-SHA256 (asymmetric)

The historical default; wide library support; larger keys and slower verification than EC. Reasonable when interoperability with older stacks matters.

ES256

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.

EdDSA

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.

HS256

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)

alg: none

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 alg to none and left the signature segment empty

§3.1: a verifier MUST have an explicit allow-list of acceptable algorithms and MUST reject none and any algorithm not on that list — never derive the verification algorithm from the token’s own header

HMAC/RSA algorithm confusion

A verifier configured with an RSA public key, if it blindly trusts the token’s alg header, can be tricked into treating that public key (which is, after all, public) as an HMAC secret — an attacker who knows the public key can then forge an HS256-signed token the verifier accepts

§3.1 (same allow-list rule, scoped per-key) — a key configured for RSA verification must never be usable to satisfy an HS256 check; the algorithm expected for a given key must be fixed by the verifier’s own configuration, not read from the token

Unverified kid (path traversal / injection)

Some implementations use the header’s kid to build a file path or database lookup key directly (e.g. keys/{kid}.pem) without sanitizing it — an attacker-controlled kid containing ../ or SQL/LDAP metacharacters can then make the verifier load an attacker-chosen key

§3.9: treat kid as an untrusted index into a known, pre-populated set of trusted keys (a JWKS document fetched from a trusted issuer endpoint) — never as a path, filename or query fragment built by string concatenation

Missing aud / iss / exp checks

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: iss, aud and exp (and nbf where present) MUST be checked on every validation, not just the signature

alg: none and algorithm confusion are not theoretical — both have had real CVEs against mainstream JWT libraries. The correct posture is: configure the verifier with an explicit expected algorithm and an explicit expected key (or key source) per issuer, and never let the token’s own header pick either for you.

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:

  1. Fetches and caches the JWKS, keyed by kid.

  2. On each token, reads the header’s kid, looks it up in the cached key set (never dereferencing kid as a URL itself).

  3. If the kid is 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.

  4. 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

iss

Required — the authorization server’s issuer identifier

exp

Required

aud

Required — the resource server(s) this token is valid for

sub

Required — the resource owner (or client, for client_credentials) the token represents

client_id

Required — the client the token was issued to

iat

Required

jti

Required — a unique token identifier, useful for revocation lists and replay logs

scope

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

alg is on an explicit, per-issuer allow-list configured by the verifier; none and any HMAC algorithm paired with an asymmetric key are always rejected

2. Signature

The signature verifies against the key identified by kid, looked up in a cached, trusted JWKS — never a key or URL taken from the token itself

3. iss

Matches the expected issuer for this deployment exactly (scheme, host, path)

4. aud

Contains this resource server’s own identifier — reject if it names only other audiences

5. exp / nbf

The current time is before exp and not before nbf (allowing only a small, deliberate clock-skew tolerance)

6. iat

Sanity-checked against an acceptable age window where the deployment cares about very old, still-unexpired tokens

7. Required claims present

sub, client_id, jti and any deployment-specific required claim are present and well-formed (RFC 9068 claim set, if that profile is in use)

8. Scope / authorization

The token’s scope (or other authorization claims) actually covers the operation being requested — signature validity is necessary, not sufficient, for authorization

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.

JWT validation pipeline

flowchart TB tok["Incoming JWT\n(compact serialisation)"] --> hdr["Parse header:\nread alg and kid"] hdr --> allow{"alg on the\nverifier's allow-list?"} allow -- "no, or none" --> reject["Reject token"] allow -- yes --> jwks["Resolve key by kid\nfrom cached, trusted JWKS"] jwks --> found{"kid found\nin cache?"} found -- no --> refresh["Refresh JWKS once\n(bounded, rate-limited)"] refresh --> found2{"kid found\nnow?"} found2 -- no --> reject found2 -- yes --> sig found -- yes --> sig["Verify signature\nwith resolved key"] sig -- invalid --> reject sig -- valid --> claims["Check iss, aud, exp, nbf,\niat, required claims"] claims -- fail --> reject claims -- pass --> scope["Check scope / authorization\nfor the requested operation"] scope -- insufficient --> deny["403: insufficient scope"] scope -- sufficient --> accept["Accept: build Authentication"]