Skip to content

JOSE and JWT (JSON Web Tokens)

Signed or encrypted JSON small enough for a URL or an HTTP header: OpenID Connect ID tokens, OAuth access tokens, ACME requests. There is no handshake. Each token names its own algorithm in a small header, so the receiver, not the sender, has to decide what it accepts.

Standard: JWS (RFC 7515), JWE (RFC 7516), JWK (RFC 7517), JWA (RFC 7518) and JWT (RFC 7519), all May 2015; Ed25519 and X25519 keys in RFC 8037 (2017); best practices in RFC 8725 (2020); fully specified names such as Ed25519 in RFC 9864 (2025); ML-DSA in RFC 9964 (2026). Implementations: panva's jose and Auth0's jsonwebtoken (JavaScript), PyJWT, joserfc and jwcrypto (Python), Nimbus JOSE+JWT (Java), go-jose (Go), jsonwebtoken (Rust), Microsoft.IdentityModel (.NET).

One token, one algorithm per category

Real output from jose 13, latchset's C tool (the Ubuntu 24.04 package), in a Linux container, Oct 2026. The inputs are the RFCs' own examples, with the keys the RFCs print: the signed JWT of RFC 7519 §3.1 (the same token as RFC 7515 Appendix A.1) and the encrypted message of RFC 7516 Appendix A.1.

$ cut -d. -f1 rfc7519.jwt | jose b64 dec -i-
{"typ":"JWT",
 "alg":"HS256"}
$ jose jws ver -i rfc7519.jwt -k rfc7515-a1.jwk -O-
{"iss":"joe",
 "exp":1300819380,
 "http://example.com/is_root":true}
$ cut -d. -f1 rfc7516-a1.jwe | jose b64 dec -i-
{"alg":"RSA-OAEP","enc":"A256GCM"}
$ jose jwe dec -i rfc7516-a1.jwe -k rfc7516-a1.jwk
The true sign of intelligence is not knowledge but imagination.
JOSE name Category Algorithm inside
HS256 MAC HMAC-SHA-256 over header.payload as sent (still base64url); the 32-byte tag is the third part
RS256, PS256 Signature RSA PKCS#1 v1.5 or PSS with SHA-256; RS256 is the OpenID Connect default
ES256 Signature ECDSA on P-256 with SHA-256, sent as 64 bytes: r then s
Ed25519 (was EdDSA) Signature Ed25519 with an OKP key (RFC 8037)
RSA-OAEP Public-key encryption RSA-OAEP with SHA-1 and MGF1-SHA-1 encrypts the random content key (CEK), 256 bits for A256GCM
ECDH-ES Key exchange ECDH between a fresh sender key (epk in the header) and the recipient's P-256, P-384, P-521 or X25519 key
A256KW, dir Key wrap AES key wrap of the CEK under a shared key; dir uses the shared key as the CEK
A256GCM Cipher AES-256 in GCM, 96-bit IV, 128-bit tag; the encoded header is the AAD
A128CBC-HS256 Cipher and MAC AES-128 in CBC, then HMAC-SHA-256 cut to 16 bytes; the 32-byte key is half MAC key, half AES key
Concat KDF KDF the one-step KDF with SHA-256 turns the ECDH secret into a key
PBES2-HS256+A128KW KDF PBKDF2-HMAC-SHA-256 from a password (salt p2s, count p2c), then AES key wrap
256 in the names, kid Hash SHA-256; a JWK thumbprint (RFC 7638) is the SHA-256 of a key's required fields, often used as its kid

Who picks the algorithm? In SSH and TLS the two sides negotiate. A token can't: the issuer writes alg (and enc for JWE) into the header, and the receiver reads it before anything is verified. The header is a claim, not an instruction. The verifier must know which algorithm each key is for and refuse the rest (RFC 7515 §5.2, RFC 8725 §3.1). The best-known JWT bugs come from libraries that let the header decide.

How a token is made and checked

  1. Keys. The issuer publishes its public keys as a JWK Set (RFC 7517), usually at a jwks_uri. Each key has a type (RSA, EC, oct, OKP, and AKP for ML-DSA), a kid and ideally an alg.
  2. Header and claims. The issuer writes a header (alg, kid, typ) and the claims: iss, sub, aud, exp, nbf, iat, jti (RFC 7519 §4.1). Both are JSON, base64url-encoded without padding.
  3. Signing (JWS). The signature or MAC covers the two encoded parts joined by a dot and becomes the third part: header.payload.signature. Anyone can still read the claims; the signature only stops changes.
  4. Encryption (JWE). A random CEK encrypts the payload under enc, with the encoded header as associated data (RFC 7516 §5.1); alg gets the CEK to the recipient. Five parts: header.key.iv.ciphertext.tag. For both, sign first, then encrypt the signed JWT ("cty":"JWT", RFC 7519 §11.2).
  5. Verifying. The receiver finds the key by kid, checks that alg is allowed for that key, checks the signature or decrypts, and only then reads the claims: exp and nbf against its clock, iss and aud against what it expects (RFC 7519 §7.2, RFC 8725 §3.8–3.9).

Where it runs. OpenID Connect providers MUST support RS256 for ID tokens (OIDC Core §15.1), and it is the default when a client names nothing (OIDC Registration §2). OAuth access tokens as JWTs (RFC 9068, 2021) must be signed, never with none, and servers must support RS256. ACME (RFC 8555, 2019), the protocol Let's Encrypt speaks, wraps each request body in a JWS; servers MUST implement ES256 and SHOULD implement EdDSA with Ed25519 (§6.2).

Algorithms by category

Category In use Standard Legacy or retired
Key management (JWE alg) Elliptic-curve DH: ECDH-ES, ECDH-ES+A128KW / +A256KW (P-256, X25519)
RSA: RSA-OAEP-256, RSA-OAEP
Shared key: A128KW, A256KW, dir
RFC 7518 §4, RFC 8037 RSA1_5: RFC 8725 says avoid; a JOSE draft deprecates it
Key management, new HPKE-0 to HPKE-7 and their -KE forms, DH-based KEMs only IETF draft (jose-hpke-encrypt-22, July 2026) —
Signature RSA: RS256, PS256
ECDSA: ES256, ES384, ES512, ES256K
EdDSA: Ed25519
RFC 7518 §3, RFC 9864, RFC 8812 EdDSA (deprecated by RFC 9864); none (being deprecated)
Signature, post-quantum ML-DSA-44, ML-DSA-65, ML-DSA-87, key type AKP RFC 9964 (2026) —
Content encryption (enc) AES-GCM: A256GCM, A128GCM
AES-CBC + HMAC: A128CBC-HS256, A256CBC-HS512
RFC 7518 §5 —
MAC HS256, HS384, HS512; HMAC inside the CBC modes RFC 7518 §3.2, §5.2 keys shorter than the hash (MUST NOT, RFC 7518 §3.2); passwords as keys
Hash SHA-256/384/512 named in each alg; SHA-256 thumbprints FIPS 180-4, RFC 7638 SHA-1 inside RSA-OAEP padding; RSA-OAEP-256 uses SHA-256
KDF Concat KDF with SHA-256 for ECDH-ES; PBKDF2 for PBES2-* SP 800-56A, RFC 8018 small p2c (RFC 7518 recommends at least 1,000)

RFC 7519 §8 made HS256 and none mandatory to implement, and RSA1_5 for encryption; two of the three are now on the way out. RFC 9864 deprecated the polymorphic EdDSA (the key's crv picks the curve) for Ed25519 and Ed448. panva's jose takes both names; PyJWT 2.15.1 knows only EdDSA.

Post-quantum. RFC 9964 adds ML-DSA with a new key type, AKP, whose private part is just the 32-byte seed; panva's jose has had it since 6.1.0 (August 2025). An ML-DSA-44 signature is 2,420 bytes (3,227 base64url characters, against 86 for ES256) in every request that carries the token. SLH-DSA and composite signatures are drafts. JWE has no post-quantum key management yet.

Known attacks

Mostly library mistakes, not broken algorithms. RFC 8725 lists them; "bis" marks additions in its replacement, draft-ietf-oauth-rfc8725bis (-10, 2026).

Attack What goes wrong Fix
alg: none an unsigned token passes because its header says so; bis adds "noNE" getting past a blocklist in a library that read alg case-insensitively never by default (RFC 7518 §3.6, RFC 8725 §3.2); an allowlist
RS256 to HS256 the attacker MACs a token with the server's public RSA key as the HMAC secret (McLean, 2015; CVE-2015-9235) one key, one algorithm (RFC 8725 §3.1)
weak HS256 secret one captured token allows offline guessing a random key at least as long as the hash (RFC 8725 §3.5)
ECDSA encoding JWS wants r and s at fixed width, 64 bytes for ES256, not the DER that OpenSSL and Java produce reject any other length (RFC 7518 §3.4); deterministic ECDSA (RFC 8725 §3.2)
RSA1_5 PKCS#1 v1.5 padding errors let an attacker decrypt the CEK (Bleichenbacher, 1998) RSA-OAEP (RFC 8725 §3.2); panva's jose dropped RSA1_5 in 6.0.0 (February 2025)
invalid curve JWEs with epk points off the curve, and what they decrypt to, reveal the private key (Sanso, 2017) validate every point (RFC 8725 §2.5, §3.4)
PBES2 p2c the sender picks the PBKDF2 count, and a huge one stalls the receiver bis: reject above 1,200,000, twice OWASP's 600,000; panva's jose stops at 10,000

Seeing it on your machine

On Debian or Ubuntu, apt install jose gives the tool used above:

jose alg                                       # every alg and enc value it knows
jose jwk gen -i '{"alg":"ES256"}' -o ec.jwk    # new P-256 key pair as a JWK
jose jwk pub -i ec.jwk -o ec.pub.jwk           # public half, for verifiers
printf '{"sub":"alice"}' | jose jws sig -I- -k ec.jwk -c -o es256.jwt
jose jws ver -i es256.jwt -k ec.pub.jwk -O-    # prints the payload if it verifies
jose jwk thp -i ec.pub.jwk                     # RFC 7638 thumbprint, a good kid
jose jwk gen -i '{"alg":"ECDH-ES+A128KW"}' -o ecdh.jwk
printf 'hello' | jose jwe enc -I- -k ecdh.jwk -c -o msg.jwe
jose jwe dec -i msg.jwe -k ecdh.jwk            # enc defaulted to A128CBC-HS256
  • Python: paste a token, or press Enter twice for the RFC 7519 example and the key from RFC 7515 Appendix A.1. For a JWE it prints the part sizes.
import base64, hashlib, hmac, json

def b64(s):
    return base64.urlsafe_b64decode(s + "=" * (-len(s) % 4))

tok = input("JWT: ").strip() or (
    "eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9."
    "eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ."
    "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk")
parts = tok.split(".")
head = json.loads(b64(parts[0]))
print("header   ", json.dumps(head))
if len(parts) == 5:                      # JWE: header.key.iv.ciphertext.tag
    print("sizes    ", [len(b64(x)) for x in parts[1:]])
else:                                    # JWS: header.payload.signature
    print("claims   ", json.dumps(json.loads(b64(parts[1]))))
    sig = b64(parts[2])
    print("signature", len(sig), "bytes")
    if head["alg"] == "HS256":
        k = b64(input("key (JWK k): ").strip() or
                "AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow")
        mac = hmac.new(k, (parts[0] + "." + parts[1]).encode(), hashlib.sha256).digest()
        print("computed ", base64.urlsafe_b64encode(mac).rstrip(b"=").decode())
        print("valid    ", hmac.compare_digest(mac, sig))
header    {"typ": "JWT", "alg": "HS256"}
claims    {"iss": "joe", "exp": 1300819380, "http://example.com/is_root": true}
signature 32 bytes
computed  dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
valid     True

The MAC matches RFC 7515 Appendix A.1, yet a real verifier would reject the token: exp 1300819380 is 22 March 2011. The RFC 7516 example gives sizes [256, 12, 63, 16]: one 2048-bit RSA block, GCM's IV, 63 bytes for 63 of text, and the tag. The ES256 token from jose has a 64-byte signature.

See also

  • TLS 1.3 / SSH: protocols that negotiate algorithms; a JWT can only announce one
  • X.509: certificates; a JWK is a bare key, though x5c can carry a chain
  • FIDO2: passkeys name their algorithms in COSE, JOSE's CBOR sibling, with the same ES256 and EdDSA names
  • HMAC / ECDSA / Key wrap / ML-DSA: the algorithms in depth
  • NIST KDFs: computes RFC 7518's ECDH-ES key derivation example

References

Weaker sources: web search results for the year of RFC 9964 (2026) and for the state of draft-ietf-jose-pqc-kem.