medzuch/jwt-bundle
medzuch/jwt-bundle is a Symfony bundle for JWT authentication, offering configurable services to issue, validate, and handle JSON Web Tokens in PHP applications. It streamlines token-based auth setup and integration with Symfony's service container.
What shipped. v1.2.0 is an interface-only release: a Refresh\ namespace containing RefreshTokenStoreInterface (four operations — store, consume, revokeGrant, revokeAllFor), a concrete RefreshTokenGenerator exposed as the Symfony service medzuch_jwt.refresh_token_generator, and the RefreshToken/RefreshTokenRecord value objects. No configuration keys, no services re-shaped, no migrations, no default storage implementation, no call sites. The library floor is unchanged at medzuch/jwt-php ^1.2.1.
Architecture fit. The package is a Symfony bundle, not a Laravel package — no service provider, no publishable config, no migrations, no facade. Consumed from a Laravel codebase it degrades to a plain Composer PHP library: the contracts and value objects are usable, the container wiring (medzuch_jwt.refresh_token_generator) is not, and would need a manual binding in a service provider. The design intent is explicit and worth respecting: the bundle owns vocabulary, not persistence. That is a defensible boundary (rotation, device records, and logout-everywhere are genuinely schema- and product-specific) but it means the package delivers the cheap half of the feature. The expensive half — the token table, the atomic conditional write, replay handling, expiry sweeps — is entirely ours.
Integration feasibility. Low friction to install, high friction to finish. Three things are genuinely well-designed and reduce our risk: consume() spends and reports a prior spend in one call, closing the find-then-delete race that causes double-use; RefreshToken derives its own SHA-256 so client value and stored hash cannot disagree; and the two revocations map cleanly to RFC 9700 §4.14.2 (lineage revocation on replay vs. revoke-all on password change). The fast-hash choice is correct for 256 bits of random_bytes() and avoids putting a slow KDF on the refresh hot path. That said, the interface is inert until we write a store, and the store is where the correctness risk lives.
Technical risk.
RefreshTokenStoreInterface is published at 1.x. Adding a fifth method later is a breaking change for every implementor. There is no stated deprecation policy for the interface itself; we should assume additive changes arrive as new interfaces, and confirm.alreadyUsed: true is reported, not acted on. Nothing revokes the lineage automatically. A store that returns the flag and moves on is a silent security hole.Key questions for the maintainers. Is the interface intended to be frozen at four methods for the 1.x line? Is there a recommended purge/retention contract, even informally? Does consume() have a required atomicity guarantee we can hold them to (unique index on hash + conditional update), or is that purely implementor discretion? And for a Laravel consumer: is the generator constructible outside a Symfony container, or is the service definition the only supported entry point?
Stack fit. Poor-to-moderate for a Laravel shop. We get two framework-agnostic value objects and a token generator we can construct manually; we do not get bundle auto-registration, config publishing, or any Laravel-idiomatic packaging. If the goal is refresh tokens in a Laravel app, Sanctum or Passport is the shorter path and carries the migrations we would otherwise write. If the goal is interoperability with sibling Symfony applications — the package's stated motivation — the vocabulary has real value. Decide which goal we have before adopting.
Migration path. Effectively zero-risk for existing consumers: nothing in configuration changes, no injected service changed shape, and an application that ignores the namespace upgrades by moving the constraint. UPGRADE.md is the whole story. For us there is nothing to migrate from; this is a greenfield adoption decision, not an upgrade.
Compatibility. PHP-only dependency chain via medzuch/jwt-php ^1.2.1; no framework version coupling introduced here. The Symfony-container coupling on the generator is the one portability gap. No Flex recipe will ship (DEC-9), which is the right call — Flex auto-registers bundles by package type, and a recipe's starter YAML would be a hand-maintained copy that drifts from the generated reference tree. The consequence for us is purely procedural: registration and setup come from jwt:key:generate plus the README quickstart, not from composer require alone.
Sequencing.
RefreshTokenGenerator and our store implementation.consume() as a single conditional write; test it under concurrency before anything else.alreadyUsed, call revokeGrant() and fail the request.Maintenance. The package surface is small and the dependency floor is loose, so version churn should be minimal; expect to track a 1.x line with occasional additive contracts. Our real maintenance burden is the store implementation and its schema, which we own outright and which will outlive any particular version of this bundle. Budget for it as first-party code, not as glue.
Support. Documentation is the support channel: README, UPGRADE.md, and docs/plan.md §8.1 carrying the rationale since v0.4. That is unusually good for a small package, but it is documentation of intent, not an SLA. There is no default implementation to lean on when something misbehaves at 2 a.m., and no upstream code path to point at — the failure will be in our store.
Scaling. One indexed write per refresh, one read-and-write per consume, one indexed update per revocation. Cheap at any realistic session volume. The scaling concerns are index design (hash for lookup, subject for revoke-all, lineage for revokeGrant) and table growth without a sweep. A unique constraint on the hash is not optional — it is what makes the atomicity guarantee real. Refresh-path latency must stay flat; the fast-hash decision protects that, and we should not "improve" it to password_hash().
Failure modes.
consume() → the same token is spent twice; the flag exists but is never observed.alreadyUsed reported and ignored → replay goes undetected, lineage lives on.null rather than throwing, which is the desired invalid_grant behavior but means silent misconfiguration looks like an auth failure, not a bug.Ramp-up. Small and self-contained: four operations, two value objects, one generator, roughly a day for a competent engineer to read the README and UPGRADE.md and produce a first store. The higher cost is conceptual, not syntactic — the team must internalize why storage is out of scope, why consume() is one call, and why the two revocations are distinct. One reviewer with the RFC 9700 §4.14.2 context should own the store review; the code will look trivially correct while being wrong in the window it was designed to close.
How can I help you explore Laravel packages today?