roundly-consulting/jwt-for-laravel
JWT authentication for Laravel: issue, validate, and refresh JSON Web Tokens with configurable guards, middleware, and token handling that integrates cleanly into Laravel applications and APIs.
Architecture fit
This release sharpens the package's position as a service-to-service authentication layer inside Laravel, not just a user-auth JWT library. The service-jwt guard, the per-issuer config (jwt.service.secrets, jwt.service.issuers, jwt.service.issuer), and HS256 signing describe a symmetric-secret trust mesh: each calling service signs with its own issuer identity, the receiving service validates against an allow-list of issuers and resolves the matching secret. For a TPM, that is a meaningful architectural statement — the package assumes services are known to each other and share provisioning, which is a good fit for a bounded internal platform (5–30 services, one platform team) and a poor fit for an open or partner-facing API surface.
The 1.2.0 additions are test-fidelity work, not runtime capability. Jwt::fake()->actingAsService($issuer, $claims, $audience) makes the fake mint a real HS256 token the way a foreign issuer would sign it — resolving that issuer's own jwt.service.secrets entry, falling back to the shared secret, and addressing the audience to the host service. Previously the fake retained the issuer it was constructed with, so cross-service tests either could not be expressed or were hand-encoded by developers. That hand-encoding is the real architectural liability this release removes: test tokens written by hand inevitably drift from the production signing and claim-assembly path, and the drift is silent until a test passes that should have failed.
forgetActing() is the more consequential fix, and it is worth flagging to stakeholders as a correctness improvement rather than a convenience. Because the fake re-attaches its bearer to every request lacking an Authorization header, withoutToken() (Laravel's standard anonymous-request helper) could not produce an unauthenticated request. Any test asserting "this endpoint rejects anonymous callers" was, in a faked context, potentially asserting nothing. Teams that adopted the fake widely should assume some of their negative-path auth coverage is weaker than it appears and re-verify it after upgrading.
Integration feasibility
Low. This is an additive minor release (1.2.0) on an existing public surface: two new methods on the object returned by Jwt::fake(), no signature changes, no config schema changes, no deprecations. Existing tests continue to compile and behave as before — including, importantly, the previously surprising withoutToken() behaviour, which is not silently corrected. That last point cuts both ways: upgrading is safe, but the fix only lands where a developer explicitly opts in by calling forgetActing(). There is no automated way to find tests that are currently under-asserting; a grep for withoutToken( inside faked test classes is the practical survey, and it will only find the tests someone thought to write.
The integration work is therefore concentrated in test suites, not production code. That is cheap to schedule and easy to scope, but it needs a deliberate sweep rather than a dependency bump, because the value is in the tests that don't exist yet.
Technical risk
forgetActing(). This is an improvement over having no escape hatch, but it introduces a stateful helper whose correct use is convention-based. Recommend a base test-case or trait that calls forgetActing() in tearDown() so the safe default is structural, not remembered.actingAsService() resolves the issuer's secret from jwt.service.secrets and falls back to the shared secret. Teams that have only ever run against the shared secret may discover, once they mint for a foreign issuer, that their per-issuer entries are absent or wrong — a test-time discovery of a latent production misconfiguration. That is a good failure, but it will land as unplanned work.jwt.service.issuers. Good — the new method cannot be used to bypass the allow-list. Confirm this remains true and that a negative test for an unknown issuer exists alongside the new positive-path tests.actingAsService() and forgetActing() take optional positional parameters ($claims, $audience). Once consumers depend on them, renaming or reordering becomes a breaking change in test code across every adopting team. Worth pinning and treating as public API in the semver contract.$audience is null, the token is addressed to "this service" implicitly. Convenient, but it means a test can pass without the author ever stating which audience it is testing. Ask for the default to be documented explicitly so reviewers know what a null-audience test actually proves.Key questions
actingAsService() with an issuer absent from jwt.service.issuers is still rejected by the guard?$audience is null, is the minted aud the host service's configured identifier, and is that documented?forgetActing() safe to call in tearDown() unconditionally, including in tests that never used the fake?jwt.service.secrets entry but the shared secret is also unset, or does it degrade to an unsigned/invalid token?Stack fit
Assumes a Laravel application on a supported PHP version with the package already installed for service-jwt guards. No new runtime dependencies are implied by the release notes — the additions are within the existing fake and signing path. Nothing here touches queue workers, scheduled jobs, or HTTP middleware registration, so the integration footprint is limited to the tests/ tree. Teams not using Jwt::fake() at all get no benefit and no risk from this release; teams using it heavily get the most value, which conveniently correlates with where the payoff is.
Migration path
Upgrade is a standard minor-version bump with no code changes required. The deliberate part is a two-track follow-up:
actingAsService($issuer, $claims, $audience). This is the primary reason to take the release: it collapses bespoke signing code into one path that the package maintains, and it is what makes cross-service authz tests expressible at all. Prioritise tests covering issuer allow-list enforcement, audience mismatch, and claim-based authorisation — the three places where hand-encoded tokens were most likely to be subtly wrong.withoutToken() or otherwise intend to assert unauthenticated behaviour while the fake is active, and add forgetActing() before the request. Where a test never asserted the anonymous case at all, add it — these endpoints are exactly the ones where a missing auth check would go unnoticed.Do track 2 as a distinct work item with its own acceptance criteria, not as cleanup folded into track 1. Its output is evidence about production security posture, and it may surface endpoints with genuinely missing authorisation rather than merely missing tests. Route anything it finds through normal security triage rather than fixing it quietly in a test branch.
Compatibility
No breaking changes. The one compatibility subtlety is behavioural, not API: because withoutToken() still fails to produce an anonymous request in a faked context, a test that is upgraded but not annotated will keep passing for the wrong reason. There is no error, no deprecation notice, and no way to detect it from the upgrade itself. This is the reason the sweep must be explicit. Also verify that any internal base test case, trait, or package-provided test helper that wraps Jwt::fake() is updated to expose the two new methods — a wrapper that returns $this->fake but predates 1.2.0 will hide them.
Sequencing
^1.2 and bump in the shared dev dependency set so all services move together — divergence here produces test suites that behave differently per repo for the same code.forgetActing() in tearDown(), making the safe default structural before anyone starts writing new tests.actingAsService(), starting with the highest-value authz tests.Maintenance
Net reduction. Every hand-encoded token removed from the test tree is signing logic no longer maintained by the consuming team; the package now owns issuer selection, secret resolution, audience addressing, and claim merging. Expect a modest one-time cost to delete those helpers and the config duplication around them, and a small ongoing cost to track the fake's API as a compatibility surface. The tearDown() convention, once in the base case, is zero-maintenance.
Support
Low and largely self-inflicted if the anonymous-path gap is not addressed. Support burden splits cleanly: actingAsService() questions will be "which issuer secret did it pick" (answerable from config), while withoutToken() questions will be "why is my anonymous test authenticating" — a confusing symptom with a one-line fix that is hard to find without documentation. Pre-empt the second category with the internal note in step 5; it will pay for itself in the first week.
Scaling
Scales with the number of services in the trust mesh, not with traffic — this release is test-time only and adds nothing to the request path. What does scale is the configuration surface: one jwt.service.secrets entry and one jwt.service.issuers entry per counterpart, per environment. As the mesh grows, that becomes an N²-shaped provisioning problem and a rotation-coordination problem, and HS256 means every validator can mint. If the platform is heading past roughly a dozen services or crossing an organisational trust boundary, the right move is to evaluate asymmetric signing rather than to invest further in symmetric-secret tooling. Flag that as a forward-looking decision, not a defect in 1.2.0.
Failure modes
forgetActing().actingAsService() resolving a per-issuer secret that production has never actually configured. Surfaces as a failing test in an unrelated change; treat as a real config finding, not test flakiness.actingAs() / actingAsService() persisting across requests or tests without forgetActing(), producing order-dependent suites. Mitigated by the tearDown() convention.Ramp-up
Roughly half a day for a team already using Jwt::fake(): read the changelog, add the tearDown() hook, convert a handful of tokens, run the sweep. Teams not using the fake should not adopt it solely for this release — the cost/benefit only works where service-auth tests already exist or are about to be written. One caveat worth stating plainly to engineering leadership: the sweep in step 4 may uncover endpoints lacking anonymous-rejection tests today. Budget for that discovery rather than presenting the upgrade as purely mechanical, because the finding is the point.
How can I help you explore Laravel packages today?