Skip to content

Learn

What is a JWT?

A short introduction to JSON Web Tokens, how they're signed and encrypted, and the mistakes that make them unsafe.

A token you can check without a database

A JSON Web Token (JWT, said “jot”) is a compact way to send a set of facts, called claims, that the receiver can check without asking anyone. A sign-in server issues one after you log in, and every API you call can confirm it's genuine using only a key.

Most JWTs are signed, not encrypted. Anyone holding one can read it, so it should never carry secrets. The signature only proves who wrote it and that nobody changed it. The standard is RFC 7519.

Three parts, separated by dots

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyLTQyIiwiZXhwIjoxNzk0MzQ1MTk4fQ.eWs5MFtZNmQSIeMI2pP2pK6NWfiuR6MfAd2wC6WeFDE

Header: how the token is protected, for example {"alg": "RS256", "kid": "key-2"}. alg names the algorithm and kid says which of the issuer's keys was used.

Payload: the claims, such as who the token is about and when it expires.

Signature: computed over the first two parts with the issuer's key. Change one character of the header or payload and it no longer matches.

The first two parts are just JSON in Base64URL encoding, which is why any tool can decode them. Decoding isn't verifying: only checking the signature with the right key tells you the token is genuine.

Signed (JWS) and encrypted (JWE)

A signed token is a JWS: three parts, readable by anyone. An encrypted token is a JWE: five parts (header, encrypted key, IV, ciphertext and tag), readable only with the right private or shared key. A JWE often wraps a signed token, so the contents are both private and verifiable.

Registered claims

These have the same meaning everywhere. Issuers add their own claims alongside them.

Issuer iss
Who created and signed the token, usually the sign-in server's URL. Use it to find the right keys, never as proof on its own.
Subject sub
Who or what the token is about: a user or a service ID, unique within the issuer.
Audience aud
Who the token is for. A string or an array. A service should reject any token that doesn't name it.
Expires exp
Seconds since 1970 after which the token must be rejected.
Not before nbf
Seconds since 1970 before which the token must be rejected.
Issued at iat
When the token was created. Useful for spotting tokens that live too long.
Token ID jti
A unique ID for this token, so a service can refuse to accept the same one twice.

Time claims are whole seconds since 1 January 1970 UTC, not milliseconds.

Signing algorithms

HMAC (HS256, HS384, HS512) uses one shared secret. Whoever can verify a token can also create one, so it suits a single service talking to itself.

Public-key (RS*, PS*, ES*, EdDSA) signs with a private key that only the issuer holds and verifies with a public key that anyone can have. This is what sign-in providers use, because APIs can check tokens without being able to forge them.

Where the keys come from

OpenID Connect providers publish their public keys as a JSON Web Key Set (JWKS). Take the token's iss, add /.well-known/openid-configuration, and the document there gives a jwks_uri. The key whose kid matches the token's header verifies it.

A match proves that issuer signed the token. It doesn't prove you should trust that issuer: a service should only accept issuers it already knows. Tessera does this lookup with Fetch keys from issuer.

Common mistakes

Tessera's security checks look for each of these.

Unsigned tokens (alg: none)

A token whose header says "alg": "none" has no signature, so anyone can write one. Libraries that honour none by default have let attackers sign themselves in. Never accept it outside tests.

Missing or long expiry

Without exp, a leaked token works forever. Access tokens usually last minutes to an hour. Check nbf and iat too: values in the future often mean a clock is wrong, and values in milliseconds instead of seconds are a common bug.

Missing or unchecked audience

If a token has no aud, or the service doesn't check it, a token issued for one API can be replayed against another that trusts the same issuer. Each service should accept only tokens that name it. When aud is an array, it's enough for one entry to match.

Weak or published HMAC secrets

HS256, HS384 and HS512 use one shared secret to sign and verify. A short secret can be guessed offline from a single token, and secrets copied from tutorials are in every wordlist. Use at least as many random bytes as the hash size (32 for HS256).

Algorithm confusion

If a verifier lets the token pick its algorithm, an attacker can take an RSA public key, which is public, and use it as an HMAC secret. Always decide the algorithm from the key you hold, not from the token's header. Tessera only uses a key with the algorithm it was made for.

Keys and key URLs in the header

The jku, x5u, jwk and x5c header parameters let a token point to, or carry, the key that verifies it. Trusting them blindly means trusting the attacker's key. Only use them when the key or certificate chain leads back to something you already trust.

Weak JWE key management

RSA1_5 is open to padding-oracle attacks, so prefer RSA-OAEP-256 or ECDH-ES. PBES2 derives a key from a password, and a low iteration count (p2c) makes that password quick to guess.

Hidden characters and duplicate keys

Unicode text-direction controls can make a claim read differently from what it contains, and JSON with the same key twice is read differently by different parsers. Tessera shows both instead of hiding them.

Ready to look at a real token? The Guide shows how to inspect, verify, decrypt and build tokens in Tessera.