Quickstart
From install to a verified token in five minutes: you’ll issue a signed JWT for a logged-in user, hand it back on the next request, and verify it before trusting a single claim.
What you’ll build
A tiny login-then-access flow. On login you issue a token that says who the user is and when it expires. On the next request the client sends it back, and you verify it — signature and expiry — before reading the user id. That’s the whole loop: issue → send → verify → read.
We’ll use HS256 (a shared secret) because it needs no key files. Swapping to RSA or
ECDSA later changes only the key, not the flow — see Asymmetric keys.
Before you start
- PHP ≥ 8.1 with
ext-opensslandext-json(check withphp -m) - A strong secret (see Installation)
Install the package:
composer require flytachi/jwtStep 1 — Issue a token on login
Build a JwtPayload with the claims you care about, then sign it with a PrivateKey.
<?php
require 'vendor/autoload.php';
use Flytachi\Jwt\JWT;
use Flytachi\Jwt\Entity\JwtPayload;
use Flytachi\Jwt\Entity\PrivateKey;
$secret = getenv('JWT_SECRET'); // load it, never hard-code it
$now = time();
$token = JWT::encode(
new JwtPayload([
'iss' => 'https://my-app.com', // who issued it
'sub' => 'user-42', // who it's about
'iat' => $now, // issued at
'exp' => $now + 3600, // expires in one hour
]),
new PrivateKey($secret, 'HS256')
);
echo $token; // eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...What are these claims?
iss, sub, iat, exp are standard registered claims. exp and iat (and nbf)
are validated automatically on decode. You can add any custom claim too — see
Working with claims.
Step 2 — Send it back and verify
On the next request the client returns the token (usually in an
Authorization: Bearer <token> header). Verify it with a PublicKey carrying the
same secret and algorithm.
<?php
require 'vendor/autoload.php';
use Flytachi\Jwt\JWT;
use Flytachi\Jwt\Entity\PublicKey;
use Flytachi\Jwt\JWTException;
$secret = getenv('JWT_SECRET');
$token = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
$token = str_replace('Bearer ', '', $token);
try {
$payload = JWT::decode($token, [new PublicKey($secret, 'HS256')]);
$userId = $payload->getClaim('sub');
echo "Authenticated as {$userId}\n";
} catch (JWTException $e) {
http_response_code(401);
echo "Invalid token: {$e->getMessage()}\n";
}Verification does three things at once
decode() checks the signature (tamper-evidence), confirms the key’s algorithm
matches the token’s header, and validates exp/nbf/iat. Any failure throws a
single JWTException, so one catch covers all of them.
Step 3 — See it reject a bad token
Prove the guarantees hold. Tamper with the token or wait for it to expire:
// A token whose secret doesn't match → signature check fails
try {
JWT::decode($token, [new PublicKey('wrong-secret', 'HS256')]);
} catch (JWTException $e) {
echo $e->getMessage(); // Signature verification failed.
}
// An already-expired token → time-claim check fails
$expired = JWT::encode(
new JwtPayload(['sub' => 'user-42', 'exp' => time() - 10]),
new PrivateKey($secret, 'HS256')
);
try {
JWT::decode($expired, [new PublicKey($secret, 'HS256')]);
} catch (JWTException $e) {
echo $e->getMessage(); // Token has expired (exp).
}The whole thing
The complete, copy-paste round trip:
<?php
require 'vendor/autoload.php';
use Flytachi\Jwt\JWT;
use Flytachi\Jwt\Entity\JwtPayload;
use Flytachi\Jwt\Entity\PrivateKey;
use Flytachi\Jwt\Entity\PublicKey;
use Flytachi\Jwt\JWTException;
$secret = 'change-me-to-a-strong-secret';
$now = time();
// Issue
$token = JWT::encode(
new JwtPayload([
'iss' => 'https://my-app.com',
'sub' => 'user-42',
'iat' => $now,
'exp' => $now + 3600,
]),
new PrivateKey($secret, 'HS256')
);
// Verify
try {
$payload = JWT::decode($token, [new PublicKey($secret, 'HS256')]);
echo "OK, user = " . $payload->getClaim('sub') . "\n";
} catch (JWTException $e) {
echo "Rejected: " . $e->getMessage() . "\n";
}Run it:
php jwt-demo.php
# OK, user = user-42What just happened
encode()built a header, base64url-encoded the header and payload, signed the pair, and joined them into theheader.payload.signaturestring you saw.decode()reversed it: split the token, verified the signature withhash_equals, checked the algorithm matched, and validated the time claims.- A single secret both signed and verified, because
HS256is symmetric. With RSA/ECDSA these become two different keys.
Next steps
- Mental model — what’s actually inside a JWT
- HMAC tokens — more shared-secret recipes
- Asymmetric keys — RSA & ECDSA sign/verify
- Working with claims — expiry, leeway, custom claims
- API reference — every class and method