Search by

wilycode / jwt-token

szymonpietrek-design

Minimal, strict HS256 JWT issuing and verification for PHP 8.4+

Package info

github.com/wilycode/jwt-token

pkg:composer/wilycode/jwt-token

Statistics

Installs: 1

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.1.0 2026-10-26 22:03 UTC

This package is auto-updated.

Last update: 2026-10-26 22:07:45 UTC


README

PHP 8.4+ MIT License Tests

Minimal, framework-independent HS256 JWT issuing and verification for PHP 8.4+. No additional production libraries are required.

WilyCode PHP Ecosystem

Part of WilyCode — independent PHP components with their own repositories, Composer packages and releases.

Explore the WilyCode PHP Ecosystem →

This package is deliberately small: it signs and verifies only HS256 tokens using PHP's built-in HMAC implementation. It is suitable for applications that control both the token issuer and verifier. It is not an OAuth 2.0 / OpenID Connect client, an ID-token validator, a JWKS client, or a replacement for a complete authentication system. If you need asymmetric signing (RS256/EdDSA), key discovery, or OIDC protocol validation, choose an established, actively maintained JWT/OIDC library instead.

Contents

Installation

composer require wilycode/jwt-token

Requires PHP 8.4+ and PHP extensions json and hash (normally included with PHP). There are no mandatory WilyCode or third-party runtime dependencies.

Quick start

<?php

declare(strict_types=1);

require __DIR__ . '/vendor/autoload.php';

use WilyCode\JWTToken\InvalidTokenException;
use WilyCode\JWTToken\JWTToken;

$secret = getenv('JWT_SECRET');
if ($secret === false) {
    throw new RuntimeException('JWT_SECRET must be configured.');
}

$jwt = new JWTToken(
    secret: $secret,
    expectedIssuer: 'my-app',
    expectedAudience: 'my-api',
);

// Issue a short-lived token. All times are Unix timestamps in seconds.
$now = time();
$token = $jwt->encode([
    'iss' => 'my-app',
    'aud' => 'my-api',
    'sub' => 'user-15',
    'iat' => $now,
    'nbf' => $now,
    'exp' => $now + 900, // 15 minutes
]);

try {
    $claims = $jwt->decode($token, leeway: 30);
    // Check that the subject is still an active, authorized user.
} catch (InvalidTokenException $e) {
    // Reject invalid, expired, or incorrectly targeted tokens.
}

decode() verifies the token before returning claims. For a simple true/false check, use $jwt->isValid($token); it does not return claims. The expectedIssuer and expectedAudience settings apply when verifying tokens, not when automatically generating the claims for encode().

Generate a signing secret

Generate an unpredictable secret with at least 256 bits of entropy:

php -r 'echo bin2hex(random_bytes(32)), PHP_EOL;'

Set its output as JWT_SECRET in your application's secret management or environment configuration. getenv() reads an existing environment variable; it does not automatically load a .env file. A string of 32 repeated characters may pass the minimum length check but is not a secure secret. Never commit the secret, include it in a token, or log it. Use separate secrets for different issuers, audiences or token purposes.

Verification rules

  • HS256 only: the alg header must equal HS256 and typ must equal JWT. none, other algorithms, critical JWS extensions and unencoded payloads are rejected. No key is ever selected from token-provided headers.
  • Signature: the complete header and payload are authenticated by HMAC-SHA256 and compared using hash_equals().
  • Expiration: an integer exp claim is required by default, both when issuing and verifying tokens. An expired token is rejected. The nbf and iat claims, when present, are validated too.
  • Claim types: exp, nbf and iat must be integer Unix timestamps, not numeric strings or floating-point values. iss, sub and jti must be strings; aud may be a string or a non-empty list of strings. This is an intentionally narrower profile than every representation permitted by the JWT RFC.
  • Context: when configured, expectedIssuer must exactly match iss, and expectedAudience must exactly match an audience value. Configure these when tokens cross application boundaries.
  • Format: exactly three unpadded, canonical Base64URL parts; JSON header and payload must be objects. Input and output are limited to 16 KiB by default.
  • Leeway: decode($token, leeway: 30) allows 30 seconds of clock skew for time-based checks. Prefer a small value.

For rare cases not involving authorization or login, the requirement for exp can be switched off explicitly with new JWTToken($secret, requireExpiration: false). This is discouraged for security tokens; otherwise they can remain valid indefinitely.

API reference

Method Purpose
new JWTToken(string $secret, ?Closure $clock = null, ?string $expectedIssuer = null, ?string $expectedAudience = null, bool $requireExpiration = true, int $maxTokenLength = 16384) Configure the HS256 key and verification policy. $clock is intended for deterministic testing.
encode(array $claims): string Produce a signed compact JWT. Rejects invalid registered claim types and missing required expiration.
decode(string $token, int $leeway = 0): array Verify and decode the token or throw InvalidTokenException.
isValid(string $token, int $leeway = 0): bool Return false for invalid tokens or invalid leeway instead of throwing.

Example with a test clock:

$jwt = new JWTToken(
    secret: $secret,
    clock: static fn (): int => 1_800_000_000,
);

The payload returned by decode() is an associative PHP array. Registered claims are validated, but custom claims are not automatically trusted or authorized; applications must validate their own schemas and rules.

Error handling

  • InvalidTokenException: malformed/invalid JWT, unsupported header, bad signature, missing or invalid claims, expiration, issuer/audience mismatch or size limit.
  • InvalidArgumentException: bad library configuration, an invalid encode() payload, a negative leeway, or an oversized token being generated.
  • isValid(): returns false on these conditions. It does not hide unrelated application errors (e.g. exceptions from a custom clock callback).

Security and limitations

JWT is signed, not encrypted. The payload is readable by anyone holding the token. Do not place passwords, private keys, or sensitive personal information in claims.

A valid signature does not grant authorization. After verification, check the subject, issuer/audience configuration, roles, account status and resource permissions using your application policy. In particular, verifying an external identity provider's ID token needs more than this library provides. Follow RFC 8725 — JWT Best Current Practices.

No automatic revocation or replay prevention. A stolen token can be reused until expiration unless your application maintains a denylist, session version or other revocation mechanism. Use HTTPS, short lifetimes, secure client-side storage, and appropriate browser CSRF protections. For logout/revocation needs, consider opaque sessions or server-maintained token state.

No secret rotation protocol, key identifiers (kid), public-key algorithms, JWKS discovery, OIDC support or automatic user validation. Different keys must be configured outside this library if you rotate keys. Do not reuse the same secret across unrelated applications.

This is a focused first-party HS256 helper, not a formally audited cryptography package. For complex or third-party token integrations, prefer an established library such as firebase/php-jwt and use an OIDC client where appropriate.

Development

composer install
composer validate --strict
composer test

GitHub Actions runs PHPUnit on PHP 8.4 and 8.5. Test files and CI configuration are excluded from Composer distribution archives.

License

MIT. See LICENSE.