Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 47 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
marker interface, extends `\Throwable`). Concrete exception classes now
extend the SPL type that best describes the failure category:
`\RuntimeException` (network/cache/data — `CacheException`,
`HttpException`, `JsonException`, `DecodeException`, `KeyException`,
`HttpException`, `JsonException`, `DecodeException`, `JwksException`,
`CodeException`, `ValidationException`, `ClaimsException`),
`\LogicException` (programmer/config bug — `BadUrlException`,
`IllegalSchemeException`, `MissingParameterException`),
Expand All @@ -40,7 +40,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
re-wrapped as `CodeException`. Both implement the marker, so a
consumer catching that is unaffected; a consumer catching only
`CodeException` will need to widen to the marker for this code path.
- `OpenIdConfigurationProvider` now throws `KeyException` when a JWK
- `OpenIdConfigurationProvider` now throws `JwksException` when a JWK
entry is missing a string `kid` (RFC 7517 §4.5), and the new
`MetadataException` when an OIDC discovery document is missing a
required key or has a non-string value at one. Previously the
Expand All @@ -52,6 +52,51 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
so consumers catching that are unaffected; consumers catching
`CacheException` specifically for the missing-key case will need to
widen to the marker or to `MetadataException`.
- `OpenIdConfigurationProvider::getJwtVerificationKeys` now validates
the JWKS payload at each level before reading values: the top-level
`keys` property must be an array (`JwksException` otherwise), each
entry must be a JSON object (`JwksException` otherwise), each entry's
`kty` must be a string (`JwksException` otherwise), and for RSA keys
the `e` and `n` modulus/exponent values must both be strings
(`JwksException` otherwise). Previously these dynamic fields were
accessed without checking and would either silently produce a
garbage `Key`, trigger a PHP type error in the base64 decode, or
fail downstream in `XMLSecurityKey::convertRSA`. The new behaviour
fails at the malformed-payload boundary with a precise message.
- `OpenIdConfigurationProvider::getIdToken` now throws `CodeException`
when the token endpoint's JSON response is missing a string
`id_token`. Previously this would have returned `mixed` from
`$payload['id_token']` and produced confusing errors at the call
site.
- Renamed `KeyException` → `JwksException` for symmetry with
`MetadataException` and clearer scope: the type fires on both
JWKS-document-level errors (`keys` array missing) and JWK-entry-
level errors (missing `kid` / `kty` / `e` / `n`), so naming it
after the document type rather than the individual key is more
accurate. Consumers catching the marker are unaffected; consumers
catching the concrete class need to swap the name.

### Documentation

- Added class-level PHPDoc to every concrete exception in
`src/Exception/` describing what it represents, when it's thrown,
the rationale for its SPL parent type, and the boundary against
related concrete types. The audit confirms each of the 15 concretes
covers a distinct failure category — none would be handled
identically by a reasonable consumer:
- `\LogicException` family — `BadUrlException` (URL syntax),
`IllegalSchemeException` (http without opt-in),
`MissingParameterException` (caller omitted state/nonce).
- `\InvalidArgumentException` family — `ConfigurationException`
(missing required ctor option), `NegativeCacheDurationException`
(value out of range), `NegativeLeewayException` (value out of range).
- `\RuntimeException` family — `CacheException` (PSR-6 layer),
`HttpException` (transport + PSR-18 `ClientExceptionInterface`),
`JsonException` (parse failure), `DecodeException` (JWK base64
bytes), `JwksException` (JWKS structure), `CodeException` (token
exchange), `ValidationException` (JWT signature/decode),
`ClaimsException` (claim values), `MetadataException` (discovery
doc structure).

### Added

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ category, so a `catch` block scoped to that SPL type will also match:

| SPL parent | Concrete types | Category |
| ---------- | -------------- | -------- |
| `\RuntimeException` | `CacheException`, `HttpException`, `JsonException`, `DecodeException`, `KeyException`, `CodeException`, `ValidationException`, `ClaimsException`, `MetadataException` | Network, cache, token validation, claims mismatch — transient or data-shape failures |
| `\RuntimeException` | `CacheException`, `HttpException`, `JsonException`, `DecodeException`, `JwksException`, `CodeException`, `ValidationException`, `ClaimsException`, `MetadataException` | Network, cache, token validation, claims mismatch — transient or data-shape failures |
| `\LogicException` | `BadUrlException`, `IllegalSchemeException`, `MissingParameterException` | Programmer/config bugs — should be fixed in code |
| `\InvalidArgumentException` | `ConfigurationException`, `NegativeCacheDurationException`, `NegativeLeewayException` | Invalid input to the constructor / setters |

Expand Down
10 changes: 10 additions & 0 deletions src/Exception/BadUrlException.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@

namespace ItkDev\OpenIdConnect\Exception;

/**
* Thrown when `openIDConnectMetadataUrl` fails URL syntax validation
* (`parse_url` rejects it because no scheme can be parsed). A programmer
* error — the value is hard-coded or comes from misread configuration, so
* fixing it requires editing code or env config, not retrying at runtime.
* Hence `\LogicException`.
*
* Distinct from {@see IllegalSchemeException} (URL parses successfully but
* uses an `http://` scheme without `allowHttp: true`).
*/
class BadUrlException extends \LogicException implements OpenIdConnectExceptionInterface
{
}
13 changes: 13 additions & 0 deletions src/Exception/CacheException.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,19 @@

namespace ItkDev\OpenIdConnect\Exception;

/**
* Wraps PSR-6 cache layer failures. Specifically thrown when the injected
* `Psr\Cache\CacheItemPoolInterface` raises `Psr\Cache\InvalidArgumentException`
* from `getItem` / `save` / `deleteItem` — typically because the cache key
* contains a character the backend rejects, or the backend itself is
* unhealthy. The original exception is chained via `$previous`. Hence
* `\RuntimeException` (transient — a different cache backend or a sanitized
* key may resolve it).
*
* Strictly cache-layer failures only. Discovery-document validation problems
* are {@see MetadataException}; JWKS validation problems are
* {@see JwksException}; JSON parse failures are {@see JsonException}.
*/
class CacheException extends \RuntimeException implements OpenIdConnectExceptionInterface
{
}
13 changes: 13 additions & 0 deletions src/Exception/ClaimsException.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,19 @@

namespace ItkDev\OpenIdConnect\Exception;

/**
* Thrown from `validateIdToken()` when the decoded ID token's claims
* don't match expectations — wrong `aud` (audience does not contain
* our client id), wrong `iss` (issuer doesn't match the discovery
* document), or wrong `nonce` (didn't match the value we sent on the
* authorization request). Hence `\RuntimeException` (typically requires
* either re-authenticating, or auditing why the IdP issued a token
* meant for someone else — security-relevant if persistent).
*
* Distinct from {@see ValidationException} (token cryptographically
* invalid — bad signature, expired) and from {@see CodeException}
* (failure obtaining the token in the first place, before decoding).
*/
class ClaimsException extends \RuntimeException implements OpenIdConnectExceptionInterface
{
}
16 changes: 16 additions & 0 deletions src/Exception/CodeException.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,22 @@

namespace ItkDev\OpenIdConnect\Exception;

/**
* Thrown by `getIdToken()` when the OAuth authorization-code exchange
* fails: the token endpoint returned a transport error
* (`Psr\Http\Client\ClientExceptionInterface`), an OAuth error response
* (`League\OAuth2\Client\Provider\Exception\IdentityProviderException`),
* non-JSON body (`\JsonException`), or a JSON body missing a string
* `id_token`. The originating exception is chained via `$previous`.
* Hence `\RuntimeException` (often transient — typical causes are an
* expired or already-used authorization code, or a brief IdP outage).
*
* Distinct from {@see HttpException} (general HTTP transport failures
* for the discovery / JWKS endpoints, not the token-exchange POST). And
* distinct from {@see ValidationException} / {@see ClaimsException},
* which fire later in the flow on a successfully-received but invalid
* token.
*/
class CodeException extends \RuntimeException implements OpenIdConnectExceptionInterface
{
}
10 changes: 7 additions & 3 deletions src/Exception/ConfigurationException.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@
namespace ItkDev\OpenIdConnect\Exception;

/**
* Thrown when the bundle is misconfigured (missing required constructor option, invalid value, etc).
* Thrown from the `OpenIdConfigurationProvider` constructor when a required
* option (`cacheItemPool`, `openIDConnectMetadataUrl`) is missing from the
* `$options` array. Invalid input to a public constructor — fixable in
* calling code only. Hence `\InvalidArgumentException`.
*
* Extends `\InvalidArgumentException` because the failure is invalid input to a public constructor;
* fixable in calling code, not at runtime.
* Distinct from {@see NegativeCacheDurationException} and
* {@see NegativeLeewayException}, which fire when a numeric option is
* present but out of range.
*/
class ConfigurationException extends \InvalidArgumentException implements OpenIdConnectExceptionInterface
{
Expand Down
11 changes: 11 additions & 0 deletions src/Exception/DecodeException.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@

namespace ItkDev\OpenIdConnect\Exception;

/**
* Thrown by the internal base64url decoder when a JWK's `e` (exponent) or
* `n` (modulus) string contains bytes that fail strict base64 decoding.
* The JWK is structurally OK (per RFC 7517) but its contents are
* unparseable. Hence `\RuntimeException`.
*
* Distinct from {@see JwksException} (the JWK structure itself is wrong
* — missing `kid` / `kty`, non-array key entry, etc.). Both can fire
* while loading the JWKS, but at different levels: JwksException at the
* shape level, DecodeException at the bytes level.
*/
class DecodeException extends \RuntimeException implements OpenIdConnectExceptionInterface
{
}
14 changes: 14 additions & 0 deletions src/Exception/HttpException.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,20 @@

use Psr\Http\Client\ClientExceptionInterface;

/**
* Wraps HTTP transport failures while fetching the OIDC discovery document
* or the JWKS — non-200 responses, network errors, or Guzzle-thrown
* `Psr\Http\Client\ClientExceptionInterface` from the underlying HTTP
* client (chained via `$previous`). Hence `\RuntimeException` (transient —
* the IdP being briefly unreachable typically resolves on retry).
*
* Also implements PSR-18's {@see ClientExceptionInterface} as part of the
* public contract: PSR-18-aware consumers can catch HTTP failures from
* this library via the standard PSR marker.
*
* Distinct from {@see CodeException} (failure during the OAuth code
* exchange POST to the token endpoint).
*/
class HttpException extends \RuntimeException implements OpenIdConnectExceptionInterface, ClientExceptionInterface
{
}
9 changes: 9 additions & 0 deletions src/Exception/IllegalSchemeException.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

namespace ItkDev\OpenIdConnect\Exception;

/**
* Thrown when `openIDConnectMetadataUrl` uses the `http://` scheme without
* the explicit `allowHttp: true` opt-in. OIDC requires TLS; plain HTTP is
* only acceptable for local IdP mocks during development. A programmer
* error — both the URL and the opt-in are configuration, fixed in code or
* env. Hence `\LogicException`.
*
* Distinct from {@see BadUrlException} (URL syntax is unparseable).
*/
class IllegalSchemeException extends \LogicException implements OpenIdConnectExceptionInterface
{
}
10 changes: 10 additions & 0 deletions src/Exception/JsonException.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@

namespace ItkDev\OpenIdConnect\Exception;

/**
* Thrown when `json_decode` fails on an IdP response body — the bytes
* didn't parse as JSON at all (raw `\JsonException` from PHP's JSON
* extension, chained via `$previous`). Hence `\RuntimeException`.
*
* Distinct from {@see MetadataException} (JSON parses fine but doesn't
* conform to the OIDC Discovery spec). The remediation differs: a parse
* failure may be transient (corrupted bytes, retry might help), while a
* malformed discovery document is a persistent IdP-configuration issue.
*/
class JsonException extends \RuntimeException implements OpenIdConnectExceptionInterface
{
}
20 changes: 20 additions & 0 deletions src/Exception/JwksException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

namespace ItkDev\OpenIdConnect\Exception;

/**
* Thrown when the JWKS payload returned from the IdP doesn't conform to
* RFC 7517 (JSON Web Key Set) — the top-level `keys` array is missing,
* an entry isn't a JSON object, a required field (`kid`, `kty`, RSA `e`
* / `n`) is missing or has the wrong type, or the `kty` is one this
* library doesn't support. Hence `\RuntimeException` (a persistent IdP
* configuration issue; retry won't help).
*
* Distinct from {@see DecodeException} (a JWK's base64 bytes are
* malformed but the structure is OK), from {@see MetadataException} (the
* OIDC discovery document — not the JWKS — is malformed), and from
* {@see JsonException} (the bytes didn't parse as JSON at all).
*/
class JwksException extends \RuntimeException implements OpenIdConnectExceptionInterface
{
}
7 changes: 0 additions & 7 deletions src/Exception/KeyException.php

This file was deleted.

7 changes: 7 additions & 0 deletions src/Exception/MissingParameterException.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

namespace ItkDev\OpenIdConnect\Exception;

/**
* Thrown from `getAuthorizationUrl()` when the caller omitted the required
* `state` or `nonce` parameter. Both are CSRF / replay-attack mitigations
* mandated by the OIDC spec — calling code is expected to generate and
* pass them on every authorization request. A programmer error in the
* calling code, hence `\LogicException`.
*/
class MissingParameterException extends \LogicException implements OpenIdConnectExceptionInterface
{
}
9 changes: 9 additions & 0 deletions src/Exception/NegativeCacheDurationException.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

namespace ItkDev\OpenIdConnect\Exception;

/**
* Thrown when the `cacheDuration` option is a negative integer. Cache TTL
* must be ≥ 0 seconds; a negative value is meaningless. Hence
* `\InvalidArgumentException` (the value is structurally a valid int, but
* out of the accepted range).
*
* Distinct from {@see ConfigurationException} (the option is missing
* entirely).
*/
class NegativeCacheDurationException extends \InvalidArgumentException implements OpenIdConnectExceptionInterface
{
}
9 changes: 9 additions & 0 deletions src/Exception/NegativeLeewayException.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

namespace ItkDev\OpenIdConnect\Exception;

/**
* Thrown when the `leeway` option is a negative integer. Leeway adjusts
* clock-skew tolerance during JWT exp/iat validation and must be ≥ 0 —
* negative leeway would push the validation window into the future. Hence
* `\InvalidArgumentException`.
*
* Distinct from {@see ConfigurationException} (the option is missing
* entirely).
*/
class NegativeLeewayException extends \InvalidArgumentException implements OpenIdConnectExceptionInterface
{
}
13 changes: 13 additions & 0 deletions src/Exception/ValidationException.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,19 @@

namespace ItkDev\OpenIdConnect\Exception;

/**
* Thrown from `validateIdToken()` when the ID token cannot be
* cryptographically validated — bad signature, expired, malformed JWT
* structure, or any failure inside `firebase/php-jwt`'s
* `JWT::decode()` (wraps `\UnexpectedValueException` and chains the
* cause via `$previous`). Hence `\RuntimeException` (often resolves by
* re-authenticating, since a fresh login produces a fresh token).
*
* Distinct from {@see ClaimsException} (the token decoded successfully
* but its claim *values* — audience, issuer, nonce — are wrong) and
* from {@see CodeException} (the failure was getting the token in the
* first place, before decode).
*/
class ValidationException extends \RuntimeException implements OpenIdConnectExceptionInterface
{
}
Loading
Loading