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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,6 @@ phpcs-report.xml
.php-cs-fixer.cache
coverage/
yarn.lock

# Per-developer Claude Code context — local tooling, not part of the public source.
/CLAUDE.md
60 changes: 60 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,66 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed (BREAKING)

- **Exception hierarchy reworked.** Every exception thrown from a public
method now implements
`\ItkDev\OpenIdConnect\Exception\OpenIdConnectExceptionInterface` (new
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`,
`CodeException`, `ValidationException`, `ClaimsException`),
`\LogicException` (programmer/config bug — `BadUrlException`,
`IllegalSchemeException`, `MissingParameterException`),
`\InvalidArgumentException` (invalid input — `ConfigurationException`,
`NegativeCacheDurationException`, `NegativeLeewayException`).
Consumers catching `ItkOpenIdConnectException` should migrate to
`OpenIdConnectExceptionInterface`; the abstract class is kept as a
`@deprecated` alias and still implements the marker, but **concrete
exceptions no longer extend it**, so existing `catch
(ItkOpenIdConnectException $e)` blocks will not match anything thrown
by 5.0+ code.
- `OpenIdConfigurationProvider::__construct` now throws
`ConfigurationException` (new, `\InvalidArgumentException`-typed)
instead of a raw `\InvalidArgumentException` when a required option
is missing. The new type implements the marker; existing
`catch (\InvalidArgumentException $e)` blocks continue to match.
- `OpenIdConfigurationProvider::getIdToken` narrowed its boundary
`catch` from `\Exception` to
`IdentityProviderException|ClientExceptionInterface|\JsonException`.
Cache failures during `getConfiguration` (called for the token
endpoint lookup) now propagate as `CacheException` rather than being
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.

### Added

- `ItkDev\OpenIdConnect\Exception\OpenIdConnectExceptionInterface`
marker for catching every OIDC failure from this library.
- `ItkDev\OpenIdConnect\Exception\ConfigurationException` for missing
or invalid constructor options.
- `tests/Exception/ExceptionHierarchyTest.php` locks the contract:
every concrete implements the marker, extends the correct SPL parent,
and is caught by a `catch (OpenIdConnectExceptionInterface $e)`
block. Failing this test class is the early warning that the public
contract has drifted.

### Deprecated

- `ItkDev\OpenIdConnect\Exception\ItkOpenIdConnectException` abstract
class (catch `OpenIdConnectExceptionInterface` instead). Kept through
5.x; removal scheduled for 6.0.

### Documentation

- Added a new "Exception handling" section to `README.md` describing the
marker interface, the SPL parents of each concrete, the PSR-18
co-implementation on `HttpException`, and the 4.x → 5.0 catch-block
migration. Also fixed the `validateIdToken` example to catch the
marker interface instead of the now-deprecated abstract.

## [4.1.2] - 2026-05-11

- Chained `previous` consistently in `OpenIdConfigurationProvider` catch
Expand Down
45 changes: 43 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,17 +185,58 @@ if (!$sessionState || $request->query->get('state') !== $sessionState) {

// Validate the id token. This will validate the token against the keys published by the
// provider (Azure AD B2C). If the token is invalid or the nonce doesn't match an
// exception will thrown.
// exception will be thrown.
try {
$claims = $provider->validateIdToken($request->query->get('id_token'), $session->get('oauth2nonce'));
// Authentication successful
} catch (ItkOpenIdConnectException $exception) {
} catch (OpenIdConnectExceptionInterface $exception) {
// Handle failed authentication
} finally {
$this->session->remove('oauth2nonce');
}
```

### Exception handling

Every exception thrown from a public method of this library implements
`\ItkDev\OpenIdConnect\Exception\OpenIdConnectExceptionInterface`. Catch the
marker to handle any OIDC failure with a single block, or scope to a more
specific type when you need to discriminate:

```php
use ItkDev\OpenIdConnect\Exception\OpenIdConnectExceptionInterface;

try {
$claims = $provider->validateIdToken($idToken, $nonce);
} catch (OpenIdConnectExceptionInterface $e) {
// Cause is preserved via $e->getPrevious()
}
```

Concrete exception classes extend the SPL type that describes the failure
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` | 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 |

`HttpException` additionally implements PSR-18's
`Psr\Http\Client\ClientExceptionInterface`, so existing PSR-18-aware
consumers can keep catching on the standard PSR marker.

Every wrap site preserves the underlying cause via `$previous`, so
`$e->getPrevious()` walks back to the originating Guzzle, firebase/php-jwt
or PSR-6 cache exception.

> **Upgrading from 4.x:** the concrete exceptions no longer extend the
> abstract `ItkOpenIdConnectException`. Catches written as
> `catch (ItkOpenIdConnectException $e)` will not match anything thrown
> by 5.0+ code — migrate to `catch (OpenIdConnectExceptionInterface $e)`.
> The abstract class itself is kept through 5.x as a documented alias
> (`@deprecated`); removal is scheduled for 6.0.

## Development Setup

A `docker-compose.yml` file with a PHP 8.3+ image is included in this project.
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
"ergebnis/composer-normalize": "^2.50",
"friendsofphp/php-cs-fixer": "^3.75",
"mockery/mockery": "^1.6.12",
"phpstan/phpstan": "^2.1.40",
"phpstan/phpstan": "^2.1.41",
"phpunit/php-code-coverage": "^12",
"phpunit/phpunit": "^12"
},
Expand Down
139 changes: 139 additions & 0 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
parameters:
ignoreErrors:
-
message: '#^Call to function is_subclass_of\(\) with ''ItkDev\\\\OpenIdConnect\\\\Exception\\\\ItkOpenIdConnectException'' and ''ItkDev\\\\OpenIdConnect\\\\Exception\\\\OpenIdConnectExceptionInterface'' will always evaluate to true\.$#'
identifier: function.alreadyNarrowedType
count: 1
path: tests/Exception/ExceptionHierarchyTest.php

-
message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertTrue\(\) with true will always evaluate to true\.$#'
identifier: method.alreadyNarrowedType
count: 1
path: tests/Exception/ExceptionHierarchyTest.php

-
message: '#^Property Tests\\Security\\MockJWT\:\:\$leeway has no type specified\.$#'
identifier: missingType.property
count: 1
path: tests/Security/MockJWT.php

-
message: '#^Access to an undefined property object\:\:\$aud\.$#'
identifier: property.notFound
count: 2
path: tests/Security/OpenIdConfigurationProviderTest.php

-
message: '#^Access to an undefined property object\:\:\$nonce\.$#'
identifier: property.notFound
count: 3
path: tests/Security/OpenIdConfigurationProviderTest.php

-
message: '#^Call to an undefined method Mockery\\ExpectationInterface\|Mockery\\HigherOrderMessage\:\:andReturn\(\)\.$#'
identifier: method.notFound
count: 6
path: tests/Security/OpenIdConfigurationProviderTest.php

-
message: '#^Call to an undefined method Mockery\\ExpectationInterface\|Mockery\\HigherOrderMessage\:\:andThrow\(\)\.$#'
identifier: method.notFound
count: 1
path: tests/Security/OpenIdConfigurationProviderTest.php

-
message: '#^Call to an undefined method Mockery\\ExpectationInterface\|Mockery\\HigherOrderMessage\:\:with\(\)\.$#'
identifier: method.notFound
count: 1
path: tests/Security/OpenIdConfigurationProviderTest.php

-
message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertTrue\(\) with true will always evaluate to true\.$#'
identifier: method.alreadyNarrowedType
count: 1
path: tests/Security/OpenIdConfigurationProviderTest.php

-
message: '#^Cannot call method generateNonce\(\) on ItkDev\\OpenIdConnect\\Security\\OpenIdConfigurationProvider\|null\.$#'
identifier: method.nonObject
count: 1
path: tests/Security/OpenIdConfigurationProviderTest.php

-
message: '#^Cannot call method generateState\(\) on ItkDev\\OpenIdConnect\\Security\\OpenIdConfigurationProvider\|null\.$#'
identifier: method.nonObject
count: 1
path: tests/Security/OpenIdConfigurationProviderTest.php

-
message: '#^Cannot call method getAuthorizationUrl\(\) on ItkDev\\OpenIdConnect\\Security\\OpenIdConfigurationProvider\|null\.$#'
identifier: method.nonObject
count: 3
path: tests/Security/OpenIdConfigurationProviderTest.php

-
message: '#^Cannot call method getBaseAccessTokenUrl\(\) on ItkDev\\OpenIdConnect\\Security\\OpenIdConfigurationProvider\|null\.$#'
identifier: method.nonObject
count: 1
path: tests/Security/OpenIdConfigurationProviderTest.php

-
message: '#^Cannot call method getBaseAuthorizationUrl\(\) on ItkDev\\OpenIdConnect\\Security\\OpenIdConfigurationProvider\|null\.$#'
identifier: method.nonObject
count: 1
path: tests/Security/OpenIdConfigurationProviderTest.php

-
message: '#^Cannot call method getDefaultScopes\(\) on ItkDev\\OpenIdConnect\\Security\\OpenIdConfigurationProvider\|null\.$#'
identifier: method.nonObject
count: 1
path: tests/Security/OpenIdConfigurationProviderTest.php

-
message: '#^Cannot call method getEndSessionUrl\(\) on ItkDev\\OpenIdConnect\\Security\\OpenIdConfigurationProvider\|null\.$#'
identifier: method.nonObject
count: 6
path: tests/Security/OpenIdConfigurationProviderTest.php

-
message: '#^Cannot call method getGuarded\(\) on ItkDev\\OpenIdConnect\\Security\\OpenIdConfigurationProvider\|null\.$#'
identifier: method.nonObject
count: 1
path: tests/Security/OpenIdConfigurationProviderTest.php

-
message: '#^Cannot call method getResourceOwnerDetailsUrl\(\) on ItkDev\\OpenIdConnect\\Security\\OpenIdConfigurationProvider\|null\.$#'
identifier: method.nonObject
count: 1
path: tests/Security/OpenIdConfigurationProviderTest.php

-
message: '#^Cannot call method getState\(\) on ItkDev\\OpenIdConnect\\Security\\OpenIdConfigurationProvider\|null\.$#'
identifier: method.nonObject
count: 1
path: tests/Security/OpenIdConfigurationProviderTest.php

-
message: '#^Cannot call method validateIdToken\(\) on ItkDev\\OpenIdConnect\\Security\\OpenIdConfigurationProvider\|null\.$#'
identifier: method.nonObject
count: 7
path: tests/Security/OpenIdConfigurationProviderTest.php

-
message: '#^Parameter \#1 \$json of function json_decode expects string, string\|false given\.$#'
identifier: argument.type
count: 3
path: tests/Security/OpenIdConfigurationProviderTest.php

-
message: '#^Parameter \#1 \$string of function parse_str expects string, string\|false\|null given\.$#'
identifier: argument.type
count: 1
path: tests/Security/OpenIdConfigurationProviderTest.php

-
message: '#^Property Tests\\Security\\OpenIdConfigurationProviderTest\:\:\$provider \(ItkDev\\OpenIdConnect\\Security\\OpenIdConfigurationProvider\|null\) is never assigned null so it can be removed from the property type\.$#'
identifier: property.unusedType
count: 1
path: tests/Security/OpenIdConfigurationProviderTest.php
5 changes: 5 additions & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
includes:
- phpstan-baseline.neon

parameters:
level: 8
paths:
- src
- tests
reportIgnoresWithoutComments: true
ignoreErrors:
-
identifier: missingType.iterableValue
2 changes: 1 addition & 1 deletion src/Exception/BadUrlException.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@

namespace ItkDev\OpenIdConnect\Exception;

class BadUrlException extends ItkOpenIdConnectException
class BadUrlException extends \LogicException implements OpenIdConnectExceptionInterface
{
}
2 changes: 1 addition & 1 deletion src/Exception/CacheException.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@

namespace ItkDev\OpenIdConnect\Exception;

class CacheException extends ItkOpenIdConnectException
class CacheException extends \RuntimeException implements OpenIdConnectExceptionInterface
{
}
2 changes: 1 addition & 1 deletion src/Exception/ClaimsException.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@

namespace ItkDev\OpenIdConnect\Exception;

class ClaimsException extends ItkOpenIdConnectException
class ClaimsException extends \RuntimeException implements OpenIdConnectExceptionInterface
{
}
2 changes: 1 addition & 1 deletion src/Exception/CodeException.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@

namespace ItkDev\OpenIdConnect\Exception;

class CodeException extends ItkOpenIdConnectException
class CodeException extends \RuntimeException implements OpenIdConnectExceptionInterface
{
}
13 changes: 13 additions & 0 deletions src/Exception/ConfigurationException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace ItkDev\OpenIdConnect\Exception;

/**
* Thrown when the bundle is misconfigured (missing required constructor option, invalid value, etc).
*
* Extends `\InvalidArgumentException` because the failure is invalid input to a public constructor;
* fixable in calling code, not at runtime.
*/
class ConfigurationException extends \InvalidArgumentException implements OpenIdConnectExceptionInterface
{
}
2 changes: 1 addition & 1 deletion src/Exception/DecodeException.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@

namespace ItkDev\OpenIdConnect\Exception;

class DecodeException extends ItkOpenIdConnectException
class DecodeException extends \RuntimeException implements OpenIdConnectExceptionInterface
{
}
2 changes: 1 addition & 1 deletion src/Exception/HttpException.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@

use Psr\Http\Client\ClientExceptionInterface;

class HttpException extends ItkOpenIdConnectException implements ClientExceptionInterface
class HttpException extends \RuntimeException implements OpenIdConnectExceptionInterface, ClientExceptionInterface
{
}
2 changes: 1 addition & 1 deletion src/Exception/IllegalSchemeException.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@

namespace ItkDev\OpenIdConnect\Exception;

class IllegalSchemeException extends ItkOpenIdConnectException
class IllegalSchemeException extends \LogicException implements OpenIdConnectExceptionInterface
{
}
8 changes: 7 additions & 1 deletion src/Exception/ItkOpenIdConnectException.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

namespace ItkDev\OpenIdConnect\Exception;

abstract class ItkOpenIdConnectException extends \Exception
/**
* @deprecated since 5.0, will be removed in 6.0. Catch
* {@see OpenIdConnectExceptionInterface} instead. Concrete exception classes
* no longer extend this abstract; existing `catch (ItkOpenIdConnectException $e)`
* blocks will not match any exception thrown by 5.0+ code.
*/
abstract class ItkOpenIdConnectException extends \Exception implements OpenIdConnectExceptionInterface
{
}
2 changes: 1 addition & 1 deletion src/Exception/JsonException.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@

namespace ItkDev\OpenIdConnect\Exception;

class JsonException extends ItkOpenIdConnectException
class JsonException extends \RuntimeException implements OpenIdConnectExceptionInterface
{
}
2 changes: 1 addition & 1 deletion src/Exception/KeyException.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@

namespace ItkDev\OpenIdConnect\Exception;

class KeyException extends ItkOpenIdConnectException
class KeyException extends \RuntimeException implements OpenIdConnectExceptionInterface
{
}
Loading
Loading