spomky-labs/lexik-jose-bridge
Integrates LexikJWTAuthenticationBundle with the Spomky-Labs JWT Framework, letting Symfony apps create and verify JWTs using JWT Framework services, key management, and configurable signing algorithms.
First, calibrate expectations: spomky-labs/lexik-jose-bridge is a Symfony integration package. It wires the web-token/jwt-framework (Jose) components into lexik/jwt-authentication-bundle via DI configuration. In a Laravel app you almost never install the bridge itself — you consume the underlying Jose library directly and hand-wire it. The bridge's README and config tree are still the best reference for how the pieces fit together.
Practical starting points:
# What you actually install in Laravel
composer require web-token/jwt-library:^3.3
# What the release notes are about (Symfony only — do not require in Laravel)
# composer require spomky-labs/lexik-jose-bridge:^4.0
Where to look first:
composer.json of the bridge — it pins the exact web-token/* and symfony/* ranges that v4.0.0 validates against. Copy those constraints for your own install.DependencyInjection/ folder — it shows every Jose service (JWSBuilder, JWSVerifier, JWKFactory, HeaderCheckerManager, ClaimCheckerManager) and the constructor arguments each needs. That is your service-provider blueprint.First use case: sign and verify your own JWTs (API tokens, service-to-service auth, webhook signatures) with RS256 and a keyset you control, rather than delegating to tymon/jwt-auth or firebase/php-jwt.
// app/Providers/JoseServiceProvider.php
public function register(): void
{
$this->app->singleton(AlgorithmManager::class, fn () => new AlgorithmManager([
new RS256(),
]));
$this->app->singleton(JWSBuilder::class, fn ($app) => new JWSBuilder(
$app->make(AlgorithmManager::class)
));
}
Signing with an asymmetric key. Keep the private key outside the repo (storage/keys/private.pem, chmod 600), load once, cache the JWK object.
$jwk = JWKFactory::createFromKeyFile(
storage_path('keys/private.pem'),
null,
['use' => 'sig', 'alg' => 'RS256', 'kid' => config('jose.kid')]
);
$jws = $builder
->create(['alg' => 'RS256', 'kid' => $jwk->get('kid')])
->withPayload(json_encode($claims, JSON_THROW_ON_ERROR))
->build();
$jws = $signer->sign($jws, $jwk, 0);
return (new CompactSerializer())->serialize($jws, 0);
Verification pipeline. Never call the verifier bare. Build a JWSLoader with a header checker (pins alg + kid) and a claim checker (enforces exp, nbf, iat, aud, iss). This is exactly the composition the bridge performs for Lexik.
$headerChecker = new HeaderCheckerManager(
[new AlgorithmChecker(['RS256']), new TokenTypeChecker(['JWT'])],
[new AlgorithmParameterManager()]
);
$claimChecker = new ClaimCheckerManager([
new ExpirationTimeChecker($clockSkew = 30),
new IssuedAtChecker($clockSkew),
new NotBeforeChecker($clockSkew),
new AudienceChecker(config('jose.audience')),
]);
Key rotation via JWKS. Publish /.well-known/jwks.json from a JWKSet, include kid in every token, and let verifiers select by kid. Consumers should Cache::remember() the JWKS for minutes, not hours, and re-fetch once on an unknown kid before failing.
Multi-tenant / per-issuer keys. Bind a small resolver class that maps iss → JWKSet, and inject it into the loader. This mirrors the bridge's key_loader / signature_algorithm config keys without the Symfony DI baggage.
Middleware integration. Wrap verification in an AuthenticateJoseToken middleware: pull the Bearer token, verify, stash claims on the request ($request->attributes->set('jose.claims', $claims)), and let a policy/gate consume them. Throw AuthenticationException so Laravel renders 401 consistently.
Outbound calls. For service-to-service, put signing in a JoseClient decorator around Illuminate\Http\Client\PendingRequest so every request gets a short-lived (~60s) Authorization: Bearer header, and verify the counterpart's JWKS on responses if signed.
Testing. Jose uses wall-clock time internally — you cannot freeze it. In feature tests, mint tokens with explicit iat/exp relative to now() and assert on rejection windows (exp = now-1) rather than mocking clocks.
composer require the bridge in Laravel. v4 pulls lexik/jwt-authentication-bundle and Symfony 6 components. It will resolve, but you inherit bundle config, Flex recipes, and a service container you don't use. Take the library, not the bridge.symfony/*:^5.4 (common in older packages), Composer will deadlock on the bridge. Your Laravel app is unaffected if you only install web-token/jwt-library, but pin your symfony/* ranges if you do pull it in.AlgorithmManager and AlgorithmChecker. Never let the token's alg header select the algorithm, and reject none outright.ExpirationTimeChecker has zero drift. Give 30–60s of tolerance or you will get spurious 401s across containers with slightly skewed clocks.[], not {}. json_encode([]) produces a JSON array, which violates the JWT spec's object requirement. Cast to (object) or use new \ArrayObject([]).JWKFactory::createFromKeyFile() wants PKCS#1/PKCS#8 PEM. Keys emitted by modern openssl genpkey are fine; .p12/.pfx bundles need createFromPKCS12CertificateFile() and a password argument.web-token/jwt-framework vs web-token/jwt-library. The framework package includes the Symfony bundle; installing both causes duplicate class maps. In Laravel, install only jwt-library (or the individual jwt-signature / jwt-key-management components).CompactSerializer::unserialize() and inspect $jws->getSignature(0)->getProtectedHeader() — the kid and alg there are what your header checker is validating. Log the kid on every verification failure; mismatched kid is the single most common cause of "valid token rejected".kid, then cache the new set. Otherwise key rotation causes a hard outage until your TTL expires.DependencyInjection/Configuration.php tree in the bridge is an excellent specification of sane defaults for each Jose service. Read it, replicate it in config/jose.php, and skip the bundle.firebase/php-jwt is a tenth of the weight. Reach for Jose when you need JWK/JWKS, key rotation, kid-based selection, or multiple algorithms — that is where web-token earns its complexity.How can I help you explore Laravel packages today?