daycry / jwt
JWT Token for Codeigniter 4
Requires
- php: ^8.2
- lcobucci/clock: ^3.0
- lcobucci/jwt: ^5.5
- psr/clock: ^1.0
Requires (Dev)
- dev-master
- v3.2.0
- v3.1.0
- v3.0.0
- v2.0.1
- v2.0.0
- v1.0.6
- v1.0.5
- v1.0.4
- v1.0.3
- v1.0.2
- v1.0.1
- v1.0.0
- dev-development
- dev-dependabot/github_actions/actions/upload-pages-artifact-5
- dev-dependabot/github_actions/actions/checkout-6
- dev-dependabot/github_actions/github/codeql-action-4
- dev-dependabot/github_actions/actions/cache-5
- dev-dependabot/github_actions/actions/setup-python-6
- dev-docs/update-after-hardening
This package is auto-updated.
Last update: 2026-06-07 10:48:04 UTC
README
A JWT (JSON Web Token) library for CodeIgniter 4, built on top of lcobucci/jwt ^5. Supports HMAC, RSA and ECDSA, an immutable faΓ§ade, and key rotation.
π Full documentation: https://daycry.github.io/jwt/ β this README is a quick overview; the site has the complete, searchable reference.
Package
Quality
Community
Requirements
- PHP 8.2 or higher
- CodeIgniter 4.x
lcobucci/jwt ^5.5
Upgrading from v2.x? Read the v2 β v3 migration guide.
Installation
composer require daycry/jwt
Publish the configuration file
php spark jwt:publish
Generate a signing key
php spark jwt:key
The key is written automatically to .env as jwt.signer. Use --show to print it without touching the file.
β οΈ Never commit
.envto version control.
Quick Start
php spark jwt:publish # write app/Config/JWT.php php spark jwt:key # generate jwt.signer in .env
use Daycry\JWT\JWT; $jwt = JWT::for(); // pulls config('JWT') // or inject an explicit config: new JWT(config('JWT')); // Encode β the uid may be a string or an integer ID (e.g. a DB primary key) $token = $jwt->encode(['user_id' => 42, 'role' => 'admin'], 'user-42'); // Decode + validate (throws on failure) $claims = $jwt->decode($token); // Plain echo $claims->claims()->get('uid'); // "user-42" // Symmetric helper β get the original payload back $payload = $jwt->getPayload($token); // ['user_id' => 42, 'role' => 'admin'] // Non-throwing alternative $claims = $jwt->tryDecode($maybeBadToken); if ($claims === null) { return $this->response->setStatusCode(401); }
The library throws
JWTConfigurationExceptionifjwt.signer,jwt.issuer,jwt.audience, orjwt.identifieris missing β bothnulland an empty string""are rejected. Defaults are intentionallynullto fail loudly.
Configuration
After publishing, edit app/Config/JWT.php. All properties are inherited from Daycry\JWT\Config\JWT and overridable via .env.
HMAC (default)
jwt.algorithmType = "symmetric" jwt.signer = "<base64-secret-from-jwt:key>" jwt.issuer = "https://api.my-app.com" jwt.audience = "https://my-app.com" jwt.identifier = "my-app-v2" jwt.expiresAt = "+1 hour" jwt.leeway = "30"
RSA / ECDSA
php spark jwt:keypair --algorithm=rsa --bits=2048 --output=writable/keys
jwt.algorithmType = "asymmetric" jwt.signingKey = "/var/www/app/writable/keys/jwt-private.pem" jwt.verifyingKey = "/var/www/app/writable/keys/jwt-public.pem" jwt.issuer = "https://api.my-app.com" jwt.audience = "https://my-app.com" jwt.identifier = "my-app-v2"
In app/Config/JWT.php set the signer class:
public string $algorithm = \Lcobucci\JWT\Signer\Rsa\Sha256::class; // RS256 // or \Lcobucci\JWT\Signer\Ecdsa\Sha256::class for ES256
See docs/configuration.md for the full reference.
Usage
Compact array payload (default)
$token = $jwt->encode(['user_id' => 1, 'role' => 'admin']); $payload = $jwt->getPayload($token); // ['user_id' => 1, 'role' => 'admin']
Split mode β claims at the top level
$jwt = JWT::for()->withSplitData(); $token = $jwt->encode(['user_id' => 1, 'role' => 'admin']); $claims = $jwt->decode($token); echo $claims->claims()->get('role'); // "admin"
Custom payload claim name
$jwt = JWT::for()->withParamData('payload'); $jwt->getPayload($jwt->encode('hello')); // "hello"
Short-lived tokens (withExpiresAt)
Override the configured expiresAt modifier for a single instance, without mutating the shared config β useful for short-lived access tokens. Like every with*() method it returns a new instance. Passing an empty string throws InvalidArgumentException.
$accessToken = JWT::for()->withExpiresAt('+5 minutes')->encode($data);
Clock skew tolerance (LooseValidAt)
$jwt = JWT::for()->withLeeway(30); // accept up to Β±30s of skew $jwt = JWT::for()->withLeeway(null); // reset to no leeway
withLeeway() accepts null to reset to "no leeway"; a negative value throws InvalidArgumentException.
Per-instance customisers (3.2.0)
Every customiser returns a new instance, leaving the shared config untouched. Override claims, audiences, headers and extra claims per call:
$jwt = JWT::for() ->withIssuer('https://api.my-app.com') ->withAudience('https://app-a.com', 'https://app-b.com') // multiple audiences ->withIdentifier(bin2hex(random_bytes(16))) // unique jti ->withClaims(['scope' => 'admin']) // extra top-level claims ->withHeader('x-trace', $traceId); // custom JOSE header $claims = $jwt->getClaims($token); // validated array of all claims $scope = $jwt->getClaim($token, 'scope'); // validated single claim
Key rotation with kid (3.2.0)
Tag issued tokens with a kid header and verify against a per-kid key map, so you can roll keys without invalidating tokens still in flight:
// Issuing side β stamp the active key id. $token = JWT::for()->withKeyId('2026-06')->encode($data); // Verifying side (app/Config/JWT.php) β accept old and new keys during the window. public ?string $keyId = '2026-06'; public array $verifyingKeys = [ '2026-05' => '/path/old-public.pem', '2026-06' => '/path/new-public.pem', ];
On decode, the token's kid selects the matching key from $verifyingKeys (falling back to $verifyingKey / $signer). The configured signer/algorithm is always used, so a token's kid can never downgrade the verifier.
Error Handling
use Daycry\JWT\Exceptions\InvalidTokenException; use Lcobucci\JWT\Validation\RequiredConstraintsViolated; try { $claims = $jwt->decode($token); } catch (RequiredConstraintsViolated $e) { // Signature, issuer, audience, exp, etc. return $this->response->setStatusCode(401)->setJSON(['error' => $e->getMessage()]); } catch (InvalidTokenException $e) { // Malformed or non-Plain token. return $this->response->setStatusCode(400)->setJSON(['error' => 'Bad token']); }
For a non-throwing flow:
$claims = $jwt->tryDecode($token); if ($claims === null) { return $this->response->setStatusCode(401); }
Fail-closed configuration guards
The library refuses unsafe configurations up front instead of silently producing weak tokens. JWTConfigurationException is thrown when:
jwt.validateClaimsdoes not contain'SignedWith'whilejwt.validate = trueβdecode()will not skip signature verification. To decode without any validation, setjwt.validate = false(intended for tests/debug only;decode()then logs awarning).jwt.algorithmTypeandjwt.algorithmdisagree β'symmetric'requires anLcobucci\JWT\Signer\Hmac\*signer,'asymmetric'requiresRsa\*orEcdsa\*. (E.g. leaving the default HMACSha256on an'asymmetric'type is caught with a clear message instead of a cryptic key error.)
An invalid jwt.canOnlyBeUsedAfter or jwt.expiresAt modifier (anything DateTimeImmutable::modify() rejects) throws InvalidArgumentException consistently across PHP versions.
Utility Methods
| Method | Returns | Description |
|---|---|---|
decode(string $token) |
Plain |
Validates and returns the parsed token. Throws on failure. |
tryDecode(string $token) |
?Plain |
Like decode() but returns null on a token failure. A JWTConfigurationException (misconfiguration) still propagates. |
getPayload(string $token) |
mixed |
Validates + returns the original payload (auto-decoded for compact mode). |
getClaims(string $token) |
array |
Validated array of all claims (the safe counterpart of extractClaimsUnsafe()). |
getClaim(string $token, string $name) |
mixed |
Validated single claim value (null when absent). |
isValid(string $token) |
bool |
True iff tryDecode() succeeds. |
isExpired(string $token) |
bool |
True for malformed/expired tokens. Parses without verifying the signature β never gate access on it. |
getTimeToExpiry(string $token) |
?int |
Seconds until exp, or null. Does not verify the signature. |
extractClaimsUnsafe(string $token) |
?array |
Claims without validation. Logs a warning unless Config::$allowUnsafeExtraction = true. |
CLI Commands
# Publish config to app/Config/JWT.php php spark jwt:publish # Generate an HMAC key (default 32 bytes) and write to .env php spark jwt:key php spark jwt:key 64 --show php spark jwt:key --force # Generate an asymmetric key pair php spark jwt:keypair --algorithm=rsa --bits=2048 php spark jwt:keypair --algorithm=ecdsa --curve=prime256v1 --output=writable/keys
On Windows,
jwt:keypairwarns thatchmod()cannot enforce file permissions β restrict the private key with NTFS ACLs (e.g.icacls) instead. It also warns when--passphraseis passed on the command line, because that value can leak via the process list and shell history; prefer a secrets manager or interactive entry.
Security Best Practices
- Use a strong key β
php spark jwt:keyenforces a 32-byte (256-bit) minimum, the floor for HS256. - Set short expiry times for API access tokens (
withExpiresAt('+15 minutes')). - Enable all validation constraints in production (keep
'SignedWith'injwt.validateClaims). - Never commit
.envor any file containingjwt.signer/ private keys. - Rotate keys without downtime using the
kidheader andjwt.verifyingKeysmap (see Key rotation) β keep the old key in the map until its tokens have expired, then drop it. If a key is leaked, remove it from the map immediately to revoke its tokens.
Testing
composer test # or without coverage (faster) vendor/bin/phpunit --no-coverage
Documentation
π The full, searchable documentation is published at https://daycry.github.io/jwt/ (built with MkDocs Material from the docs/ folder).
| Document | Description |
|---|---|
| Getting Started | Installation and first token in minutes |
| Configuration | Every property, its type, default, and .env key |
| Usage | Complete API reference with examples |
| Advanced | Utility methods, key rotation, middleware, multi-tenant patterns |
| CLI Commands | jwt:key, jwt:keypair, jwt:publish reference |
| Threat Model | Security model and guarantees |
| Testing | Test suite structure and writing new tests |
| Migration v2 β v3 | Upgrade guide |
Contributing
- Fork the repository
- Create a feature branch:
git checkout -b feature/my-feature - Commit your changes:
git commit -m 'Add my feature' - Push and open a Pull Request
License
MIT β see LICENSE.
Support
- π Open an issue for bug reports or feature requests
- π° Donate via PayPal