re #96 migrate JWT adapter to lcobucci/jwt 5.x + fix content negotiation#109
Merged
Conversation
added 2 commits
May 28, 2026 07:39
…ation
The Altair\Http\Jwt adapters targeted the lcobucci/jwt 3.x API (removed
Jose\Parsing namespace, Constraint\ValidAt, Validation\InvalidTokenException,
new Key() on an interface, Lcobucci\Clock\SystemClock) while the framework
requires ^5.3 — so the code could not run against the installed library.
JWT (lcobucci 5.x):
- Generator and parser now derive a fresh Lcobucci\JWT\Configuration from the
framework TokenConfigurationInterface instead of injecting shared Builder/Parser
primitives (the builder is immutable in v5; the old shared-builder loop also
silently discarded withClaim() results).
- Parser performs full validation: signature (SignedWith, algorithm fixed to the
configured signer so alg=none / alg-confusion is rejected), issuer (IssuedBy),
and time window (LooseValidAt). The old validateToken() was dead code, so expiry
was never enforced — that gap is now closed.
- Add a PSR-20 Altair\Http\Jwt\SystemClock (injectable, frozen in tests) and
declare psr/clock as a direct dependency.
- Security hardening: reject the unconfigured TOKEN_PUBLIC_KEY placeholder at
runtime (fail fast) and stop interpolating the raw token into parse-failure
exception messages (log-injection / oracle risk).
Content negotiation:
- FormatNegotiator: narrow Negotiator::getBest() (typed as the empty AcceptHeader
marker interface) to BaseAccept before calling getValue(), and flatten the mime
priorities with array_merge(...) — the previous call_user_func('array_merge', ...)
passed an array-of-arrays as one argument, so header negotiation never worked.
Tests: round-trip, tampered-signature / foreign-key / expired / wrong-issuer /
malformed rejection for JWT; Accept-header negotiation for FormatNegotiator.
Regenerate phpstan-baseline.neon: 14 Lcobucci/Negotiation findings drop out;
no entries added. Baseline 65 -> 51 errors.
Address the JWT security-review follow-ups (Findings 4 & 5): - iss is now a stable, configurable issuer (TOKEN_ISSUER) on TokenConfigurationInterface instead of the per-request URI. The old request-URI issuer made a token valid only at the exact endpoint that minted it; the generator and parser no longer depend on ServerRequest. TOKEN_ISSUER is required (fail fast) like TOKEN_PUBLIC_KEY. - Optional audience (aud): when TOKEN_AUDIENCE is configured the generator adds permittedFor() and the parser asserts PermittedFor; absent audience keeps the single-service default (no aud). Also add #[\Override] to the JWT test helpers/fixtures (FrozenClock::now, setUpBeforeClass) so the full-codebase `rector process --dry-run` is clean — these were missed because the migration commit only ran rector against src/. Tests extended for the configured issuer and for audience accept/reject.
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.
Summary
Part of the #96 burn-down. The baseline entries that looked like "optional-dep class-not-found" were actually real API-version-mismatch bugs: the
Altair\Http\Jwt\*adapters targeted the lcobucci/jwt 3.x API while the framework requires^5.3, so the JWT feature could not run against the installed library. Fixed properly (noignoreErrors).JWT — migrated to lcobucci/jwt 5.x
LcobucciTokenGenerator/LcobucciTokenParsernow derive a freshLcobucci\JWT\Configurationfrom the frameworkTokenConfigurationInterfaceinstead of injecting sharedBuilder/Parserprimitives. (The v5 builder is immutable; the old shared-builder loop also silently discardedwithClaim()results — a latent bug.)SignedWith, algorithm fixed to the configured signer →alg=none/algorithm-confusion rejected), issuer (IssuedBy), and time window (LooseValidAt). The oldvalidateToken()was dead code, so token expiry was never enforced — that security gap is now closed and covered by tests.Altair\Http\Jwt\SystemClock(injectable; frozen in tests for deterministic expiry) and declaredpsr/clockas a direct dependency.Security hardening (from independent security review)
TOKEN_PUBLIC_KEYplaceholder at runtime (fail fast) instead of silently issuing unverifiable tokens.issis now a configurableTOKEN_ISSUER(required, fail-fast) onTokenConfigurationInterfacerather than the per-request URI — a token is no longer valid only at the exact endpoint that minted it. The generator/parser no longer depend onServerRequest.TOKEN_AUDIENCE— when set, the generator addspermittedFor()and the parser assertsPermittedFor; absent audience keeps the single-service default.Content negotiation
FormatNegotiator: narrowNegotiator::getBest()(typed as the emptyAcceptHeadermarker interface) toBaseAcceptbeforegetValue(); and flatten mime priorities witharray_merge(...)— the previouscall_user_func('array_merge', …)passed an array-of-arrays as one argument, so header negotiation never worked.PHPStan
phpstan-baseline.neon: 14 Lcobucci/Negotiation findings drop out, none added. 65 → 51 errors.Test plan
composer cs— cleancomposer stan—[OK] No errorsvendor/bin/rector process --dry-run(full codebase) — cleancomposer test— JWT round-trip + tampered/foreign-key/expired/wrong-issuer/malformed rejection + audience accept/reject + Accept-header negotiation all pass; only pre-existing localMongoSessionHandlerTestenv errors (CI loads ext-mongodb)