web-token/jwt-key-mgmt
PHP component for managing JSON Web Keys (JWK) and key sets (JWKSet): load, create, convert, and organize keys for JWT signing and encryption across symmetric and asymmetric algorithms. Part of the Web-Token JWT framework, with utilities for key set handling and selection.
Install (and pick the right package). The repo is marked READ ONLY — the component now ships inside the framework monorepo. On v3 use:
composer require web-token/jwt-framework
On v4/v5 use web-token/jwt-library instead. Only pull web-token/jwt-key-mgmt standalone if you are pinned to the v1/v2 split-package era. Requirements: PHP ext-openssl (and ext-sodium if you touch Ed25519/Ed448 / X25519 keys).
Where to look first — the entire surface you will use day-to-day is four classes under Jose\Component\KeyManagement:
| Class | Job |
|---|---|
JWKFactory |
Turn PEM, secrets, certificates, files, or nothing into a JWK |
JKUFactory |
Fetch a remote JWKS from a jku URL |
X5UFactory |
Fetch a certificate from an x5u URL |
Analyzer\* |
Lint keys for weak parameters |
First realistic use case — you already have a PEM on disk and need a signing key for JWSBuilder:
use Jose\Component\KeyManagement\JWKFactory;
$privateKey = JWKFactory::createFromKeyFile(
storage_path('keys/private.pem'),
password: env('JWT_KEY_PASSPHRASE'), // null if unencrypted
additionalValues: ['use' => 'sig', 'alg' => 'RS256', 'kid' => '2024-01']
);
That JWK is the same object you hand to JWSBuilder, JWTLoader, SignatureChecker, or persist as JSON. For HMAC, skip the file entirely:
$secret = JWKFactory::createFromSecret(config('app.jwt_secret'), ['use' => 'sig', 'alg' => 'HS256']);
1. A key-loading layer, not scattered JWKFactory calls. Wrap creation in a single resolver service keyed by kid. This is the difference between a maintainable token layer and one that grows a new file_get_contents every sprint:
class KeyResolver
{
public function forSigning(string $kid): JWK { /* cache + factory */ }
public function forVerification(string $kid): JWKSet { /* may return set during rotation */ }
}
2. Persist keys as JSON, not as PEM blobs. JWK is JsonSerializable and round-trips through JWKFactory::createFromValues($array). Storing the JWK JSON (in a keys table, in config, in Vault) lets you carry kid, use, alg, and key_ops alongside the material — the metadata is what actually prevents algorithm-confusion bugs later.
3. Rotation via JWKSet, not key replacement. During a rollover, expose the old and new public keys in one set, sign with the new one, and drop the old after the max token lifetime + clock skew. JWKSet is what SignatureChecker consumes; the header kid selects among members.
4. Generation is an admin/CLI concern, never a request concern. JWKFactory::generateRSAKey(4096) and friends are deliberately verbose — run them in an artisan command or deployment step and store the result. Regenerating a key pair per request is the single most common performance footgun with this package.
// artisan jwt:key:generate
$jwk = JWKFactory::generateRSAKey(4096, ['use' => 'sig', 'alg' => 'RS256', 'kid' => Str::uuid()]);
For a public/private pair use createKeyPair() / createRSAKeyPair() and keep only the public half on verifiers.
5. Remote key sets: cache aggressively, validate defensively. JKUFactory makes a synchronous HTTP call through PSR-7/PSR-18. In Laravel, inject a caching wrapper (a few minutes TTL) around it, and never let a jku header decide where to fetch from in a multi-tenant context — allowlist the issuer's JKU URL yourself and pass it explicitly.
6. Analyze before you trust. The bundled analyzers catch weak RSA moduli, missing use, deprecated curves, and short HMAC secrets:
$analyzers = [new \Jose\Component\KeyManagement\Analyzer\AlgorithmAnalyzer(), /* ... */];
foreach ($analyzers as $analyzer) {
$analyzer->analyze($jwk, $collection); // $collection accumulates messages
}
Run this once at boot/deploy and log the results — it turns silent misconfiguration into a startup failure.
7. Framework integration. Register the resolver as a singleton, bind it to your JWSBuilder, and expose the verifying JWKSet to whatever middleware validates tokens. The package is intentionally framework-agnostic; a Laravel service provider that publishes config/jwt.php and lazily resolves keys is the natural shape.
createFromKey vs createFromKeyFile. The former takes PEM content, the latter takes a path. Passing a path to createFromKey produces a confusing "unable to load key" from OpenSSL, not a file error.null. If you get a null JWK back, check openssl_error_string() — the factory swallows the detail.createFromKey tries to infer kty/alg from the material. Always set alg and use explicitly in additionalValues; relying on inference is how you end up signing with a key whose advertised algorithm doesn't match what you built the token with.oct. createFromSecret produces that, but a hand-built createFromValues(['kty' => 'oct', 'k' => base64_encode($s)]) requires the base64url of the raw bytes — a very common off-by-encoding bug.kid is not generated for you. If you use a JWKSet with more than one key, always set kid yourself and put the same value in the JWS header.jku/x5u as an SSRF vector.createFromCertificate expects a real certificate, not a bare public key. Extracting the embedded public key is what it does; a CSR or a PEM public key file will fail.$jwk->all() dumps the full JWK array — the fastest way to confirm alg, use, and key_ops actually match your intent. $jwk->has('d') tells you whether it's a private key.Analyzer for custom policy checks, and wrap JKUFactory/X5UFactory (rather than subclassing) for caching and authentication. The factories are final-ish static helpers — composition is the intended path.Jose\Component\KeyManagement), but the package name moved twice (jwt-key-mgmt → jwt-framework → jwt-library). Pin the package in composer.json and read the upgrade notes before the next minor.How can I help you explore Laravel packages today?