refactor: contain the JWT::$leeway static, and restore it after each decode - #61
Merged
Merged
Conversation
firebase/php-jwt exposes leeway only as JWT::$leeway, a process-global static with no per-call alternative. The write already sat immediately before the decode, but only a comment said it had to, and nothing stopped a future edit from putting a cache read or an HTTP call between them. decodeWithLeeway() now does nothing but set the static and decode, so the invariant has a single home and a stated rationale: no suspension point may come between the two, which is why the verification keys are resolved by the caller before it is entered. Under PHP-FPM that ordering is a formality; under cooperative concurrency it is what stops a fibre decoding with a sibling provider's leeway, and under a preemptive model it would not be enough at all. Behaviour is unchanged. The test suite already carried MockJWT::$leeway for this and never asserted it, so there is now a test pinning the configured leeway to what reaches the decode. The extraction also needed the return type spelled as \stdClass rather than object: `object` erased the narrowing JWT::decode() provides and PHPStan lost $claims->aud.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## develop #61 +/- ##
===========================================
Coverage 100.00% 100.00%
- Complexity 87 88 +1
===========================================
Files 1 1
Lines 220 224 +4
===========================================
+ Hits 220 224 +4
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Setting the process-global is unavoidable — firebase/php-jwt offers no per-call leeway — but leaving it set is a choice. Under PHP-FPM the mutated static dies with the request. In a worker process it persists for the life of the process and silently applies to every other firebase/php-jwt consumer in it that never sets its own leeway, which makes this library a bad neighbour in exactly the runtime it otherwise suits. The restore is in a finally block, so a rejected token cannot leave our leeway applied either. Both paths are tested: the value in effect during the decode, and the value left behind after a success and after a SignatureInvalidException.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
firebase/php-jwtexposes clock-skew tolerance only asJWT::$leeway, a process-global static with no per-call alternative. Two problems follow from that, one inward and one outward.Inward: the invariant had no home
The write already sat immediately before the decode, but only a comment said it had to, and nothing stopped a future edit from putting a cache read or an HTTP call between them.
decodeWithLeeway()now does nothing but set the static and decode, so the constraint has a single home and a stated rationale: no suspension point may come between the two, which is why the verification keys are resolved by the caller before it is entered.Under PHP-FPM, where a request owns its process, that ordering is a formality. Under cooperative concurrency it is what stops a fibre decoding with a sibling provider's leeway, and under a preemptive model it would not be sufficient at all — noted in the docblock so a future move to a worker runtime has something to find.
Outward: the static stayed written
Under PHP-FPM the mutated static dies with the request, so this was invisible. In a worker process it persists for the life of the process and silently applies to any other
firebase/php-jwtconsumer in it that never sets its own leeway — a token that should have been rejected as expired could be accepted, because this library happened to run first with a laxer setting.The previous value is now restored in a
finally, so a rejected token cannot leave our leeway applied either. Writing a process-global is unavoidable given the upstream API; leaving it written is not.Behaviour inside this library is unchanged.
Tests
The suite has carried
MockJWT::$leewayfor exactly this purpose and never asserted it. Now three things are pinned:decode()rather than after the factSignatureInvalidExceptionAsserting from inside the decode is what makes the restore testable at all — checking the static afterwards can only ever see the restored value.
Verification
137 tests, all green. Coverage 100% (31/31 methods, 194/194 lines). Mutation: 241 generated, 237 killed, 4 ignored, zero escaped, MSI 100%. PHPStan max clean at the ceiling and at the dependency floor, php-cs-fixer, markdownlint, prettier and
composer normalizeall clean.Note on the return type
The extraction needed
\stdClassrather thanobjectas the return type:objecterased the narrowingJWT::decode()provides and PHPStan lost$claims->audin the caller.