Mental model
A JWT isn’t encryption and it isn’t a secret. It’s a signed, readable claim: anyone can see what it says, but only the holder of the key can prove it’s authentic. Hold that, and every rule in this library follows.
Three dots, three parts
A token is just three base64url strings joined by dots:
header . payload . signature
│ │ │
│ │ └─ proves the first two weren't changed
│ └───────────── the claims (sub, exp, custom fields)
└───────────────────────── which algorithm signed it (alg, typ, kid)Decode the first two parts and you can read them — they are not encrypted, only
base64url-encoded. The signature is the interesting part: it’s computed over
header.payload with a key, so changing a single byte of either invalidates it.
A JWT is signed, not sealed
Never put secrets (passwords, card numbers) in a payload — anyone holding the token can read the claims. A JWT protects against tampering, not against reading.
The one rule: signing key vs verifying key
Everything about which key goes where comes down to the algorithm family:
- HMAC (
HS*) is symmetric — one secret does both jobs. The party that signs and the party that verifies share the same string. Good when that’s the same party (your own API issuing tokens to itself). - RSA / ECDSA (
RS*/ES*) is asymmetric — a private key signs, a public key verifies. You keep the private key; you can hand the public key to anyone. Good when others must verify tokens you issued (or you verify tokens a provider issued).
That distinction is why the library has two separate types:
use Flytachi\Jwt\Entity\PrivateKey;
use Flytachi\Jwt\Entity\PublicKey;
// Signing side — encode() only ever takes a PrivateKey
new PrivateKey($secretOrPrivatePem, 'HS256');
// Verifying side — decode() only ever takes PublicKeys
new PublicKey($secretOrPublicPem, 'HS256');For HMAC the “private” and “public” key hold the same secret string — the type names describe the role, not that the material differs.
A key is bound to its algorithm
A PrivateKey and PublicKey each carry an algorithm, and decode() enforces that
the verifying key’s algorithm equals the one named in the token header. This isn’t
bureaucracy — it’s the defense against the classic algorithm-confusion attack, where
an attacker takes your RSA public key (which is, well, public) and tries to use it as an
HMAC secret. Because the key is pinned to RS256, a token claiming HS256 is rejected
before verification even runs.
This is why
Every “the algorithm must match” error traces back to this pin. You choose the algorithm when you construct the key, and the token can’t talk you out of it.
What happens on encode()
Gently, end to end:
- The header is built from the key’s algorithm (and its
kid, if set). - Header and payload are JSON-encoded, then base64url-encoded.
header.payloadis signed with the private key — HMAC forHS*,openssl_signforRS*/ES*.- The signature is base64url-encoded and appended. You get
header.payload.signature.
What happens on decode()
- The token is split into three parts (wrong count → rejected).
- The header is read to learn the algorithm — and, for asymmetric tokens, the
kid. - The right
PublicKeyis selected: the sole key for HMAC, or thekid-matched key for RSA/ECDSA. - The key’s algorithm is checked against the header (the pin above).
- The signature is verified — with
hash_equalsfor HMAC, so the comparison is constant-time and leaks no timing information. - Finally the time claims (
exp,nbf,iat) are validated, honoringleeway.
Only if all of that passes do you get a JwtPayload back. The gritty version —
ECDSA DER conversion, JWKS PEM building, the timing-attack defense — lives in the
Security model.
When to reach for each family
- ✅ HMAC — your service issues tokens and verifies them itself. Simplest, fastest.
- ✅ RSA / ECDSA — third parties (or separate services) must verify without your signing secret, or you verify a provider’s tokens via JWKS.
- ❌ Storing secrets in the payload — it’s readable. Use a claim as an identifier, then look up sensitive data server-side.
Next
- Do it hands-on: Quickstart
- Recipes: HMAC tokens · Asymmetric keys
- The authoritative surface: API reference