PR B: Document and tighten the exception system; narrow JSON payload accesses#44
Merged
Merged
Conversation
Two methods read fields out of JSON payloads (JWKS, token endpoint response) where PHPStan could only see `mixed`: - `getJwtVerificationKeys` walks `$jwks['keys']` and reads `kid`, `kty`, `e`, `n` on each entry without validating the structure. Spec-compliant payloads from real IdPs work, but a malformed payload silently produces garbage (a `Key` constructed from empty/wrong inputs) or trips a downstream type error in `XMLSecurityKey::convertRSA`. - `getIdToken` reads `$payload['id_token']` without checking that `$payload` is an array or that the field is a string, returning whatever it finds. Each access is now gated by an explicit `is_array` / `is_string` guard at the actual error boundary, throwing the appropriate typed exception with a precise message: - JWKS missing array `keys` → `KeyException`. - JWK entry not a JSON object → `KeyException`. - JWK entry missing string `kty` → `KeyException`. - RSA JWK missing string `e` / `n` → `KeyException`. - Token endpoint response missing string `id_token` → `CodeException`. Adds a `createProviderWithCustomJwks(string $jwksJson)` helper to the test and uses it for the four new JWKS test cases (the existing `testGetJwtVerificationKeysRejectsNonStringKid` / `testGetJwtVerificationKeysUnsupportedKeyType` cases keep their existing inline setup — out of scope to refactor). One additional test covers the new getIdToken throw. 100% coverage maintained (161/161 lines). Removes 7 errors when PHPStan is run at `level: max`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three related changes to the exception layer: 1. **Document every concrete.** Each of the 15 concretes in `src/Exception/` now has a class-level PHPDoc describing what it represents, when it's thrown, the rationale for its SPL parent (`\RuntimeException` / `\LogicException` / `\InvalidArgumentException`), and the boundary against related concrete types — most importantly: `DecodeException` vs `JwksException` (bytes-level vs shape-level failure inside the JWK); `JsonException` vs `MetadataException` (parse failure vs parsed-but-non-conformant document); `JwksException` vs `MetadataException` (JWKS vs OIDC-discovery document, both IdP-side spec violations at different layers); `CodeException` vs `ValidationException` vs `ClaimsException` (different stages of the auth flow: token-exchange vs signature vs claim-value). 2. **Rename `KeyException` → `JwksException`** for symmetry with `MetadataException` and clearer scope: the type fires for both document-level errors (`keys` array missing) and entry-level errors (missing `kid` / `kty` / `e` / `n`). Naming after the document type (JWKS) rather than the individual key is more accurate; matches Symfony's `JwkSet` PascalCase convention. 3. **Narrow JSON payload accesses** in `getJwtVerificationKeys` and `getIdToken`. Each previously-dynamic field read now has an explicit `is_array` / `is_string` guard at the actual error boundary, throwing the appropriate typed exception with a precise message: `JwksException` for malformed JWKS structure; `CodeException` for a token endpoint response missing string `id_token`. Adds a `createProviderWithCustomJwks(string)` test helper used by four new JWKS validation tests plus one inline getIdToken test. 100% coverage maintained (161/161 lines, up from 151). Audit confirms each of the 15 concretes covers a distinct failure category — no two would be handled identically by a reasonable consumer (per ADR 001's granularity rule). The overall count is higher than the ADR's "three to five per package" target; consolidation candidates are flagged via the cross-references but not collapsed (any rename or removal is a deferred MAJOR break). Static analysis: PHPStan at `level: max` drops 7 errors (was 26 on this branch tip, now 19). Remaining 19 covered by PR C. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## fix/replace-string-casts-with-guards #44 +/- ##
========================================================================
Coverage 100.00% 100.00%
- Complexity 64 72 +8
========================================================================
Files 1 1
Lines 175 185 +10
========================================================================
+ Hits 175 185 +10
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
6 tasks
jekuaitk
approved these changes
May 12, 2026
5 tasks
turegjorup
added a commit
that referenced
this pull request
May 12, 2026
`[Unreleased]` had two `### Documentation` subsections — one added in PR #44 for the README "Exception handling" section, one added later for the per-class PHPDoc audit. markdownlint flags duplicate sibling headings (MD024) and the CI step was failing. Merge the README bullet into the per-class section so there's a single `### Documentation` heading. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
turegjorup
added a commit
that referenced
this pull request
May 12, 2026
A few docblocks in `OpenIdConfigurationProvider` and the test suite
had their descriptions wrapped onto a continuation `*` line:
* @param int $length
* Length of the random string to be generated
*
* @return string
* The generated state
The `phpdoc_align: vertical` rule from the @symfony preset doesn't
create those wraps — it just aligns whatever multi-line structure
exists in the source. Flattening each description back onto its
`@param` / `@return` line lets the rule pad the columns into a
tidy table instead:
* @param int $length Length of the random string to be generated
* @return string The generated state
No `.php-cs-fixer.dist.php` change: the @symfony default produces
the clean form once the source isn't pre-wrapped.
Also consolidates two `### Documentation` subsections under
`[Unreleased]` (left over from PR #44) into one — markdownlint's
MD024 was flagging the duplicate heading.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
turegjorup
added a commit
that referenced
this pull request
May 12, 2026
A few docblocks in `OpenIdConfigurationProvider` and the test suite
had their descriptions wrapped onto a continuation `*` line:
* @param int $length
* Length of the random string to be generated
*
* @return string
* The generated state
The `phpdoc_align: vertical` rule from the @symfony preset doesn't
create those wraps — it just aligns whatever multi-line structure
exists in the source. Flattening each description back onto its
`@param` / `@return` line lets the rule pad the columns into a
tidy table instead:
* @param int $length Length of the random string to be generated
* @return string The generated state
No `.php-cs-fixer.dist.php` change: the @symfony default produces
the clean form once the source isn't pre-wrapped.
Also consolidates two `### Documentation` subsections under
`[Unreleased]` (left over from PR #44) into one — markdownlint's
MD024 was flagging the duplicate heading.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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
Second of three follow-ups (A → B → C) on the path to
level: maxPHPStan. Combines three closely-related concerns into one PR after review feedback on PR #42 surfaced naming + documentation gaps:getJwtVerificationKeysandgetIdToken— replaces dynamic$jwks['keys'][n]['kid']/$payload['id_token']reads with explicitis_array/is_stringguards that throw a typed exception at the actual error boundary.KeyException→JwksExceptionfor symmetry withMetadataException.1. JSON-payload narrowing
$jwks['keys']not an arrayJwksException('JWKS payload missing array "keys" property (RFC 7517 §5)')$keynot an objectJwksException('JWK entry is not a JSON object')$key['kty']not a stringJwksException('JWK entry missing string "kty" for key id: $kid')$key['e']/$key['n']not stringsJwksException('JWK RSA entry missing string "e"/"n" for key id: $kid')id_tokenCodeException('Token endpoint response missing string "id_token"')All new throws are existing types implementing
OpenIdConnectExceptionInterface— consumers catching the marker are unaffected.Adds a
createProviderWithCustomJwks(string)test helper used by four new JWKS validation tests plus one inline getIdToken test. 100% coverage maintained (161/161 lines, up from 151).2. Documentation
Every concrete in
src/Exception/now carries a class-level PHPDoc describing:The audit shows each of the 15 concretes covers a distinct failure category — no two would be handled identically by a reasonable consumer (per ADR 001's granularity rule).
The overall count (15) is higher than ADR 001's "three to five per package" target. Consolidation candidates are flagged via the cross-references but not collapsed in this PR (any rename or removal is a deferred MAJOR break).
3. Rename
KeyException→JwksExceptionThe type fires for both document-level errors (
keysarray missing in the JWKS top level) and entry-level errors (missingkid/kty/e/non a JWK inside). Naming it after the JWKS document type rather than the individual key is more accurate, and mirrorsMetadataException(named after the OIDC discovery document).Casing:
JwksException(PSR-1/12 PascalCase) — matches Symfony'sJwkSetconvention. Avoids the eye-poke of all-caps acronyms incatchblocks.Consumers catching
OpenIdConnectExceptionInterfaceare unaffected. Consumers catching the concrete need to swap the class name.Test plan
task test:coverage— 100% onOpenIdConfigurationProvider(24/24 methods, 161/161 lines).task analyze:phpatlevel: 8— no errors.task analyze:phpatlevel: max— 7 errors removed (26 → 19). Remaining 19 covered by PR C.task lint:php— clean.task lint:markdown— clean.Follow-up: PR C
Will narrow
validateIdToken's claim accesses and the test-side Mockery / fixture-helper return types. After PR C,level: maxis achievable in a single-line config bump.🤖 Generated with Claude Code