Package · jwt

Working with claims

Claims are the payload of a token — the statements you’re signing. This guide covers the registered claims the library validates for you, how to add your own, and how to read them back safely.

Setting claims

A JwtPayload is a plain associative array of claims. Standard (registered) claims use short three-letter keys; you can mix in any custom keys you like:

php
use Flytachi\Jwt\Entity\JwtPayload;

$now = time();
$payload = new JwtPayload([
  // registered claims
  'iss' => 'https://my-app.com',       // issuer
  'sub' => 'user-42',                  // subject (who the token is about)
  'aud' => 'https://api.my-app.com',   // audience
  'iat' => $now,                       // issued at
  'nbf' => $now,                       // not valid before
  'exp' => $now + 3600,                // expires in one hour
  // custom claims — anything JSON-serializable
  'role'   => 'admin',
  'scopes' => ['orders:read', 'orders:write'],
]);

Time claims are validated automatically

Three registered claims control a token’s validity window, and decode() checks all three for you:

Claim Meaning Rejected when
exp Expiration time now (minus leeway) is at or past exp
nbf Not before nbf is later than now (plus leeway)
iat Issued at iat is later than now (plus leeway)

Each is optional — omit exp and the token never expires (rarely what you want). All values are Unix timestamps (seconds).

Always set exp

A token without exp is valid forever, so a leaked one can’t be aged out. Set a short lifetime (minutes to an hour for access tokens) and issue a fresh token when it lapses.

Allow for clock skew with leeway

Servers’ clocks drift by a few seconds. decode() takes a third argument, leeway (seconds), that widens the acceptance window on every time check — so a token that just expired, or whose nbf is a second in your future, isn’t rejected over a tiny mismatch:

php
use Flytachi\Jwt\JWT;

// Tolerate up to 60 seconds of clock difference
$payload = JWT::decode($token, [$publicKey], leeway: 60);

Keep leeway small — tens of seconds. Large values effectively extend every token’s life past its stated exp.

Reading claims back

getClaim() reads a single claim, with an optional default when it’s absent:

php
$userId = $payload->getClaim('sub');              // 'user-42'
$role   = $payload->getClaim('role', 'guest');    // default if missing
$scopes = $payload->getClaim('scopes', []);       // ['orders:read', ...]

if (in_array('orders:write', $payload->getClaim('scopes', []), true)) {
  // authorized to write orders
}

Need the whole set at once — for logging or forwarding? Use toArray():

php
$all = $payload->toArray();
// ['iss' => 'https://my-app.com', 'sub' => 'user-42', 'role' => 'admin', ...]

getClaim never throws on a missing claim

An absent claim returns the default (null if you don’t pass one), so reading is safe. Validate the claims you require explicitly — e.g. reject a token with no sub.

Validate application claims yourself

Beyond the time claims, meaning is up to you. Assert issuer, audience, and any authorization claim after decoding:

php
use Flytachi\Jwt\JWTException;

$payload = JWT::decode($token, [$publicKey]);

if ($payload->getClaim('aud') !== 'https://api.my-app.com') {
  throw new JWTException('Token audience mismatch.');
}
if ($payload->getClaim('role') !== 'admin') {
  http_response_code(403);
  exit('Admins only.');
}