Skip to content

feat: Socialite OIDC JWKS refresh#388

Merged
binaryfire merged 3 commits into
0.4from
feat/socialite-oidc-jwks-refresh
May 15, 2026
Merged

feat: Socialite OIDC JWKS refresh#388
binaryfire merged 3 commits into
0.4from
feat/socialite-oidc-jwks-refresh

Conversation

@binaryfire
Copy link
Copy Markdown
Collaborator

This PR makes Socialite's OpenID Connect token verification resilient to provider JWKS rotation in long-running workers.

Previously, OpenIdProvider fetched the provider discovery document and JWKS once, cached the parsed keys on the provider instance, and reused them for the worker lifetime. That is fast, but under Swoole it means a provider key rotation can leave a worker validating ID tokens against stale keys until restart.

The provider now refreshes discovery/JWKS and retries verification when Firebase JWT reports a stale-key-shaped failure:

  • an ID token references a new kid that is not in the cached JWKS
  • an ID token uses an existing kid but the cached key material no longer verifies the signature

Malformed tokens, missing kids, invalid claims, nonce failures, issuer failures, and audience failures still fail normally without forcing JWKS refresh.

Details

  • Adds forced-refresh support to OpenIdProvider::getOpenIdConfig() and getJwks().
  • Retries JWT::decode() once after a forced JWKS refresh for unknown kid and stale key material cases.
  • Adds a small per-provider cooldown for forced JWKS refresh attempts so repeated bad unknown-kid tokens cannot turn verification into unlimited JWKS HTTP requests.
  • Keeps the normal hot path unchanged: valid tokens continue to use the cached JWKS with no extra HTTP call.

This intentionally avoids adding a PSR-6 cache adapter or switching to Firebase's CachedKeySet. The issue is narrow, key rotation is rare, and a local retry/cooldown keeps the fix simple without adding framework-wide cache compatibility surface.

Tests

Added real Firebase JWT/OpenSSL coverage for:

  • refreshing JWKS when the token kid is missing from cached keys
  • reusing cached JWKS when the token kid is already present
  • not refreshing for a token with no kid
  • applying the refresh cooldown to repeated unknown-kid failures
  • refreshing when key material changes under the same kid

Adds a refresh flag to OpenIdProvider::getOpenIdConfig() and getJwks() so
the discovery doc and key set can be re-fetched when verification fails
for a key reason. getUserByOIDCToken() retries decode once after a
forced refresh on UnexpectedValueException matching the "kid" invalid
path, and on SignatureInvalidException for the rare case where a
provider replaces key material under an existing kid.

A 10-second forced-refresh cooldown bounds the refresh rate so a flood
of tokens with unknown kids cannot turn the verification path into a
JWKS endpoint hammer. The timestamp is set before the HTTP call, so a
failed fetch also counts toward cooldown.
Concrete OpenIdProvider used by the JWKS rotation tests. Exposes the
real JWT verification path via verifyToken() and allows tests to
override jwksRefreshCooldownSeconds and the Guzzle client directly,
avoiding the per-request httpClient context plumbing for the JWKS
rotation cases under test.
Five tests covering the refresh-on-miss + cooldown behavior using real
RSA keypairs and end-to-end Firebase JWT::encode / JWT::decode rather
than mock-shaped fakes:

- unknown new kid refreshes JWKS and succeeds
- cached valid kid does not refetch
- token without kid fails without triggering refresh
- cooldown bounds repeated unknown-kid refreshes
- same kid with stale key material refreshes once and succeeds
@binaryfire binaryfire merged commit a7326ff into 0.4 May 15, 2026
34 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant