refactor: build JWKS keys with firebase/php-jwt, drop xmlseclibs - #58
Merged
Conversation
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 Report✅ All modified and coverable lines are covered by tests. 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
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:
|
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.
Removes the
robrichards/xmlseclibsdependency by delegating JWKS key construction tofirebase/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 thephpseclib/phpseclib/paragoniesubtree 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:parseKeySet()alone[42]JwksExceptionTypeErrorkeysnot an arrayJwksExceptione: ""/" "/"="JwksExceptione: 42JwksExceptionkidJwksException"42"ktyJwksExceptionSo the guards stay in front and only the construction is delegated. Every
JwksExceptionthis library threw before, it still throws, with the same message.The cache changes shape
JWK::parseKey()returns keys wrapping anOpenSSLAsymmetricKey, and PHP refuses to serialize those: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
…||jwksto…||jwks-document, so entries written by 5.0 — which hold serializedKeyobjects — are never read back as a document. They are left to expire.Test seam
The suite overload-mocks
Firebase\JWT\JWTto stubdecode(), which also took outJWT::urlsafeB64Decode()— called byJWK::parseKey()in production, so ten tests broke. A singleoverloadJwt()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 byparseKey(), so there is now a test asserting that arrives asJwksExceptionwith 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 typesparseKey()documents, but onlyUnexpectedValueExceptionis reachable through it: our guards run first, soInvalidArgumentException("JWK must not be empty") cannot fire, andDomainExceptioncomes only from the EC/OKP branches theRSAcheck excludes or from anopenssl_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 theCatch_mutants are excluded with that reasoning recorded ininfection.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 validateandcomposer auditall clean.