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.
This release is entirely test-facing: Jwt::fake() now covers the service-to-service side of the package, not just user impersonation. If you already use Jwt::fake()->actingAs($user), the new API is the same shape, one level down.
service-jwt driver, plus jwt.service.issuers (the allow-list of issuers the guard accepts) and jwt.service.secrets (per-issuer secrets, falling back to a shared secret) in config/jwt.php.service-jwt and expects a 200 rather than a 401. Replace any hand-rolled withHeader('Authorization', 'Bearer '.$token) helper with actingAsService().Jwt::fake()->actingAsService('billing', ['scope' => 'invoices:read']);
$this->getJson('/api/invoices')->assertOk();
By default the token is signed with that issuer's own jwt.service.secrets entry if present, otherwise the shared secret, and is addressed to this service as the audience. Pass a third argument to override the audience for cross-service tests.
Jwt::fake()->forgetActing() returns subsequent requests to unauthenticated.Per-test setup, not per-class. The fake holds acting state across requests within a test. Reset it in setUp()/tearDown(), or in Pest via beforeEach(fn () => Jwt::fake()->forgetActing()), so a service identity never leaks from one test case to the next.
Testing multi-tenant / multi-issuer flows. Because actingAsService($issuer, ...) now mints a token that actually carries the foreign issuer (rather than inheriting jwt.service.issuer), you can write a single test that loops over allowed issuers and asserts identical behaviour, and a companion test that feeds a non-allow-listed issuer and asserts a 401. That negative case is the one hosts previously had to hand-encode to exercise.
Claims as a first-class fixture. Put tenant IDs, scopes, and roles in the $claims array rather than stubbing authorization internals. You are then testing the real guard path end-to-end, signature verification included.
Switch identities mid-test. actingAsService('a') → request → actingAsService('b') → request is supported: the second call replaces the acting identity. Use forgetActing() explicitly when you need to prove the endpoint rejects anonymous callers, rather than relying on the absence of a prior actingAs().
Combine with user impersonation carefully. In a test that exercises a user request and a service request, set the service identity immediately before the service call, then forgetActing() and use actingAs($user) for the user call. Don't assume the fake distinguishes request targets for you.
Integration tip: assert the audience. If the calling service relies on audience validation, write one test with the default audience (this service) and one with a mismatched audience to lock in rejection behaviour.
Keep real outbound tokens out of the fake. The app's own outbound tokens still carry jwt.service.issuer as configured. actingAsService() is a test affordance for simulating an inbound caller, not a way to generate production tokens — don't reach for it outside tests.
withoutToken() will not make you anonymous. The fake re-attaches its bearer to any request lacking an Authorization header, so Laravel's token-stripping helper is silently overridden. forgetActing() is the supported escape hatch — this is the single most likely source of confusing "why is this 200?" failures.jwt.service.issuers. actingAsService() will happily mint for any issuer string you give it, but the guard still rejects issuers off the allow-list. A 401 after a successful-looking actingAsService() almost always means the issuer is missing from that config array.jwt.service.secrets wins; the shared secret is the fallback. If you rotate the shared secret in .env but an issuer has a stale explicit entry, your test token verifies against the stale secret and the guard fails against the app's real secret. Check both places when signature errors appear.aud claim may now behave differently once rewritten to use actingAsService(). Pass null/an explicit value deliberately if the endpoint under test doesn't validate audience.Jwt::fake() and reset with forgetActing() in its teardown.iss, aud, and exp in one place resolves most of these in a single pass.How can I help you explore Laravel packages today?