Package · jwt

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.

Time ~5 minLevel BeginnerPrereqs PHP 8.1+

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-openssl and ext-json (check with php -m)
  • A strong secret (see Installation)

Install the package:

bash
composer require flytachi/jwt

Step 1 — Issue a token on login

Build a JwtPayload with the claims you care about, then sign it with a PrivateKey.

login.php
<?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.

api.php
<?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:

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

jwt-demo.php
<?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:

bash
php jwt-demo.php
# OK, user = user-42

What just happened

  • encode() built a header, base64url-encoded the header and payload, signed the pair, and joined them into the header.payload.signature string you saw.
  • decode() reversed it: split the token, verified the signature with hash_equals, checked the algorithm matched, and validated the time claims.
  • A single secret both signed and verified, because HS256 is symmetric. With RSA/ECDSA these become two different keys.

Next steps