Skip to content

refactor: build JWKS keys with firebase/php-jwt, drop xmlseclibs - #58

Merged
turegjorup merged 1 commit into
developfrom
refactor/jwks-via-firebase-jwk
Aug 26, 2026
Merged

refactor: build JWKS keys with firebase/php-jwt, drop xmlseclibs#58
turegjorup merged 1 commit into
developfrom
refactor/jwks-via-firebase-jwk

Conversation

@turegjorup

Copy link
Copy Markdown
Collaborator

Removes the robrichards/xmlseclibs dependency by delegating JWKS key construction to firebase/php-jwt, which this library already depends on and which verifies the signature anyway.

Why

XMLSecurityKey::convertRSA() was the only thing used from xmlseclibs. JWK::parseKey() does the same modulus-and-exponent conversion. Dropping the dependency also drops the phpseclib/phpseclib / paragonie subtree that xmlseclibs 4.0 introduced in #57.

Not parseKeySet()

The obvious form of this change — replacing the whole loop with JWK::parseKeySet() — would have undone the strict JWKS validation 5.0.0 shipped as a breaking change. Measured against the inputs our tests cover:

Input This library parseKeySet() alone
non-object entry [42] JwksException bare TypeError
keys not an array JwksException PHP warning, then a generic throw
RSA e: "" / " " / "=" JwksException accepted, builds a key
RSA e: 42 JwksException accepted
non-string kid JwksException coerced to "42"
unsupported kty JwksException silently skipped

So the guards stay in front and only the construction is delegated. Every JwksException this library threw before, it still throws, with the same message.

The cache changes shape

JWK::parseKey() returns keys wrapping an OpenSSLAsymmetricKey, and PHP refuses to serialize those:

CURRENT  (PEM string):  serialize OK, 628 bytes
FIREBASE parseKeySet:   Serialization of 'OpenSSLAsymmetricKey' is not allowed

So the built keys can no longer go into a PSR-6 pool. getJwksDocument() caches the fetched JWKS document instead, and parsing happens per call — microseconds, with the network fetch still cached exactly as before. Arguably the better shape regardless: the previous design is what produced the 4.1.0 bug "Fixed JWKS verification keys not being persisted to cache".

Upgrade note, in the changelog: the cache key moves from …||jwks to …||jwks-document, so entries written by 5.0 — which hold serialized Key objects — are never read back as a document. They are left to expire.

Test seam

The suite overload-mocks Firebase\JWT\JWT to stub decode(), which also took out JWT::urlsafeB64Decode() — called by JWK::parseKey() in production, so ten tests broke. A single overloadJwt() helper now sets up both and replaces twenty hand-rolled overloads.

New coverage: a JWKS entry carrying a private key (d) clears every guard and is refused by parseKey(), so there is now a test asserting that arrives as JwksException with the cause chained and code 0. Plus a test for a JWK Set with additional top-level members, which RFC 7517 §5 allows and which the document cache must now preserve.

One deliberate infection exclusion

getJwtVerificationKeys() catches the three SPL types parseKey() documents, but only UnexpectedValueException is reachable through it: our guards run first, so InvalidArgumentException ("JWK must not be empty") cannot fire, and DomainException comes only from the EC/OKP branches the RSA check excludes or from an openssl_pkey_get_public() failure no input reproduces here. Dropping either arm would let a bare SPL exception escape a public method, so the breadth is deliberate and the Catch_ mutants are excluded with that reasoning recorded in infection.json5.

Verification

119 tests → 121, all green. Coverage 100% (27/27 methods, 169/169 lines). Mutation: 215 generated, 211 killed, 4 ignored, zero escaped, MSI 100% against the binding threshold. PHPStan max clean at the ceiling and at the dependency floor, php-cs-fixer, markdownlint, prettier, composer normalize, composer validate and composer audit all clean.

XMLSecurityKey::convertRSA() was the only thing this library used from
robrichards/xmlseclibs, and firebase/php-jwt — already a direct dependency,
and the library that verifies the signature — does the same conversion in
JWK::parseKey(). Dropping xmlseclibs also drops the phpseclib subtree its
4.0 release introduced.

JWK::parseKeySet() was the obvious candidate but is laxer than the
validation 5.0.0 introduced: it accepts a non-string, empty or undecodable
exponent, coerces a non-string kid, treats an unsupported kty as a key to
skip, and raises a bare \TypeError on a non-object entry. Adopting it would
have undone that work, so the existing guards stay in front and only the key
construction is delegated.

The cache changes shape as a consequence. JWK::parseKey() returns keys
wrapping an OpenSSLAsymmetricKey, which PHP refuses to serialize, so the
built keys cannot go into a PSR-6 pool. getJwksDocument() caches the fetched
JWKS document instead and parsing happens per call — microseconds, and the
network fetch is still cached. The cache key changes from `…||jwks` to
`…||jwks-document` so entries written by 5.0, which hold serialized Key
objects, are never read back as a document.

Two test-visible consequences:

- The suite overload-mocks Firebase\JWT\JWT to stub decode(), which also
  took out JWT::urlsafeB64Decode() — called by JWK::parseKey() in
  production. A single overloadJwt() helper now sets up both, replacing
  twenty hand-rolled overloads.
- A JWKS entry carrying a private key clears every guard and is refused by
  parseKey() instead, so there is now a test for the wrap-and-chain
  behaviour on that path.
@codecov

codecov Bot commented Aug 26, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (ab66ce5) to head (a92a825).

Additional details and impacted files
@@             Coverage Diff             @@
##             develop       #58   +/-   ##
===========================================
  Coverage     100.00%   100.00%           
- Complexity        75        77    +2     
===========================================
  Files              1         1           
  Lines            192       196    +4     
===========================================
+ Hits             192       196    +4     
Flag Coverage Δ
unittests 100.00% <100.00%> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@turegjorup turegjorup self-assigned this Aug 26, 2026
@turegjorup
turegjorup merged commit 41f3e05 into develop Aug 26, 2026
18 checks passed
@turegjorup
turegjorup deleted the refactor/jwks-via-firebase-jwk branch August 26, 2026 10:22
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