wilycode / jwt-token
Minimal, strict HS256 JWT issuing and verification for PHP 8.4+
Requires
- php: ^8.4
- ext-hash: *
- ext-json: *
Requires (Dev)
- phpunit/phpunit: ^11.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-26 22:07:45 UTC
README
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.
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
- Quick start
- Generate a signing secret
- Verification rules
- API reference
- Error handling
- Security and limitations
- Development
- License
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
algheader must equalHS256andtypmust equalJWT.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
expclaim is required by default, both when issuing and verifying tokens. An expired token is rejected. Thenbfandiatclaims, when present, are validated too. - Claim types:
exp,nbfandiatmust be integer Unix timestamps, not numeric strings or floating-point values.iss,subandjtimust be strings;audmay 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,
expectedIssuermust exactly matchiss, andexpectedAudiencemust 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 invalidencode()payload, a negative leeway, or an oversized token being generated.isValid(): returnsfalseon 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.