diff --git a/.github/workflows/changelog.yaml b/.github/workflows/changelog.yaml index 15297e3..e0cd8a0 100644 --- a/.github/workflows/changelog.yaml +++ b/.github/workflows/changelog.yaml @@ -18,7 +18,7 @@ jobs: fail-fast: false steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 2 diff --git a/.github/workflows/composer.yaml b/.github/workflows/composer.yaml index 539693f..f807f83 100644 --- a/.github/workflows/composer.yaml +++ b/.github/workflows/composer.yaml @@ -41,7 +41,7 @@ jobs: strategy: fail-fast: false steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Create docker network run: | @@ -55,7 +55,7 @@ jobs: strategy: fail-fast: false steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Create docker network run: | @@ -70,7 +70,7 @@ jobs: strategy: fail-fast: false steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Create docker network run: | diff --git a/.github/workflows/github_build_release.yml b/.github/workflows/github_build_release.yml index 63aca71..9246e0a 100644 --- a/.github/workflows/github_build_release.yml +++ b/.github/workflows/github_build_release.yml @@ -16,7 +16,7 @@ jobs: APP_ENV: prod steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Create a release in GitHub run: gh release create ${{ github.ref_name }} --verify-tag --generate-notes diff --git a/.github/workflows/markdown.yaml b/.github/workflows/markdown.yaml index cdd8b4b..4aa1189 100644 --- a/.github/workflows/markdown.yaml +++ b/.github/workflows/markdown.yaml @@ -34,7 +34,7 @@ jobs: fail-fast: false steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Create docker network run: | diff --git a/.github/workflows/php.yaml b/.github/workflows/php.yaml index b4ed005..0220370 100644 --- a/.github/workflows/php.yaml +++ b/.github/workflows/php.yaml @@ -15,7 +15,7 @@ jobs: name: PHP - Check Coding Standards runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Create docker network run: | @@ -29,7 +29,7 @@ jobs: name: PHPStan runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Create docker network run: | @@ -65,7 +65,7 @@ jobs: php: "8.5" prefer: prefer-stable steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Create docker network run: | diff --git a/.github/workflows/yaml.yaml b/.github/workflows/yaml.yaml index ba8ca52..2e910c0 100644 --- a/.github/workflows/yaml.yaml +++ b/.github/workflows/yaml.yaml @@ -31,7 +31,7 @@ jobs: yaml-lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Create docker network run: | diff --git a/.markdownlint.json b/.markdownlint.json deleted file mode 100644 index cf2915d..0000000 --- a/.markdownlint.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "default": true, - "line-length": { "code_blocks": false } -} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 606eed0..557a083 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [4.1.2] - 2026-05-11 + +- Chained `previous` consistently in `OpenIdConfigurationProvider` catch + blocks (`validateIdToken`, `getJwtVerificationKeys`, `fetchJsonResource`, + `getConfiguration`) so consumers can walk back to the underlying + Guzzle/firebase/PSR exception via `getPrevious()` +- Tightened `@throws` phpdoc on public methods (`validateIdToken`, + `getIdToken`, `getBaseAuthorizationUrl`) to enumerate the actual + transitive exceptions instead of declaring only the parent type. Removed + the inaccurate `ClientExceptionInterface` declaration on `getIdToken` + (the catch-all wraps it as `CodeException` with the original chained) +- Documented HTTP timeout/proxy/verify configuration via constructor `$options` + (capability already provided by league/oauth2-client; no code change) +- Bumped `actions/checkout` from v5 to v6 in all CI workflows +- Added `ci` profile to docker-compose matrix services to avoid starting them during local development +- Fixed `test:coverage` task to run via docker-compose with `XDEBUG_MODE=coverage` +- Fixed `test:run` to remove stale `composer.lock` before `composer update` +- Fixed `test:matrix:reset` to use `--profile ci` flag +- Removed unused `.markdownlint.json` + ## [4.1.1] - 2026-05-07 ### Security @@ -143,7 +163,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - This CHANGELOG file to hopefully serve as an evolving example of a standardized open source project CHANGELOG. -[Unreleased]: https://github.com/itk-dev/openid-connect/compare/4.1.1...HEAD +[Unreleased]: https://github.com/itk-dev/openid-connect/compare/4.1.2...HEAD +[4.1.2]: https://github.com/itk-dev/openid-connect/compare/4.1.1...4.1.2 [4.1.1]: https://github.com/itk-dev/openid-connect/compare/4.1.0...4.1.1 [4.1.0]: https://github.com/itk-dev/openid-connect/compare/4.0.3...4.1.0 [4.0.3]: https://github.com/itk-dev/openid-connect/compare/4.0.2...4.0.3 diff --git a/README.md b/README.md index 2c72d77..852c83d 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![Github](https://img.shields.io/badge/source-itk--dev/openid--connect-blue?style=flat-square)](https://github.com/itk-dev/openid-connect) [![Release](https://img.shields.io/packagist/v/itk-dev/openid-connect.svg?style=flat-square&label=release)](https://packagist.org/packages/itk-dev/openid-connect) [![PHP Version](https://img.shields.io/packagist/php-v/itk-dev/openid-connect.svg?style=flat-square&colorB=%238892BF)](https://www.php.net/downloads) -[![Build Status](https://img.shields.io/github/actions/workflow/status/itk-dev/openid-connect/pr.yaml?label=CI&logo=github&style=flat-square)](https://github.com/itk-dev/openid-connect/actions?query=workflow%3A%22Test+%26+Code+Style+Review%22) +[![Build Status](https://img.shields.io/github/actions/workflow/status/itk-dev/openid-connect/php.yaml?branch=develop&label=CI&logo=github&style=flat-square)](https://github.com/itk-dev/openid-connect/actions/workflows/php.yaml?query=branch%3Adevelop) [![Codecov Code Coverage](https://img.shields.io/codecov/c/gh/itk-dev/openid-connect?label=codecov&logo=codecov&style=flat-square)](https://codecov.io/gh/itk-dev/openid-connect) [![Read License](https://img.shields.io/packagist/l/itk-dev/openid-connect.svg?style=flat-square&colorB=darkcyan)](https://github.com/itk-dev/openid-connect/blob/master/LICENSE.md) [![Package downloads on Packagist](https://img.shields.io/packagist/dt/itk-dev/openid-connect.svg?style=flat-square&colorB=darkmagenta)](https://packagist.org/packages/itk-dev/openid-connect/stats) @@ -77,6 +77,33 @@ $provider = new OpenIdConfigurationProvider([ ]); ``` +##### HTTP timeout, proxy, and TLS verification + +This library extends `league/oauth2-client`, which uses Guzzle for HTTP. To +bound how long a request to the IdP can take (recommended for production), +pass `timeout` (seconds) in the constructor `$options`: + +```php +$provider = new OpenIdConfigurationProvider([ + // ... required options ... + 'timeout' => 5, + 'proxy' => 'http://proxy.example.com:8080', + 'verify' => true, // only consulted by Guzzle when proxy is set +]); +``` + +`league/oauth2-client` whitelists exactly these three keys (`timeout`, `proxy`, +`verify`) and forwards them to the underlying Guzzle client. Other Guzzle +options (e.g. `connect_timeout`) are silently dropped. + +> **Why Guzzle and not Symfony HttpClient?** +> `league/oauth2-client` hard-types its HTTP client as +> `GuzzleHttp\ClientInterface`. Symfony HttpClient implements PSR-18 / HTTPlug, +> not Guzzle's interface, and there is no maintained adapter going Symfony → +> Guzzle. To plug in a non-Guzzle client you would need to write such an +> adapter yourself and pass it via `$collaborators['httpClient']` to the +> constructor. + ##### Leeway To account for clock skew times between the signing and verifying servers, diff --git a/Taskfile.yml b/Taskfile.yml index 905c7f8..ca0349a 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -142,7 +142,7 @@ tasks: test:coverage: desc: Run tests with coverage cmds: - - "{{.PHP}} vendor/bin/phpunit --coverage-clover=coverage/unit.xml" + - "{{.DOCKER_COMPOSE}} exec -e XDEBUG_MODE=coverage phpfpm vendor/bin/phpunit --coverage-text --coverage-clover=coverage/unit.xml" test:run: desc: "Run tests for a PHP version and dependency set (e.g. task test:run PHP=8.4 DEPS=lowest)" @@ -162,6 +162,7 @@ tasks: cp /app/vendor/.composer.lock /app/composer.lock composer install -q else + rm -f /app/composer.lock composer update -q --{{.PREFER}} cp /app/composer.lock /app/vendor/.composer.lock fi' @@ -170,7 +171,7 @@ tasks: test:matrix:reset: desc: Remove cached vendor volumes to force a fresh dependency resolve cmds: - - "{{.DOCKER_COMPOSE}} down --volumes" + - "{{.DOCKER_COMPOSE}} --profile ci down --volumes" test:matrix: desc: Run tests across all PHP versions (mirrors CI matrix) diff --git a/docker-compose.yml b/docker-compose.yml index 64fc402..f71d58c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -30,16 +30,22 @@ services: extends: service: phpfpm image: itkdev/php8.4-fpm:latest + profiles: + - ci phpfpm85: extends: service: phpfpm image: itkdev/php8.5-fpm:latest + profiles: + - ci # Test matrix services (PHP version × dependency set) phpfpm83-stable: extends: service: phpfpm + profiles: + - ci volumes: - .:/app - phpfpm83-stable-vendor:/app/vendor @@ -48,6 +54,8 @@ services: phpfpm83-lowest: extends: service: phpfpm + profiles: + - ci volumes: - .:/app - phpfpm83-lowest-vendor:/app/vendor @@ -56,6 +64,8 @@ services: phpfpm84-stable: extends: service: phpfpm84 + profiles: + - ci volumes: - .:/app - phpfpm84-stable-vendor:/app/vendor @@ -64,6 +74,8 @@ services: phpfpm84-lowest: extends: service: phpfpm84 + profiles: + - ci volumes: - .:/app - phpfpm84-lowest-vendor:/app/vendor @@ -72,6 +84,8 @@ services: phpfpm85-stable: extends: service: phpfpm85 + profiles: + - ci volumes: - .:/app - phpfpm85-stable-vendor:/app/vendor @@ -80,6 +94,8 @@ services: phpfpm85-lowest: extends: service: phpfpm85 + profiles: + - ci volumes: - .:/app - phpfpm85-lowest-vendor:/app/vendor diff --git a/src/Security/OpenIdConfigurationProvider.php b/src/Security/OpenIdConfigurationProvider.php index bd4a10f..5b34a06 100644 --- a/src/Security/OpenIdConfigurationProvider.php +++ b/src/Security/OpenIdConfigurationProvider.php @@ -103,6 +103,11 @@ public function getGuarded(): array return ['cacheItemPool', 'cacheDuration', 'openIDConnectMetadataUrl', 'leeway', 'allowHttp']; } + /** + * @throws CacheException + * @throws HttpException + * @throws JsonException + */ public function getBaseAuthorizationUrl(): string { return $this->getConfiguration('authorization_endpoint'); @@ -197,7 +202,13 @@ public function getEndSessionUrl(?string $postLogoutRedirectUri = null, ?string * @return object * The JWT's payload as a PHP object * - * @throws ItkOpenIdConnectException + * @throws CacheException + * @throws ClaimsException + * @throws DecodeException + * @throws HttpException + * @throws JsonException + * @throws KeyException + * @throws ValidationException */ public function validateIdToken(string $idToken, string $nonce): object { @@ -222,7 +233,7 @@ public function validateIdToken(string $idToken, string $nonce): object return $claims; } catch (\UnexpectedValueException $e) { - throw new ValidationException('ID token validation failed: '.$e->getMessage()); + throw new ValidationException('ID token validation failed: '.$e->getMessage(), 0, $e); } } @@ -235,8 +246,7 @@ public function validateIdToken(string $idToken, string $nonce): object * @return string * The ID token * - * @throws CodeException - * @throws ClientExceptionInterface + * @throws CodeException Wraps any \Exception thrown by token-endpoint HTTP, JSON parsing, or `getConfiguration()` (with `previous` chained) */ public function getIdToken(string $code): string { @@ -371,7 +381,7 @@ private function getJwtVerificationKeys(): array $this->cacheItemPool->save($item); } } catch (InvalidArgumentException $e) { - throw new CacheException($e->getMessage()); + throw new CacheException($e->getMessage(), 0, $e); } return $keys; @@ -418,9 +428,9 @@ private function fetchJsonResource(string $resourceUrl): array return $resource; } catch (ClientExceptionInterface $e) { - throw new HttpException($e->getMessage()); + throw new HttpException($e->getMessage(), 0, $e); } catch (\JsonException $e) { - throw new JsonException($e->getMessage()); + throw new JsonException($e->getMessage(), 0, $e); } } @@ -461,7 +471,7 @@ private function getConfiguration(string $key): string return $value; } catch (InvalidArgumentException $e) { - throw new CacheException($e->getMessage()); + throw new CacheException($e->getMessage(), 0, $e); } }