sgoranov/identity-link-shared
Shared identity-linking utilities for Laravel and PHP applications, offering common contracts and helpers to associate user accounts across services with consistent, reusable logic.
This release is a good moment to adopt the package if you have not already, but do the dependency check before the feature work.
symfony/http-foundation to 6.4.48. The bundle's own lock file changing does not patch your application. Run:
composer why symfony/http-foundation
composer update symfony/http-foundation --with-dependencies
composer audit
Confirm the resolved version is >=6.4.48 via composer show symfony/http-foundation. Laravel pulls this package in transitively, so a Laravel app can be exposed to the ReDoS even if it never touches this bundle directly.https://api.example.com), wire the guard to your api middleware group, mint a token with that aud, and hit a protected route. Get the happy path and the 401 path working before touching the audience list.Where to look first, in order: the guard registration, the audience config key, the token-validation middleware, and the aud claim handling in the validator.
Keep one audience per guard. The healthy default is a 1:1 mapping between a deployable service and an audience string. Config like:
'guards' => [
'api' => [
'driver' => 'jwt',
'audiences' => [env('JWT_AUDIENCE')], // single entry, not a list
],
],
Resist adding entries "just in case." Every extra value widens what your service accepts.
Multi-audience as an explicit migration tool. The new list support is genuinely useful for two scenarios:
api-v1 to api-v2. List both during the overlap window, deploy consumers first, then remove the old value once issued-token volume for it hits zero. Track that removal as a task with a date — these lists have a habit of becoming permanent.Assert on the matched audience, not just success. If your validator exposes which entry matched, propagate it into the request (e.g. a request attribute) so downstream authorisation can branch. If it does not expose it, re-decode the aud claim yourself in a small middleware rather than re-parsing the raw header in controllers.
Mint tokens through one factory. Centralise token creation (claims, issuer, expiry, aud) in a single service or test helper. When the audience config changes, a scattered JWT::encode call is how you get tokens that fail in staging but pass locally.
Feature-test both sides of the boundary. Use a data provider over the configured audiences:
it('accepts tokens for each configured audience', function (string $aud) { /* ... */ })
->with(fn () => config('jwt.audiences'));
Plus negative cases: an unlisted audience, a token with no aud, and a token with aud as a bare string rather than an array. Those last two are your regression net for the new matching logic.
Middleware ordering. Put the token validation middleware before anything that reads the Cache-Control header (response caching, ETag, surrogate-key middleware). Given the ReDoS was header-parsing-driven, rejecting malformed or unauthenticated requests as early as possible limits the blast radius of any future parser issue.
Any-to-any matching is the headline risk. A token is accepted if any of its aud values matches any configured audience. So with ['api-a', 'api-b'] configured, a token minted solely for api-b will pass a check on a service that identifies as api-a. This is an intersection, not a subset or per-audience check. If you were expecting "the token must be for the audience I name," you will be surprised. Audit any config with more than one entry before shipping.
No priority or ordering semantics. Don't design flows that depend on which audience matched — the acceptance rule is a boolean over the cross-product. If you need to distinguish, do it explicitly downstream.
aud is polymorphic in the wild. The JWT spec permits aud to be a single string or an array. Confirm the package normalises both, and add a test for whichever form your issuer actually emits. A list-only comparison against a string claim (or vice versa) is a silent 401.
CVE-2026-108082 remediation is on you. The advisory names HeaderUtils::split() reached through the Cache-Control request header — that is a request-time path, so every route that parses Cache-Control (including caching middleware and some proxies/edge layers) is a candidate. After bumping:
composer audit in CI, not just locally.symfony/http-foundation; a stale transitive constraint can hold you below 6.4.48 while composer update reports success.composer require does not silently downgrade the resolved version.Verify the fix is actually resolved. composer show symfony/http-foundation in the built artifact (container image, not just the dev checkout) is the only reliable confirmation.
Debugging 401s. In order: (1) decode the token's aud and compare to the live config value — including trailing slashes and scheme, which are the most common mismatch; (2) confirm you are reading the config from cache in the right environment (config:clear after changing audience values); (3) check clock skew and any leeway setting if the failure is intermittent. Log the expected vs. received audience identifiers at warn level — never log the token itself.
Config caching quirks. If the audience list is assembled in a service provider or from a runtime source, it will be frozen into the cached config. Anything dynamic must be injected at validation time, not baked into config.
Don't over-index on the bundle's lock file. Release notes mention the bundle's own dependency pin, which protects the package's test suite, not your app. It is a useful signal that 6.4.48 is the safe floor; it is not a fix you receive for free.
Extension point worth checking. If you need stricter audience semantics than any-to-any (likely for multi-tenant), the validator's audience-matching step is the place to override or wrap — not the controller. Doing it at the edge keeps the rule uniform across every guarded route.
How can I help you explore Laravel packages today?