Package · jwt

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:

text
  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:

php
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:

  1. The header is built from the key’s algorithm (and its kid, if set).
  2. Header and payload are JSON-encoded, then base64url-encoded.
  3. header.payload is signed with the private key — HMAC for HS*, openssl_sign for RS*/ES*.
  4. The signature is base64url-encoded and appended. You get header.payload.signature.

What happens on decode()

  1. The token is split into three parts (wrong count → rejected).
  2. The header is read to learn the algorithm — and, for asymmetric tokens, the kid.
  3. The right PublicKey is selected: the sole key for HMAC, or the kid-matched key for RSA/ECDSA.
  4. The key’s algorithm is checked against the header (the pin above).
  5. The signature is verified — with hash_equals for HMAC, so the comparison is constant-time and leaks no timing information.
  6. Finally the time claims (exp, nbf, iat) are validated, honoring leeway.

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 one party, shared secretRSA / ECDSA many verifiers, public key
  • 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