spomky-labs/jose
spomky-labs/jose is a PHP library for JSON Object Signing and Encryption, offering JWS, JWE, JWK and JWT support. It helps developers sign, encrypt, verify and manage keys for secure tokens and payloads in web applications and APIs.
spomky-labs/jose is a full JOSE stack for PHP — JWS (signing), JWE (encryption), JWK/JWKSet (keys), JWA (algorithms) and JWT on top. It is archived (last release May 2018), so treat it as a legacy-maintenance dependency, not a greenfield choice. If you are starting fresh, use its direct successor web-token/jwt-framework (same author, same object model) or lcobucci/jwt; the patterns below transfer almost 1:1.
Install and pin
composer require spomky-labs/jose:^3.0
The package targets PHP 7.x. On PHP 8+ you will fight composer install unless you --ignore-platform-reqs, and even then the pure-PHP EC backend (mdanter/ecc) has known issues — prefer RSA (ext-openssl) or HMAC.
Where to look first
| Namespace | What lives there |
|---|---|
Jose\Factory\ |
JWKFactory, JWSFactory, JWEFactory — your entry points |
Jose\Object\ |
JWKInterface, JWKSetInterface, JWSInterface, JWEInterface — type-hint these, not concretes |
Jose\ |
Signer, Verifier, Encrypter, Decrypter, Loader |
Jose\Checker\ |
Claim validators (ExpirationTimeChecker, NotBeforeChecker, …) |
Ignore the README's crypto-primer sections on first read; go straight to Factory\ and the Signer/Verifier pair.
First use case: sign and verify an HS256 token
use Jose\Factory\JWKFactory;
use Jose\Factory\JWSFactory;
use Jose\Signer;
use Jose\Verifier;
use Jose\Loader;
use Jose\Checker\ExpirationTimeChecker;
use Jose\Checker\NotBeforeChecker;
// 1. Key
$jwk = JWKFactory::createFromSecret('a-long-shared-secret', [
'alg' => 'HS256',
'use' => 'sig',
]);
// 2. Build and sign
$jws = JWSFactory::createJWSWithClaims([
'iss' => 'https://app.example.com',
'sub' => 'user-42',
'iat' => time(),
'exp' => time() + 3600,
]);
$jws->addSignature($jwk, ['alg' => 'HS256']);
Signer::createSigner(['HS256'])->sign($jws);
$token = $jws->toJSON(); // see Gotchas: this is the JSON serialization
// 3. Load and verify
$loaded = (new Loader())->load($token);
$verifier = Verifier::createVerifier(
['HS256'], // algorithm allowlist — never wildcard
[new ExpirationTimeChecker(), new NotBeforeChecker()]
);
$verifier->verify($loaded, $jwk);
$claims = $loaded->getPayload();
If that round-trip works, everything else in the package is a variation on key type, algorithm, and claim checks.
Wrap it in one service class, immediately. The package has no Laravel service provider. Do not sprinkle Signer::createSigner() through controllers — you will be migrating off this library, and the successor's API is similar but not identical. One seam makes that a one-file change.
// app/Support/Jwt.php
final class Jwt
{
public function __construct(
private JWKInterface $signingKey,
private JWKSetInterface $verificationKeys,
) {}
public function issue(array $claims): string
{
$jws = JWSFactory::createJWSWithClaims($claims + ['iat' => time()]);
$jws->addSignature($this->signingKey, [
'alg' => 'RS256',
'kid' => $this->signingKey->get('kid'),
]);
Signer::createSigner(['RS256'])->sign($jws);
return $jws->toJSON();
}
public function parse(string $token): array
{
$jws = (new Loader())->load($token);
Verifier::createVerifier(['RS256'], [
new ExpirationTimeChecker(),
new NotBeforeChecker(),
])->verify($jws, $this->verificationKeys);
return $jws->getPayload();
}
}
Bind it as a singleton in a service provider; Signer/Verifier are cheap and stateless, but the JWK objects are worth constructing once.
Stateless API auth middleware.
public function handle(Request $request, Closure $next)
{
$claims = app(Jwt::class)->parse($request->bearerToken());
$request->attributes->set('jwt', $claims);
Auth::setUser(new \App\Models\User((string) $claims['sub']));
return $next($request);
}
Prefer $request->attributes over Auth::setUser if you want the token usable outside a Laravel auth guard (queues, artisan commands).
OIDC / third-party token validation via JWKS. JWKFactory::createFromJKU() performs a live HTTP fetch. There is no caching inside the package, so cache the serialized set and rehydrate.
$jwkset = Cache::remember('oidc:jwks', now()->addHour(), function () {
return JWKFactory::createFromJKU('https://idp.example.com/.well-known/jwks.json');
});
Rehydrate from cache with new JWKSet(json_decode($cached, true)) rather than storing the object. On an unknown kid (key rotation at the IdP), bust the cache and refetch once before failing.
Key rotation with kid. Keep an array of JWKs in a JWKSet, tag each with a distinct kid, and sign with the active one. The Verifier matches against the whole set, so old tokens stay valid through a rotation window — this is the main reason to use a JOSE library rather than hash_hmac by hand.
Custom claim checks as checkers. Jose\Checker is the intended extension point. Implement the checker interface to enforce scope, tenant, or aud in the same pass as exp, instead of forgetting them in application code.
Nested JWTs for sensitive payloads. Sign with JWSFactory, then encrypt the resulting token with Encrypter::createEncrypter(['RSA-OAEP'], ['A256GCM']). Decrypt first, then verify — never the reverse, or you are verifying attacker-controlled ciphertext.
Testing. Generate a throwaway key in setUp() with JWKFactory::createFromSecret() or a fixture PEM; never commit production keys or make test suites depend on a network JWKS fetch.
toJSON() is not compact serialization. It returns the general JSON form ({"payload":…,"signatures":[…]}), not the xxx.yyy.zzz string most clients and Authorization: Bearer headers expect. This is the single most common first-hour bug. Check the API of the tag you pinned for the compact accessor, or serialize the detatched form yourself.exp is not enforced by default. The Verifier only runs the checkers you hand it. A token with exp in the past verifies fine if you omit ExpirationTimeChecker. Same for nbf, iat, aud and iss — build the checker list explicitly, and add audience/issuer checks yourself if the bundled Checker namespace doesn't cover them.Verifier::createVerifier([$tokenAlg], …) reading the algorithm from the token is the classic alg: none / HS256-vs-RS256 confusion attack. Hardcode the expected algorithm(s) at the call site.kid to select keys. Match kid against your known set; a kid pointing at an attacker-supplied JWK (especially via jku/x5u headers) is a key-injection vector. Ignore jku/x5u entirely unless you have a strict host allowlist.getPayload() vs. claim accessors) and signature handling changed. Pin an exact minor version and verify sample code against the tag you installed, not against blog posts.JWKFactory::createFromJKU() has no timeout or retry. A slow IdP blocks your request thread indefinitely. Wrap it, or fetch JWKS out-of-band in a scheduled job.JWK array, k, n, e, d must be base64url-encoded (no padding). createFromSecret() and createFromKeyFile() handle this; raw arrays from a config file usually don't.mdanter/ecc is pure PHP. Unless you specifically need small signature sizes or are targeting JOSE conformance, RS256 is faster and better supported on PHP 7/8.$e->getPrevious(), and dump $jws->getSignatures()[0]->getProtectedHeaders() when debugging — the headers usually reveal the mismatch.composer audit on the lockfile and budget the migration to web-token/jwt-framework — the object model (JWKSet → JWKFactory → JWSFactory → Signer/Verifier) is close enough that the service-class wrapper above can be rewritten in an afternoon.How can I help you explore Laravel packages today?