From eb3f69b0bb5b8461bc56df9a6af8cf3979bd5488 Mon Sep 17 00:00:00 2001 From: Kevin J Gao <32936811+gaokevin1@users.noreply.github.com> Date: Sat, 4 Jan 2025 23:41:59 -0800 Subject: [PATCH] feat: Added JWK Caching and Support for Laravel 11 (#32) --- .github/workflows/phpunit.yml | 31 + README.md | 56 +- composer.json | 1 - composer.lock | 2354 +---------------- phpunit.xml | 8 + sample/dashboard.php | 1 + src/SDK/API.php | 42 +- src/SDK/Auth/SSO.php | 12 +- src/SDK/Cache/APCuCache.php | 21 + src/SDK/Cache/CacheInterface.php | 10 + src/SDK/Cache/NullCache.php | 21 + src/SDK/Configuration/SDKConfig.php | 53 +- src/SDK/DescopeSDK.php | 132 +- src/SDK/Exception/ValidationException.php | 57 + .../Password/UserPasswordBcrypt.php | 2 +- .../Password/UserPasswordDjango.php | 2 +- .../Password/UserPasswordFirebase.php | 2 +- .../Management/Password/UserPasswordMD5.php | 41 + .../Password/UserPasswordPHPass.php | 2 +- .../Password/UserPasswordPbkdf2.php | 2 +- src/SDK/Management/Role.php | 96 + src/SDK/Token/Extractor.php | 232 +- src/SDK/Token/Verifier.php | 181 +- src/tests/DescopeSDKTest.php | 46 +- src/tests/Management/UserPwdTest.php | 14 + src/tests/Management/UserTest.php | 3 - 26 files changed, 897 insertions(+), 2525 deletions(-) create mode 100644 .github/workflows/phpunit.yml create mode 100644 phpunit.xml create mode 100644 src/SDK/Cache/APCuCache.php create mode 100644 src/SDK/Cache/CacheInterface.php create mode 100644 src/SDK/Cache/NullCache.php create mode 100644 src/SDK/Exception/ValidationException.php create mode 100644 src/SDK/Management/Password/UserPasswordMD5.php create mode 100644 src/SDK/Management/Role.php diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml new file mode 100644 index 0000000..101487b --- /dev/null +++ b/.github/workflows/phpunit.yml @@ -0,0 +1,31 @@ +name: PHP Unit Tests + +on: + pull_request: + branches: + - main + +jobs: + test: + runs-on: ubuntu-latest + + env: + DESCOPE_PROJECT_ID: ${{ vars.DESCOPE_PROJECT_ID }} + + strategy: + matrix: + php-version: [8.1, 7.4] + + steps: + - uses: actions/checkout@v2 + + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-version }} + + - name: Install dependencies + run: composer install + + - name: Run tests + run: vendor/bin/phpunit --configuration phpunit.xml \ No newline at end of file diff --git a/README.md b/README.md index 3778e3b..03a24d9 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,61 @@ $descopeSDK = new DescopeSDK([ ]); ``` -This SDK will easily allow you to handle Descope JWT tokens with the following built-in functions: +### Caching Mechanism + +The Descope PHP SDK uses a caching mechanism to store frequently accessed data, such as JSON Web Key Sets (JWKs) for session token validation. By default, the SDK uses **APCu** for caching, provided it is enabled and configured in your environment. If APCu is not available, and no other caching mechanism is provided, caching is disabled. + +By using the `CacheInterface`, you can integrate the Descope PHP SDK with any caching mechanism that suits your application, ensuring optimal performance in both small and large-scale deployments. + +#### Custom Caching with `CacheInterface` + +The SDK allows you to provide a custom caching mechanism by implementing the `CacheInterface`. This interface defines three methods that any cache implementation should support: + +- `get(string $key)`: Retrieve a value by key. +- `set(string $key, $value, int $ttl = 3600): bool`: Store a value with a specified time-to-live (TTL). +- `delete(string $key): bool`: Remove a value by key. + +You can provide your custom caching implementation by creating a class that implements `CacheInterface`. Here’s an example using Laravel’s cache system: + +```php +namespace App\Cache; + +use Descope\SDK\Cache\CacheInterface; +use Illuminate\Support\Facades\Cache; + +class LaravelCache implements CacheInterface +{ + public function get(string $key) + { + return Cache::get($key); + } + + public function set(string $key, $value, int $ttl = 3600): bool + { + // Laravel TTL is in minutes + return Cache::put($key, $value, max(1, ceil($ttl / 60))); + } + + public function delete(string $key): bool + { + return Cache::forget($key); + } +} +``` + +To use the Laravel cache in the SDK: + +```php +use Descope\SDK\DescopeSDK; +use App\Cache\LaravelCache; + +$descopeSDK = new DescopeSDK([ + 'projectId' => $_ENV['DESCOPE_PROJECT_ID'], + 'managementKey' => $_ENV['DESCOPE_MANAGEMENT_KEY'], +], new LaravelCache()); +``` + +Once you've configured your caching, you're ready to use the SDK. This SDK will easily allow you integrate Descope functionality with the following built-in functions: ## Password Authentication diff --git a/composer.json b/composer.json index 79020bc..dd29eb7 100644 --- a/composer.json +++ b/composer.json @@ -16,7 +16,6 @@ ], "require": { "php": "^7.3 || ^8.0", - "web-token/jwt-framework": "2.2.11 as 2.3.0", "guzzlehttp/guzzle": "7.9.2 as 7.9.3", "paragonie/constant_time_encoding": "^2.7.0", "vlucas/phpdotenv": "^5.6.1" diff --git a/composer.lock b/composer.lock index d26365e..9f6cc75 100644 --- a/composer.lock +++ b/composer.lock @@ -4,144 +4,8 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "35e70dbd77fca8551fdaf29f9d79b765", + "content-hash": "445d03808865bcc00bd5191e7902ce33", "packages": [ - { - "name": "brick/math", - "version": "0.9.3", - "source": { - "type": "git", - "url": "https://github.com/brick/math.git", - "reference": "ca57d18f028f84f777b2168cd1911b0dee2343ae" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/brick/math/zipball/ca57d18f028f84f777b2168cd1911b0dee2343ae", - "reference": "ca57d18f028f84f777b2168cd1911b0dee2343ae", - "shasum": "" - }, - "require": { - "ext-json": "*", - "php": "^7.1 || ^8.0" - }, - "require-dev": { - "php-coveralls/php-coveralls": "^2.2", - "phpunit/phpunit": "^7.5.15 || ^8.5 || ^9.0", - "vimeo/psalm": "4.9.2" - }, - "type": "library", - "autoload": { - "psr-4": { - "Brick\\Math\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "Arbitrary-precision arithmetic library", - "keywords": [ - "Arbitrary-precision", - "BigInteger", - "BigRational", - "arithmetic", - "bigdecimal", - "bignum", - "brick", - "math" - ], - "support": { - "issues": "https://github.com/brick/math/issues", - "source": "https://github.com/brick/math/tree/0.9.3" - }, - "funding": [ - { - "url": "https://github.com/BenMorel", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/brick/math", - "type": "tidelift" - } - ], - "time": "2021-08-15T20:50:18+00:00" - }, - { - "name": "fgrosse/phpasn1", - "version": "v2.5.0", - "source": { - "type": "git", - "url": "https://github.com/fgrosse/PHPASN1.git", - "reference": "42060ed45344789fb9f21f9f1864fc47b9e3507b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/fgrosse/PHPASN1/zipball/42060ed45344789fb9f21f9f1864fc47b9e3507b", - "reference": "42060ed45344789fb9f21f9f1864fc47b9e3507b", - "shasum": "" - }, - "require": { - "php": "^7.1 || ^8.0" - }, - "require-dev": { - "php-coveralls/php-coveralls": "~2.0", - "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" - }, - "suggest": { - "ext-bcmath": "BCmath is the fallback extension for big integer calculations", - "ext-curl": "For loading OID information from the web if they have not bee defined statically", - "ext-gmp": "GMP is the preferred extension for big integer calculations", - "phpseclib/bcmath_compat": "BCmath polyfill for servers where neither GMP nor BCmath is available" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "FG\\": "lib/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Friedrich Große", - "email": "friedrich.grosse@gmail.com", - "homepage": "https://github.com/FGrosse", - "role": "Author" - }, - { - "name": "All contributors", - "homepage": "https://github.com/FGrosse/PHPASN1/contributors" - } - ], - "description": "A PHP Framework that allows you to encode and decode arbitrary ASN.1 structures using the ITU-T X.690 Encoding Rules.", - "homepage": "https://github.com/FGrosse/PHPASN1", - "keywords": [ - "DER", - "asn.1", - "asn1", - "ber", - "binary", - "decoding", - "encoding", - "x.509", - "x.690", - "x509", - "x690" - ], - "support": { - "issues": "https://github.com/fgrosse/PHPASN1/issues", - "source": "https://github.com/fgrosse/PHPASN1/tree/v2.5.0" - }, - "abandoned": true, - "time": "2022-12-19T11:08:26+00:00" - }, { "name": "graham-campbell/result-type", "version": "v1.1.3", @@ -332,16 +196,16 @@ }, { "name": "guzzlehttp/promises", - "version": "2.0.3", + "version": "2.0.4", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "6ea8dd08867a2a42619d65c3deb2c0fcbf81c8f8" + "reference": "f9c436286ab2892c7db7be8c8da4ef61ccf7b455" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/6ea8dd08867a2a42619d65c3deb2c0fcbf81c8f8", - "reference": "6ea8dd08867a2a42619d65c3deb2c0fcbf81c8f8", + "url": "https://api.github.com/repos/guzzle/promises/zipball/f9c436286ab2892c7db7be8c8da4ef61ccf7b455", + "reference": "f9c436286ab2892c7db7be8c8da4ef61ccf7b455", "shasum": "" }, "require": { @@ -395,7 +259,7 @@ ], "support": { "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/2.0.3" + "source": "https://github.com/guzzle/promises/tree/2.0.4" }, "funding": [ { @@ -411,7 +275,7 @@ "type": "tidelift" } ], - "time": "2024-07-18T10:29:17+00:00" + "time": "2024-10-17T10:06:22+00:00" }, { "name": "guzzlehttp/psr7", @@ -671,104 +535,6 @@ ], "time": "2024-07-20T21:41:07+00:00" }, - { - "name": "psr/container", - "version": "1.1.2", - "source": { - "type": "git", - "url": "https://github.com/php-fig/container.git", - "reference": "513e0666f7216c7459170d56df27dfcefe1689ea" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/container/zipball/513e0666f7216c7459170d56df27dfcefe1689ea", - "reference": "513e0666f7216c7459170d56df27dfcefe1689ea", - "shasum": "" - }, - "require": { - "php": ">=7.4.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Psr\\Container\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "Common Container Interface (PHP FIG PSR-11)", - "homepage": "https://github.com/php-fig/container", - "keywords": [ - "PSR-11", - "container", - "container-interface", - "container-interop", - "psr" - ], - "support": { - "issues": "https://github.com/php-fig/container/issues", - "source": "https://github.com/php-fig/container/tree/1.1.2" - }, - "time": "2021-11-05T16:50:12+00:00" - }, - { - "name": "psr/event-dispatcher", - "version": "1.0.0", - "source": { - "type": "git", - "url": "https://github.com/php-fig/event-dispatcher.git", - "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", - "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", - "shasum": "" - }, - "require": { - "php": ">=7.2.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\EventDispatcher\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" - } - ], - "description": "Standard interfaces for event handling.", - "keywords": [ - "events", - "psr", - "psr-14" - ], - "support": { - "issues": "https://github.com/php-fig/event-dispatcher/issues", - "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" - }, - "time": "2019-01-08T18:20:26+00:00" - }, { "name": "psr/http-client", "version": "1.0.3", @@ -929,1487 +695,31 @@ }, "time": "2023-04-04T09:54:51+00:00" }, - { - "name": "psr/log", - "version": "1.1.4", - "source": { - "type": "git", - "url": "https://github.com/php-fig/log.git", - "reference": "d49695b909c3b7628b6289db5479a1c204601f11" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11", - "reference": "d49695b909c3b7628b6289db5479a1c204601f11", - "shasum": "" - }, - "require": { - "php": ">=5.3.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Log\\": "Psr/Log/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "Common interface for logging libraries", - "homepage": "https://github.com/php-fig/log", - "keywords": [ - "log", - "psr", - "psr-3" - ], - "support": { - "source": "https://github.com/php-fig/log/tree/1.1.4" - }, - "time": "2021-05-03T11:20:27+00:00" - }, { "name": "ralouphie/getallheaders", "version": "3.0.3", "source": { "type": "git", "url": "https://github.com/ralouphie/getallheaders.git", - "reference": "120b605dfeb996808c31b6477290a714d356e822" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", - "reference": "120b605dfeb996808c31b6477290a714d356e822", - "shasum": "" - }, - "require": { - "php": ">=5.6" - }, - "require-dev": { - "php-coveralls/php-coveralls": "^2.1", - "phpunit/phpunit": "^5 || ^6.5" - }, - "type": "library", - "autoload": { - "files": [ - "src/getallheaders.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Ralph Khattar", - "email": "ralph.khattar@gmail.com" - } - ], - "description": "A polyfill for getallheaders.", - "support": { - "issues": "https://github.com/ralouphie/getallheaders/issues", - "source": "https://github.com/ralouphie/getallheaders/tree/develop" - }, - "time": "2019-03-08T08:55:37+00:00" - }, - { - "name": "spomky-labs/aes-key-wrap", - "version": "v6.0.0", - "source": { - "type": "git", - "url": "https://github.com/Spomky-Labs/aes-key-wrap.git", - "reference": "97388255a37ad6fb1ed332d07e61fa2b7bb62e0d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Spomky-Labs/aes-key-wrap/zipball/97388255a37ad6fb1ed332d07e61fa2b7bb62e0d", - "reference": "97388255a37ad6fb1ed332d07e61fa2b7bb62e0d", - "shasum": "" - }, - "require": { - "ext-mbstring": "*", - "lib-openssl": "*", - "php": ">=7.2", - "thecodingmachine/safe": "^1.1" - }, - "require-dev": { - "php-coveralls/php-coveralls": "^2.0", - "phpstan/phpstan": "^0.12", - "phpstan/phpstan-beberlei-assert": "^0.12", - "phpstan/phpstan-deprecation-rules": "^0.12", - "phpstan/phpstan-phpunit": "^0.12", - "phpstan/phpstan-strict-rules": "^0.12", - "phpunit/phpunit": "^7.0|^8.0|^9.0", - "thecodingmachine/phpstan-safe-rule": "^1.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "AESKW\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Florent Morselli", - "homepage": "https://github.com/Spomky-Labs/aes-key-wrap/contributors" - } - ], - "description": "AES Key Wrap for PHP.", - "homepage": "https://github.com/Spomky-Labs/aes-key-wrap", - "keywords": [ - "A128KW", - "A192KW", - "A256KW", - "RFC3394", - "RFC5649", - "aes", - "key", - "padding", - "wrap" - ], - "support": { - "issues": "https://github.com/Spomky-Labs/aes-key-wrap/issues", - "source": "https://github.com/Spomky-Labs/aes-key-wrap/tree/v6.0.0" - }, - "time": "2020-08-01T14:07:55+00:00" - }, - { - "name": "spomky-labs/base64url", - "version": "v2.0.4", - "source": { - "type": "git", - "url": "https://github.com/Spomky-Labs/base64url.git", - "reference": "7752ce931ec285da4ed1f4c5aa27e45e097be61d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Spomky-Labs/base64url/zipball/7752ce931ec285da4ed1f4c5aa27e45e097be61d", - "reference": "7752ce931ec285da4ed1f4c5aa27e45e097be61d", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "require-dev": { - "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "^0.11|^0.12", - "phpstan/phpstan-beberlei-assert": "^0.11|^0.12", - "phpstan/phpstan-deprecation-rules": "^0.11|^0.12", - "phpstan/phpstan-phpunit": "^0.11|^0.12", - "phpstan/phpstan-strict-rules": "^0.11|^0.12" - }, - "type": "library", - "autoload": { - "psr-4": { - "Base64Url\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Florent Morselli", - "homepage": "https://github.com/Spomky-Labs/base64url/contributors" - } - ], - "description": "Base 64 URL Safe Encoding/Decoding PHP Library", - "homepage": "https://github.com/Spomky-Labs/base64url", - "keywords": [ - "base64", - "rfc4648", - "safe", - "url" - ], - "support": { - "issues": "https://github.com/Spomky-Labs/base64url/issues", - "source": "https://github.com/Spomky-Labs/base64url/tree/v2.0.4" - }, - "funding": [ - { - "url": "https://github.com/Spomky", - "type": "github" - }, - { - "url": "https://www.patreon.com/FlorentMorselli", - "type": "patreon" - } - ], - "time": "2020-11-03T09:10:25+00:00" - }, - { - "name": "symfony/config", - "version": "v5.4.40", - "source": { - "type": "git", - "url": "https://github.com/symfony/config.git", - "reference": "d4e1db78421163b98dd9971d247fd0df4a57ee5e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/d4e1db78421163b98dd9971d247fd0df4a57ee5e", - "reference": "d4e1db78421163b98dd9971d247fd0df4a57ee5e", - "shasum": "" - }, - "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/filesystem": "^4.4|^5.0|^6.0", - "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-php80": "^1.16", - "symfony/polyfill-php81": "^1.22" - }, - "conflict": { - "symfony/finder": "<4.4" - }, - "require-dev": { - "symfony/event-dispatcher": "^4.4|^5.0|^6.0", - "symfony/finder": "^4.4|^5.0|^6.0", - "symfony/messenger": "^4.4|^5.0|^6.0", - "symfony/service-contracts": "^1.1|^2|^3", - "symfony/yaml": "^4.4|^5.0|^6.0" - }, - "suggest": { - "symfony/yaml": "To use the yaml reference dumper" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\Config\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/config/tree/v5.4.40" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-05-31T14:33:22+00:00" - }, - { - "name": "symfony/console", - "version": "v5.4.44", - "source": { - "type": "git", - "url": "https://github.com/symfony/console.git", - "reference": "5b5a0aa66e3296e303e22490f90f521551835a83" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/5b5a0aa66e3296e303e22490f90f521551835a83", - "reference": "5b5a0aa66e3296e303e22490f90f521551835a83", - "shasum": "" - }, - "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/polyfill-mbstring": "~1.0", - "symfony/polyfill-php73": "^1.9", - "symfony/polyfill-php80": "^1.16", - "symfony/service-contracts": "^1.1|^2|^3", - "symfony/string": "^5.1|^6.0" - }, - "conflict": { - "psr/log": ">=3", - "symfony/dependency-injection": "<4.4", - "symfony/dotenv": "<5.1", - "symfony/event-dispatcher": "<4.4", - "symfony/lock": "<4.4", - "symfony/process": "<4.4" - }, - "provide": { - "psr/log-implementation": "1.0|2.0" - }, - "require-dev": { - "psr/log": "^1|^2", - "symfony/config": "^4.4|^5.0|^6.0", - "symfony/dependency-injection": "^4.4|^5.0|^6.0", - "symfony/event-dispatcher": "^4.4|^5.0|^6.0", - "symfony/lock": "^4.4|^5.0|^6.0", - "symfony/process": "^4.4|^5.0|^6.0", - "symfony/var-dumper": "^4.4|^5.0|^6.0" - }, - "suggest": { - "psr/log": "For using the console logger", - "symfony/event-dispatcher": "", - "symfony/lock": "", - "symfony/process": "" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\Console\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Eases the creation of beautiful and testable command line interfaces", - "homepage": "https://symfony.com", - "keywords": [ - "cli", - "command-line", - "console", - "terminal" - ], - "support": { - "source": "https://github.com/symfony/console/tree/v5.4.44" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-09-20T07:56:40+00:00" - }, - { - "name": "symfony/dependency-injection", - "version": "v5.4.44", - "source": { - "type": "git", - "url": "https://github.com/symfony/dependency-injection.git", - "reference": "23eb9f3803a931aef16a65f362a9aeb0640a1374" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/23eb9f3803a931aef16a65f362a9aeb0640a1374", - "reference": "23eb9f3803a931aef16a65f362a9aeb0640a1374", - "shasum": "" - }, - "require": { - "php": ">=7.2.5", - "psr/container": "^1.1.1", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/polyfill-php80": "^1.16", - "symfony/polyfill-php81": "^1.22", - "symfony/service-contracts": "^1.1.6|^2" - }, - "conflict": { - "ext-psr": "<1.1|>=2", - "symfony/config": "<5.3", - "symfony/finder": "<4.4", - "symfony/proxy-manager-bridge": "<4.4", - "symfony/yaml": "<4.4.26" - }, - "provide": { - "psr/container-implementation": "1.0", - "symfony/service-implementation": "1.0|2.0" - }, - "require-dev": { - "symfony/config": "^5.3|^6.0", - "symfony/expression-language": "^4.4|^5.0|^6.0", - "symfony/yaml": "^4.4.26|^5.0|^6.0" - }, - "suggest": { - "symfony/config": "", - "symfony/expression-language": "For using expressions in service container configuration", - "symfony/finder": "For using double-star glob patterns or when GLOB_BRACE portability is required", - "symfony/proxy-manager-bridge": "Generate service proxies to lazy load them", - "symfony/yaml": "" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\DependencyInjection\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Allows you to standardize and centralize the way objects are constructed in your application", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/dependency-injection/tree/v5.4.44" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-09-12T20:01:35+00:00" - }, - { - "name": "symfony/deprecation-contracts", - "version": "v2.5.3", - "source": { - "type": "git", - "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "80d075412b557d41002320b96a096ca65aa2c98d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/80d075412b557d41002320b96a096ca65aa2c98d", - "reference": "80d075412b557d41002320b96a096ca65aa2c98d", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "2.5-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" - } - }, - "autoload": { - "files": [ - "function.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "A generic function and convention to trigger deprecation notices", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.3" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2023-01-24T14:02:46+00:00" - }, - { - "name": "symfony/error-handler", - "version": "v5.4.42", - "source": { - "type": "git", - "url": "https://github.com/symfony/error-handler.git", - "reference": "db15ba0fd50890156ed40087ccedc7851a1f5b76" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/db15ba0fd50890156ed40087ccedc7851a1f5b76", - "reference": "db15ba0fd50890156ed40087ccedc7851a1f5b76", - "shasum": "" - }, - "require": { - "php": ">=7.2.5", - "psr/log": "^1|^2|^3", - "symfony/var-dumper": "^4.4|^5.0|^6.0" - }, - "require-dev": { - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/http-kernel": "^4.4|^5.0|^6.0", - "symfony/serializer": "^4.4|^5.0|^6.0" - }, - "bin": [ - "Resources/bin/patch-type-declarations" - ], - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\ErrorHandler\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Provides tools to manage errors and ease debugging PHP code", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/error-handler/tree/v5.4.42" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-07-23T12:34:05+00:00" - }, - { - "name": "symfony/event-dispatcher", - "version": "v5.4.40", - "source": { - "type": "git", - "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "a54e2a8a114065f31020d6a89ede83e34c3b27a4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/a54e2a8a114065f31020d6a89ede83e34c3b27a4", - "reference": "a54e2a8a114065f31020d6a89ede83e34c3b27a4", - "shasum": "" - }, - "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/event-dispatcher-contracts": "^2|^3", - "symfony/polyfill-php80": "^1.16" - }, - "conflict": { - "symfony/dependency-injection": "<4.4" - }, - "provide": { - "psr/event-dispatcher-implementation": "1.0", - "symfony/event-dispatcher-implementation": "2.0" - }, - "require-dev": { - "psr/log": "^1|^2|^3", - "symfony/config": "^4.4|^5.0|^6.0", - "symfony/dependency-injection": "^4.4|^5.0|^6.0", - "symfony/error-handler": "^4.4|^5.0|^6.0", - "symfony/expression-language": "^4.4|^5.0|^6.0", - "symfony/http-foundation": "^4.4|^5.0|^6.0", - "symfony/service-contracts": "^1.1|^2|^3", - "symfony/stopwatch": "^4.4|^5.0|^6.0" - }, - "suggest": { - "symfony/dependency-injection": "", - "symfony/http-kernel": "" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\EventDispatcher\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v5.4.40" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-05-31T14:33:22+00:00" - }, - { - "name": "symfony/event-dispatcher-contracts", - "version": "v2.5.3", - "source": { - "type": "git", - "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "540f4c73e87fd0c71ca44a6aa305d024ac68cb73" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/540f4c73e87fd0c71ca44a6aa305d024ac68cb73", - "reference": "540f4c73e87fd0c71ca44a6aa305d024ac68cb73", - "shasum": "" - }, - "require": { - "php": ">=7.2.5", - "psr/event-dispatcher": "^1" - }, - "suggest": { - "symfony/event-dispatcher-implementation": "" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "2.5-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Contracts\\EventDispatcher\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Generic abstractions related to dispatching event", - "homepage": "https://symfony.com", - "keywords": [ - "abstractions", - "contracts", - "decoupling", - "interfaces", - "interoperability", - "standards" - ], - "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v2.5.3" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-01-23T13:51:25+00:00" - }, - { - "name": "symfony/filesystem", - "version": "v5.4.44", - "source": { - "type": "git", - "url": "https://github.com/symfony/filesystem.git", - "reference": "76c3818964e9d32be3862c9318ae3ba9aa280ddc" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/76c3818964e9d32be3862c9318ae3ba9aa280ddc", - "reference": "76c3818964e9d32be3862c9318ae3ba9aa280ddc", - "shasum": "" - }, - "require": { - "php": ">=7.2.5", - "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-mbstring": "~1.8", - "symfony/polyfill-php80": "^1.16" - }, - "require-dev": { - "symfony/process": "^5.4|^6.4" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\Filesystem\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Provides basic utilities for the filesystem", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/filesystem/tree/v5.4.44" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-09-16T14:52:48+00:00" - }, - { - "name": "symfony/http-foundation", - "version": "v5.4.44", - "source": { - "type": "git", - "url": "https://github.com/symfony/http-foundation.git", - "reference": "ae0d217e5932aa0b70ddb4cf7822cc76d48aee53" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/ae0d217e5932aa0b70ddb4cf7822cc76d48aee53", - "reference": "ae0d217e5932aa0b70ddb4cf7822cc76d48aee53", - "shasum": "" - }, - "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/polyfill-mbstring": "~1.1", - "symfony/polyfill-php80": "^1.16" - }, - "require-dev": { - "predis/predis": "^1.0|^2.0", - "symfony/cache": "^4.4|^5.0|^6.0", - "symfony/dependency-injection": "^5.4|^6.0", - "symfony/expression-language": "^4.4|^5.0|^6.0", - "symfony/http-kernel": "^5.4.12|^6.0.12|^6.1.4", - "symfony/mime": "^4.4|^5.0|^6.0", - "symfony/rate-limiter": "^5.2|^6.0" - }, - "suggest": { - "symfony/mime": "To use the file extension guesser" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\HttpFoundation\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Defines an object-oriented layer for the HTTP specification", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/http-foundation/tree/v5.4.44" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-09-15T07:55:06+00:00" - }, - { - "name": "symfony/http-kernel", - "version": "v5.4.44", - "source": { - "type": "git", - "url": "https://github.com/symfony/http-kernel.git", - "reference": "788dcf72d9af7432a886aa3b0c5904d68087ba13" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/788dcf72d9af7432a886aa3b0c5904d68087ba13", - "reference": "788dcf72d9af7432a886aa3b0c5904d68087ba13", - "shasum": "" - }, - "require": { - "php": ">=7.2.5", - "psr/log": "^1|^2", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/error-handler": "^4.4|^5.0|^6.0", - "symfony/event-dispatcher": "^5.0|^6.0", - "symfony/http-foundation": "^5.4.21|^6.2.7", - "symfony/polyfill-ctype": "^1.8", - "symfony/polyfill-php73": "^1.9", - "symfony/polyfill-php80": "^1.16" - }, - "conflict": { - "symfony/browser-kit": "<5.4", - "symfony/cache": "<5.0", - "symfony/config": "<5.0", - "symfony/console": "<4.4", - "symfony/dependency-injection": "<5.3", - "symfony/doctrine-bridge": "<5.0", - "symfony/form": "<5.0", - "symfony/http-client": "<5.0", - "symfony/mailer": "<5.0", - "symfony/messenger": "<5.0", - "symfony/translation": "<5.0", - "symfony/twig-bridge": "<5.0", - "symfony/validator": "<5.0", - "twig/twig": "<2.13" - }, - "provide": { - "psr/log-implementation": "1.0|2.0" - }, - "require-dev": { - "psr/cache": "^1.0|^2.0|^3.0", - "symfony/browser-kit": "^5.4|^6.0", - "symfony/config": "^5.0|^6.0", - "symfony/console": "^4.4|^5.0|^6.0", - "symfony/css-selector": "^4.4|^5.0|^6.0", - "symfony/dependency-injection": "^5.3|^6.0", - "symfony/dom-crawler": "^4.4|^5.0|^6.0", - "symfony/expression-language": "^4.4|^5.0|^6.0", - "symfony/finder": "^4.4|^5.0|^6.0", - "symfony/http-client-contracts": "^1.1|^2|^3", - "symfony/process": "^4.4|^5.0|^6.0", - "symfony/routing": "^4.4|^5.0|^6.0", - "symfony/stopwatch": "^4.4|^5.0|^6.0", - "symfony/translation": "^4.4|^5.0|^6.0", - "symfony/translation-contracts": "^1.1|^2|^3", - "symfony/var-dumper": "^4.4.31|^5.4", - "twig/twig": "^2.13|^3.0.4" - }, - "suggest": { - "symfony/browser-kit": "", - "symfony/config": "", - "symfony/console": "", - "symfony/dependency-injection": "" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\HttpKernel\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Provides a structured process for converting a Request into a Response", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/http-kernel/tree/v5.4.44" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-09-21T05:47:58+00:00" - }, - { - "name": "symfony/polyfill-ctype", - "version": "v1.31.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", - "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", - "shasum": "" - }, - "require": { - "php": ">=7.2" - }, - "provide": { - "ext-ctype": "*" - }, - "suggest": { - "ext-ctype": "For best performance" - }, - "type": "library", - "extra": { - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Ctype\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Gert de Pagter", - "email": "BackEndTea@gmail.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for ctype functions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "ctype", - "polyfill", - "portable" - ], - "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-09-09T11:45:10+00:00" - }, - { - "name": "symfony/polyfill-intl-grapheme", - "version": "v1.31.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", - "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", - "shasum": "" - }, - "require": { - "php": ">=7.2" - }, - "suggest": { - "ext-intl": "For best performance" - }, - "type": "library", - "extra": { - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Intl\\Grapheme\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for intl's grapheme_* functions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "grapheme", - "intl", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.31.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-09-09T11:45:10+00:00" - }, - { - "name": "symfony/polyfill-intl-normalizer", - "version": "v1.31.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "3833d7255cc303546435cb650316bff708a1c75c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", - "reference": "3833d7255cc303546435cb650316bff708a1c75c", - "shasum": "" - }, - "require": { - "php": ">=7.2" - }, - "suggest": { - "ext-intl": "For best performance" - }, - "type": "library", - "extra": { - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Intl\\Normalizer\\": "" - }, - "classmap": [ - "Resources/stubs" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for intl's Normalizer class and related functions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "intl", - "normalizer", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.31.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-09-09T11:45:10+00:00" - }, - { - "name": "symfony/polyfill-mbstring", - "version": "v1.31.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341", - "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341", - "shasum": "" - }, - "require": { - "php": ">=7.2" - }, - "provide": { - "ext-mbstring": "*" - }, - "suggest": { - "ext-mbstring": "For best performance" - }, - "type": "library", - "extra": { - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Mbstring\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for the Mbstring extension", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "mbstring", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-09-09T11:45:10+00:00" - }, - { - "name": "symfony/polyfill-php73", - "version": "v1.31.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-php73.git", - "reference": "0f68c03565dcaaf25a890667542e8bd75fe7e5bb" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/0f68c03565dcaaf25a890667542e8bd75fe7e5bb", - "reference": "0f68c03565dcaaf25a890667542e8bd75fe7e5bb", - "shasum": "" - }, - "require": { - "php": ">=7.2" - }, - "type": "library", - "extra": { - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Php73\\": "" - }, - "classmap": [ - "Resources/stubs" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-php73/tree/v1.31.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-09-09T11:45:10+00:00" - }, - { - "name": "symfony/polyfill-php80", - "version": "v1.31.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8" + "reference": "120b605dfeb996808c31b6477290a714d356e822" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", - "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", "shasum": "" }, "require": { - "php": ">=7.2" + "php": ">=5.6" }, - "type": "library", - "extra": { - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" }, + "type": "library", "autoload": { "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Php80\\": "" - }, - "classmap": [ - "Resources/stubs" + "src/getallheaders.php" ] }, "notification-url": "https://packagist.org/downloads/", @@ -2418,78 +728,47 @@ ], "authors": [ { - "name": "Ion Bazan", - "email": "ion.bazan@gmail.com" - }, - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" } ], - "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], + "description": "A polyfill for getallheaders.", "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.31.0" + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2019-03-08T08:55:37+00:00" }, { - "name": "symfony/polyfill-php81", - "version": "v1.31.0", + "name": "symfony/deprecation-contracts", + "version": "v2.5.4", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php81.git", - "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c" + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "605389f2a7e5625f273b53960dc46aeaf9c62918" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", - "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/605389f2a7e5625f273b53960dc46aeaf9c62918", + "reference": "605389f2a7e5625f273b53960dc46aeaf9c62918", "shasum": "" }, "require": { - "php": ">=7.2" + "php": ">=7.1" }, "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "2.5-dev" } }, "autoload": { "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Php81\\": "" - }, - "classmap": [ - "Resources/stubs" + "function.php" ] }, "notification-url": "https://packagist.org/downloads/", @@ -2506,16 +785,10 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions", + "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], "support": { - "source": "https://github.com/symfony/polyfill-php81/tree/v1.31.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.4" }, "funding": [ { @@ -2531,46 +804,44 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2024-09-25T14:11:13+00:00" }, { - "name": "symfony/service-contracts", - "version": "v2.5.3", + "name": "symfony/polyfill-ctype", + "version": "v1.31.0", "source": { "type": "git", - "url": "https://github.com/symfony/service-contracts.git", - "reference": "a2329596ddc8fd568900e3fc76cba42489ecc7f3" + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/a2329596ddc8fd568900e3fc76cba42489ecc7f3", - "reference": "a2329596ddc8fd568900e3fc76cba42489ecc7f3", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", "shasum": "" }, "require": { - "php": ">=7.2.5", - "psr/container": "^1.1", - "symfony/deprecation-contracts": "^2.1|^3" + "php": ">=7.2" }, - "conflict": { - "ext-psr": "<1.1|>=2" + "provide": { + "ext-ctype": "*" }, "suggest": { - "symfony/service-implementation": "" + "ext-ctype": "For best performance" }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "2.5-dev" - }, "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { + "files": [ + "bootstrap.php" + ], "psr-4": { - "Symfony\\Contracts\\Service\\": "" + "Symfony\\Polyfill\\Ctype\\": "" } }, "notification-url": "https://packagist.org/downloads/", @@ -2579,26 +850,24 @@ ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Generic abstractions related to writing services", + "description": "Symfony polyfill for ctype functions", "homepage": "https://symfony.com", "keywords": [ - "abstractions", - "contracts", - "decoupling", - "interfaces", - "interoperability", - "standards" + "compatibility", + "ctype", + "polyfill", + "portable" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v2.5.3" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0" }, "funding": [ { @@ -2614,50 +883,45 @@ "type": "tidelift" } ], - "time": "2023-04-21T15:04:16+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { - "name": "symfony/string", - "version": "v5.4.44", + "name": "symfony/polyfill-mbstring", + "version": "v1.31.0", "source": { "type": "git", - "url": "https://github.com/symfony/string.git", - "reference": "832caa16b6d9aac6bf11747315225f5aba384c24" + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/832caa16b6d9aac6bf11747315225f5aba384c24", - "reference": "832caa16b6d9aac6bf11747315225f5aba384c24", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341", + "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-intl-grapheme": "~1.0", - "symfony/polyfill-intl-normalizer": "~1.0", - "symfony/polyfill-mbstring": "~1.0", - "symfony/polyfill-php80": "~1.15" + "php": ">=7.2" }, - "conflict": { - "symfony/translation-contracts": ">=3.0" + "provide": { + "ext-mbstring": "*" }, - "require-dev": { - "symfony/error-handler": "^4.4|^5.0|^6.0", - "symfony/http-client": "^4.4|^5.0|^6.0", - "symfony/translation-contracts": "^1.1|^2", - "symfony/var-exporter": "^4.4|^5.0|^6.0" + "suggest": { + "ext-mbstring": "For best performance" }, "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, "autoload": { "files": [ - "Resources/functions.php" + "bootstrap.php" ], "psr-4": { - "Symfony\\Component\\String\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] + "Symfony\\Polyfill\\Mbstring\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -2673,18 +937,17 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "description": "Symfony polyfill for the Mbstring extension", "homepage": "https://symfony.com", "keywords": [ - "grapheme", - "i18n", - "string", - "unicode", - "utf-8", - "utf8" + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" ], "support": { - "source": "https://github.com/symfony/string/tree/v5.4.44" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0" }, "funding": [ { @@ -2700,56 +963,41 @@ "type": "tidelift" } ], - "time": "2024-09-20T07:56:40+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { - "name": "symfony/var-dumper", - "version": "v5.4.43", + "name": "symfony/polyfill-php80", + "version": "v1.31.0", "source": { "type": "git", - "url": "https://github.com/symfony/var-dumper.git", - "reference": "6be6a6a8af4818564e3726fc65cf936f34743cef" + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/6be6a6a8af4818564e3726fc65cf936f34743cef", - "reference": "6be6a6a8af4818564e3726fc65cf936f34743cef", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", + "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/polyfill-mbstring": "~1.0", - "symfony/polyfill-php80": "^1.16" - }, - "conflict": { - "symfony/console": "<4.4" - }, - "require-dev": { - "ext-iconv": "*", - "symfony/console": "^4.4|^5.0|^6.0", - "symfony/http-kernel": "^4.4|^5.0|^6.0", - "symfony/process": "^4.4|^5.0|^6.0", - "symfony/uid": "^5.1|^6.0", - "twig/twig": "^2.13|^3.0.4" - }, - "suggest": { - "ext-iconv": "To convert non-UTF-8 strings to UTF-8 (or symfony/polyfill-iconv in case ext-iconv cannot be used).", - "ext-intl": "To show region name in time zone dump", - "symfony/console": "To use the ServerDumpCommand and/or the bin/var-dump-server script" + "php": ">=7.2" }, - "bin": [ - "Resources/bin/var-dump-server" - ], "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, "autoload": { "files": [ - "Resources/functions/dump.php" + "bootstrap.php" ], "psr-4": { - "Symfony\\Component\\VarDumper\\": "" + "Symfony\\Polyfill\\Php80\\": "" }, - "exclude-from-classmap": [ - "/Tests/" + "classmap": [ + "Resources/stubs" ] }, "notification-url": "https://packagist.org/downloads/", @@ -2757,6 +1005,10 @@ "MIT" ], "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, { "name": "Nicolas Grekas", "email": "p@tchwork.com" @@ -2766,14 +1018,16 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Provides mechanisms for walking through any arbitrary PHP variable", + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", "homepage": "https://symfony.com", "keywords": [ - "debug", - "dump" + "compatibility", + "polyfill", + "portable", + "shim" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v5.4.43" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.31.0" }, "funding": [ { @@ -2789,146 +1043,7 @@ "type": "tidelift" } ], - "time": "2024-08-30T16:01:46+00:00" - }, - { - "name": "thecodingmachine/safe", - "version": "v1.3.3", - "source": { - "type": "git", - "url": "https://github.com/thecodingmachine/safe.git", - "reference": "a8ab0876305a4cdaef31b2350fcb9811b5608dbc" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/thecodingmachine/safe/zipball/a8ab0876305a4cdaef31b2350fcb9811b5608dbc", - "reference": "a8ab0876305a4cdaef31b2350fcb9811b5608dbc", - "shasum": "" - }, - "require": { - "php": ">=7.2" - }, - "require-dev": { - "phpstan/phpstan": "^0.12", - "squizlabs/php_codesniffer": "^3.2", - "thecodingmachine/phpstan-strict-rules": "^0.12" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "0.1-dev" - } - }, - "autoload": { - "files": [ - "deprecated/apc.php", - "deprecated/libevent.php", - "deprecated/mssql.php", - "deprecated/stats.php", - "lib/special_cases.php", - "generated/apache.php", - "generated/apcu.php", - "generated/array.php", - "generated/bzip2.php", - "generated/calendar.php", - "generated/classobj.php", - "generated/com.php", - "generated/cubrid.php", - "generated/curl.php", - "generated/datetime.php", - "generated/dir.php", - "generated/eio.php", - "generated/errorfunc.php", - "generated/exec.php", - "generated/fileinfo.php", - "generated/filesystem.php", - "generated/filter.php", - "generated/fpm.php", - "generated/ftp.php", - "generated/funchand.php", - "generated/gmp.php", - "generated/gnupg.php", - "generated/hash.php", - "generated/ibase.php", - "generated/ibmDb2.php", - "generated/iconv.php", - "generated/image.php", - "generated/imap.php", - "generated/info.php", - "generated/ingres-ii.php", - "generated/inotify.php", - "generated/json.php", - "generated/ldap.php", - "generated/libxml.php", - "generated/lzf.php", - "generated/mailparse.php", - "generated/mbstring.php", - "generated/misc.php", - "generated/msql.php", - "generated/mysql.php", - "generated/mysqli.php", - "generated/mysqlndMs.php", - "generated/mysqlndQc.php", - "generated/network.php", - "generated/oci8.php", - "generated/opcache.php", - "generated/openssl.php", - "generated/outcontrol.php", - "generated/password.php", - "generated/pcntl.php", - "generated/pcre.php", - "generated/pdf.php", - "generated/pgsql.php", - "generated/posix.php", - "generated/ps.php", - "generated/pspell.php", - "generated/readline.php", - "generated/rpminfo.php", - "generated/rrd.php", - "generated/sem.php", - "generated/session.php", - "generated/shmop.php", - "generated/simplexml.php", - "generated/sockets.php", - "generated/sodium.php", - "generated/solr.php", - "generated/spl.php", - "generated/sqlsrv.php", - "generated/ssdeep.php", - "generated/ssh2.php", - "generated/stream.php", - "generated/strings.php", - "generated/swoole.php", - "generated/uodbc.php", - "generated/uopz.php", - "generated/url.php", - "generated/var.php", - "generated/xdiff.php", - "generated/xml.php", - "generated/xmlrpc.php", - "generated/yaml.php", - "generated/yaz.php", - "generated/zip.php", - "generated/zlib.php" - ], - "psr-4": { - "Safe\\": [ - "lib/", - "deprecated/", - "generated/" - ] - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "PHP core functions that throw exceptions instead of returning FALSE on error", - "support": { - "issues": "https://github.com/thecodingmachine/safe/issues", - "source": "https://github.com/thecodingmachine/safe/tree/v1.3.3" - }, - "time": "2020-10-28T17:51:34+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "vlucas/phpdotenv", @@ -3013,183 +1128,6 @@ } ], "time": "2024-07-20T21:52:34+00:00" - }, - { - "name": "web-token/jwt-framework", - "version": "v2.2.11", - "source": { - "type": "git", - "url": "https://github.com/web-token/jwt-framework.git", - "reference": "643cced197e32471418bd89e7a44b69fd04eb9de" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/web-token/jwt-framework/zipball/643cced197e32471418bd89e7a44b69fd04eb9de", - "reference": "643cced197e32471418bd89e7a44b69fd04eb9de", - "shasum": "" - }, - "require": { - "brick/math": "^0.8.17|^0.9", - "ext-json": "*", - "ext-mbstring": "*", - "ext-openssl": "*", - "ext-sodium": "*", - "fgrosse/phpasn1": "^2.0", - "php": ">=7.2", - "psr/event-dispatcher": "^1.0", - "psr/http-client": "^1.0", - "psr/http-factory": "^1.0", - "spomky-labs/aes-key-wrap": "^5.0|^6.0", - "spomky-labs/base64url": "^1.0|^2.0", - "symfony/config": "^4.2|^5.0", - "symfony/console": "^4.2|^5.0", - "symfony/dependency-injection": "^4.2|^5.0", - "symfony/event-dispatcher": "^4.2|^5.0", - "symfony/http-kernel": "^4.2|^5.0", - "symfony/polyfill-mbstring": "^1.12" - }, - "conflict": { - "spomky-labs/jose": "*" - }, - "replace": { - "web-token/encryption-pack": "self.version", - "web-token/jwt-bundle": "self.version", - "web-token/jwt-checker": "self.version", - "web-token/jwt-console": "self.version", - "web-token/jwt-core": "self.version", - "web-token/jwt-easy": "self.version", - "web-token/jwt-encryption": "self.version", - "web-token/jwt-encryption-algorithm-aescbc": "self.version", - "web-token/jwt-encryption-algorithm-aesgcm": "self.version", - "web-token/jwt-encryption-algorithm-aesgcmkw": "self.version", - "web-token/jwt-encryption-algorithm-aeskw": "self.version", - "web-token/jwt-encryption-algorithm-dir": "self.version", - "web-token/jwt-encryption-algorithm-ecdh-es": "self.version", - "web-token/jwt-encryption-algorithm-experimental": "self.version", - "web-token/jwt-encryption-algorithm-pbes2": "self.version", - "web-token/jwt-encryption-algorithm-rsa": "self.version", - "web-token/jwt-key-mgmt": "self.version", - "web-token/jwt-nested-token": "self.version", - "web-token/jwt-signature": "self.version", - "web-token/jwt-signature-algorithm-ecdsa": "self.version", - "web-token/jwt-signature-algorithm-eddsa": "self.version", - "web-token/jwt-signature-algorithm-experimental": "self.version", - "web-token/jwt-signature-algorithm-hmac": "self.version", - "web-token/jwt-signature-algorithm-none": "self.version", - "web-token/jwt-signature-algorithm-rsa": "self.version", - "web-token/jwt-util-ecc": "self.version", - "web-token/signature-pack": "self.version" - }, - "require-dev": { - "bjeavons/zxcvbn-php": "^1.0", - "blackfire/php-sdk": "^1.14", - "ext-curl": "*", - "ext-gmp": "*", - "friendsofphp/php-cs-fixer": "^2.16", - "infection/infection": "^0.15|^0.16|^0.17|^0.18|^0.19|^0.20", - "matthiasnoback/symfony-config-test": "^3.1|^4.0", - "nyholm/psr7": "^1.3", - "php-coveralls/php-coveralls": "^2.0", - "php-http/mock-client": "^1.0", - "phpstan/phpstan": "^0.12", - "phpstan/phpstan-deprecation-rules": "^0.12", - "phpstan/phpstan-phpunit": "^0.12", - "phpstan/phpstan-strict-rules": "^0.12", - "phpunit/phpunit": "^8.0|^9.0", - "symfony/browser-kit": "^4.2|^5.0", - "symfony/finder": "^4.2|^5.0", - "symfony/framework-bundle": "^4.2|^5.0", - "symfony/http-client": "^5.2", - "symfony/phpunit-bridge": "^4.2|^5.0", - "symfony/serializer": "^4.2|^5.0", - "symfony/var-dumper": "^4.2|^5.0" - }, - "suggest": { - "bjeavons/zxcvbn-php": "Adds key quality check for oct keys.", - "ext-sodium": "Sodium is required for OKP key creation, EdDSA signature algorithm and ECDH-ES key encryption with OKP keys", - "php-http/httplug": "To enable JKU/X5U support.", - "php-http/httplug-bundle": "To enable JKU/X5U support.", - "php-http/message-factory": "To enable JKU/X5U support.", - "symfony/serializer": "Use the Symfony serializer to serialize/unserialize JWS and JWE tokens.", - "symfony/var-dumper": "Used to show data on the debug toolbar." - }, - "type": "symfony-bundle", - "autoload": { - "psr-4": { - "Jose\\": "src/", - "Jose\\Component\\Core\\Util\\Ecc\\": [ - "src/Ecc" - ], - "Jose\\Component\\Signature\\Algorithm\\": [ - "src/SignatureAlgorithm/ECDSA", - "src/SignatureAlgorithm/EdDSA", - "src/SignatureAlgorithm/HMAC", - "src/SignatureAlgorithm/None", - "src/SignatureAlgorithm/RSA", - "src/SignatureAlgorithm/Experimental" - ], - "Jose\\Component\\Encryption\\Algorithm\\": [ - "src/EncryptionAlgorithm/Experimental" - ], - "Jose\\Component\\Encryption\\Algorithm\\KeyEncryption\\": [ - "src/EncryptionAlgorithm/KeyEncryption/AESGCMKW", - "src/EncryptionAlgorithm/KeyEncryption/AESKW", - "src/EncryptionAlgorithm/KeyEncryption/Direct", - "src/EncryptionAlgorithm/KeyEncryption/ECDHES", - "src/EncryptionAlgorithm/KeyEncryption/PBES2", - "src/EncryptionAlgorithm/KeyEncryption/RSA" - ], - "Jose\\Component\\Encryption\\Algorithm\\ContentEncryption\\": [ - "src/EncryptionAlgorithm/ContentEncryption/AESGCM", - "src/EncryptionAlgorithm/ContentEncryption/AESCBC" - ] - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Florent Morselli", - "homepage": "https://github.com/Spomky" - }, - { - "name": "All contributors", - "homepage": "https://github.com/web-token/jwt-framework/contributors" - } - ], - "description": "JSON Object Signing and Encryption library for PHP and Symfony Bundle.", - "homepage": "https://github.com/web-token/jwt-framework", - "keywords": [ - "JOSE", - "JWE", - "JWK", - "JWKSet", - "JWS", - "Jot", - "RFC7515", - "RFC7516", - "RFC7517", - "RFC7518", - "RFC7519", - "RFC7520", - "bundle", - "jwa", - "jwt", - "symfony" - ], - "support": { - "issues": "https://github.com/web-token/jwt-framework/issues", - "source": "https://github.com/web-token/jwt-framework/tree/v2.2.11" - }, - "funding": [ - { - "url": "https://github.com/Spomky", - "type": "github" - } - ], - "time": "2021-06-25T15:59:52+00:00" } ], "packages-dev": [ @@ -3265,16 +1203,16 @@ }, { "name": "myclabs/deep-copy", - "version": "1.12.0", + "version": "1.12.1", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c" + "reference": "123267b2c49fbf30d78a7b2d333f6be754b94845" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c", - "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/123267b2c49fbf30d78a7b2d333f6be754b94845", + "reference": "123267b2c49fbf30d78a7b2d333f6be754b94845", "shasum": "" }, "require": { @@ -3313,7 +1251,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.12.0" + "source": "https://github.com/myclabs/DeepCopy/tree/1.12.1" }, "funding": [ { @@ -3321,20 +1259,20 @@ "type": "tidelift" } ], - "time": "2024-06-12T14:39:25+00:00" + "time": "2024-11-08T17:47:46+00:00" }, { "name": "nikic/php-parser", - "version": "v5.2.0", + "version": "v5.4.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "23c79fbbfb725fb92af9bcf41065c8e9a0d49ddb" + "reference": "447a020a1f875a434d62f2a401f53b82a396e494" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/23c79fbbfb725fb92af9bcf41065c8e9a0d49ddb", - "reference": "23c79fbbfb725fb92af9bcf41065c8e9a0d49ddb", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/447a020a1f875a434d62f2a401f53b82a396e494", + "reference": "447a020a1f875a434d62f2a401f53b82a396e494", "shasum": "" }, "require": { @@ -3377,9 +1315,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.2.0" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.4.0" }, - "time": "2024-09-15T16:40:33+00:00" + "time": "2024-12-30T11:07:19+00:00" }, { "name": "phar-io/manifest", @@ -3820,16 +1758,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.6.21", + "version": "9.6.22", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "de6abf3b6f8dd955fac3caad3af7a9504e8c2ffa" + "reference": "f80235cb4d3caa59ae09be3adf1ded27521d1a9c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/de6abf3b6f8dd955fac3caad3af7a9504e8c2ffa", - "reference": "de6abf3b6f8dd955fac3caad3af7a9504e8c2ffa", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/f80235cb4d3caa59ae09be3adf1ded27521d1a9c", + "reference": "f80235cb4d3caa59ae09be3adf1ded27521d1a9c", "shasum": "" }, "require": { @@ -3840,7 +1778,7 @@ "ext-mbstring": "*", "ext-xml": "*", "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.12.0", + "myclabs/deep-copy": "^1.12.1", "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=7.3", @@ -3903,7 +1841,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.21" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.22" }, "funding": [ { @@ -3919,7 +1857,7 @@ "type": "tidelift" } ], - "time": "2024-09-19T10:50:18+00:00" + "time": "2024-12-05T13:48:26+00:00" }, { "name": "sebastian/cli-parser", @@ -4886,16 +2824,16 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "3.10.3", + "version": "3.11.2", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", - "reference": "62d32998e820bddc40f99f8251958aed187a5c9c" + "reference": "1368f4a58c3c52114b86b1abe8f4098869cb0079" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/62d32998e820bddc40f99f8251958aed187a5c9c", - "reference": "62d32998e820bddc40f99f8251958aed187a5c9c", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/1368f4a58c3c52114b86b1abe8f4098869cb0079", + "reference": "1368f4a58c3c52114b86b1abe8f4098869cb0079", "shasum": "" }, "require": { @@ -4962,7 +2900,7 @@ "type": "open_collective" } ], - "time": "2024-09-18T10:38:58+00:00" + "time": "2024-12-11T16:04:26+00:00" }, { "name": "theseer/tokenizer", @@ -5021,12 +2959,6 @@ "version": "7.9.2.0", "alias": "7.9.3", "alias_normalized": "7.9.3.0" - }, - { - "package": "web-token/jwt-framework", - "version": "2.2.11.0", - "alias": "2.3.0", - "alias_normalized": "2.3.0.0" } ], "minimum-stability": "stable", diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..6520de3 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,8 @@ + + + + + src/tests/DescopeSDKTest.php + + + \ No newline at end of file diff --git a/sample/dashboard.php b/sample/dashboard.php index 527189e..f412ecb 100644 --- a/sample/dashboard.php +++ b/sample/dashboard.php @@ -1,5 +1,6 @@ client = new Client(); $this->projectId = $config['projectId']; $this->managementKey = $config['managementKey'] ?? ''; + if ($cache) { + $this->cache = $cache; + } elseif (extension_loaded('apcu') && ini_get('apc.enable_cli')) { + $this->cache = new APCuCache(); + } else { + $this->cache = new NullCache(); + error_log('APCu is not enabled. Falling back to NullCache. Caching is disabled.'); + } + EndpointsV2::setBaseUrl($config['projectId']); + } + + /** + * Gets the current JWKSet. Fetches a new one if not cached or if explicitly requested. + */ + public function getJWKSets(bool $forceRefresh = false): array + { + if (!$forceRefresh) { + $cachedJWKSets = $this->cache->get(self::JWKS_CACHE_KEY); + if ($cachedJWKSets) { + return $cachedJWKSets; + } + } - $this->jwkSets = $this->getJWKSets(); + $jwks = $this->fetchJWKSets(); + $this->cache->set(self::JWKS_CACHE_KEY, $jwks, 3600); // Cache for 1 hour + return $jwks; } /** - * Gets the current JWK KeySet that will be needed to validate the JWT + * Fetch the JWK KeySet from the Descope API. */ - private function getJWKSets() + private function fetchJWKSets(): array { try { $url = EndpointsV2::getPublicKeyPath() . '/' . $this->projectId; - - // Fetch JWK public key from Descope API - $res = $this->client->request('GET', $url); - $jwkSets = json_decode($res->getBody(), true); + $response = $this->client->request('GET', $url); + $jwkSets = json_decode($response->getBody(), true); + + if (!isset($jwkSets['keys']) || !is_array($jwkSets['keys'])) { + throw new \Exception('Invalid JWK response'); + } + return $jwkSets; - } catch (RequestException $re) { - return $re; + } catch (RequestException $e) { + throw new \Exception('Failed to fetch JWK KeySet: ' . $e->getMessage()); } } } diff --git a/src/SDK/DescopeSDK.php b/src/SDK/DescopeSDK.php index 67ac5a2..554ab77 100644 --- a/src/SDK/DescopeSDK.php +++ b/src/SDK/DescopeSDK.php @@ -12,6 +12,10 @@ use Descope\SDK\Auth\Management\User; use Descope\SDK\Auth\Management\Audit; use Descope\SDK\EndpointsV1; +use Descope\SDK\EndpointsV2; +use Descope\SDK\Exception\AuthException; +use Descope\SDK\Exception\ValidationException; + use Descope\SDK\Management\MgmtV1; class DescopeSDK @@ -45,6 +49,8 @@ public function __construct(array $config) MgmtV1::setBaseUrl($config['projectId']); } + $this->verifier = new Verifier($this->config, $this->api); + $this->password = new Password($this->api); $this->sso = new SSO($this->api); } @@ -56,16 +62,16 @@ public function __construct(array $config) * @return bool Verification result. * @throws AuthException */ - public function verify($sessionToken = null) + public function verify($sessionToken = null): bool { - $sessionToken = $sessionToken ?? $_COOKIE[EndpointsV1::SESSION_COOKIE_NAME_NAME] ?? null; + $sessionToken = $sessionToken ?? $_COOKIE[EndpointsV1::$SESSION_COOKIE_NAME] ?? null; if (!$sessionToken) { - throw new \InvalidArgumentException('Session token is required.'); + throw ValidationException::forMissingSessionToken(); } - $verifier = new Verifier($this->config); - return $verifier->verify($sessionToken); + $verifier = new Verifier($this->config, $this->api); + return $this->verifier->verify($sessionToken); } /** @@ -75,16 +81,26 @@ public function verify($sessionToken = null) * @return array The new session information. * @throws AuthException */ - public function refreshSession($refreshToken = null) + public function refreshSession($refreshToken = null): array { - $refreshToken = $refreshToken ?? $_COOKIE[EndpointsV1::REFRESH_COOKIE_NAME] ?? null; + $refreshToken = $refreshToken ?? $_COOKIE[EndpointsV1::$REFRESH_COOKIE_NAME] ?? null; - if (!$refreshToken) { - throw new \InvalidArgumentException('Refresh token is required.'); + if (empty($refreshToken)) { + throw ValidationException::forMissingRefreshToken(); } - $verifier = new Verifier($this->config); - return $verifier->refreshSession($refreshToken); + try { + return $this->api->doPost( + EndpointsV1::$REFRESH_TOKEN_PATH, + [], + false, + $refreshToken + ); + } catch (RequestException $e) { + $statusCode = $e->getResponse() ? $e->getResponse()->getStatusCode() : 'N/A'; + $responseBody = $e->getResponse() ? $e->getResponse()->getBody()->getContents() : 'No response body'; + throw new AuthException($statusCode, 'RequestException', $e->getMessage()); + } } /** @@ -95,17 +111,21 @@ public function refreshSession($refreshToken = null) * @return array The refreshed session information. * @throws AuthException */ - public function verifyAndRefreshSession($sessionToken = null, $refreshToken = null) + public function verifyAndRefreshSession($sessionToken = null, $refreshToken = null): array { - $sessionToken = $sessionToken ?? $_COOKIE[EndpointsV1::SESSION_COOKIE_NAME] ?? null; - $refreshToken = $refreshToken ?? $_COOKIE[EndpointsV1::REFRESH_COOKIE_NAME] ?? null; + $sessionToken = $sessionToken ?? $_COOKIE[EndpointsV1::$SESSION_COOKIE_NAME] ?? null; + $refreshToken = $refreshToken ?? $_COOKIE[EndpointsV1::$REFRESH_COOKIE_NAME] ?? null; - if (!$sessionToken || !$refreshToken) { - throw new \InvalidArgumentException('Session token and refresh token are required.'); + if (empty($sessionToken) || empty($refreshToken)) { + throw new ValidateException('Session or refresh token cannot be null or empty.'); + } + + try { + $this->verify($sessionToken); + return $this->refreshSession($refreshToken); + } catch (AuthException $e) { + return $this->refreshSession($refreshToken); } - - $verifier = new Verifier($this->config); - return $verifier->verifyAndRefreshSession($sessionToken, $refreshToken); } /** @@ -115,12 +135,12 @@ public function verifyAndRefreshSession($sessionToken = null, $refreshToken = nu * @return array The JWT claims. * @throws AuthException */ - public function getClaims($token = null) + public function getClaims($token = null): array { - $token = $token ?? $_COOKIE[EndpointsV1::SESSION_COOKIE_NAME] ?? null; + $token = $token ?? $_COOKIE[EndpointsV1::$SESSION_COOKIE_NAME] ?? null; if (!$token) { - throw new \InvalidArgumentException('Token is required.'); + throw ValidationException::forMissingSessionToken(); } $extractor = new Extractor($this->config); @@ -134,19 +154,25 @@ public function getClaims($token = null) * @return array The user details. * @throws AuthException */ - public function getUserDetails(string $refreshToken = null) + public function getUserDetails(string $refreshToken = null): array { - $refreshToken = $refreshToken ?? $_COOKIE[EndpointsV1::REFRESH_COOKIE_NAME] ?? null; + $refreshToken = $refreshToken ?? $_COOKIE[EndpointsV1::$REFRESH_COOKIE_NAME] ?? null; if (!$refreshToken) { - throw new \InvalidArgumentException('Refresh token is required.'); + throw ValidationException::forMissingRefreshToken(); } - return $this->api->doGet( - EndpointsV1::$ME_PATH, - false, - $refreshToken - ); + try { + return $this->api->doGet( + EndpointsV1::$ME_PATH, + false, + $refreshToken + ); + } catch (RequestException $e) { + $statusCode = $e->getResponse() ? $e->getResponse()->getStatusCode() : 'N/A'; + $responseBody = $e->getResponse() ? $e->getResponse()->getBody()->getContents() : 'No response body'; + throw new AuthException($statusCode, 'RequestException', $e->getMessage()); + } } /** @@ -158,18 +184,25 @@ public function getUserDetails(string $refreshToken = null) */ public function logout(string $refreshToken = null): void { - $refreshToken = $refreshToken ?? $_COOKIE[EndpointsV1::REFRESH_COOKIE_NAME] ?? null; + $refreshToken = $refreshToken ?? $_COOKIE[EndpointsV1::$REFRESH_COOKIE_NAME] ?? null; if (!$refreshToken) { - throw new \InvalidArgumentException('Refresh token is required.'); + throw ValidationException::forMissingRefreshToken(); } - $this->api->doPost( - EndpointsV1::$LOGOUT_PATH, - [], - false, - $refreshToken - ); + try { + $this->api->doPost( + EndpointsV1::$LOGOUT_PATH, + [], + false, + $refreshToken + ); + return; + } catch (RequestException $e) { + $statusCode = $e->getResponse() ? $e->getResponse()->getStatusCode() : 'N/A'; + $responseBody = $e->getResponse() ? $e->getResponse()->getBody()->getContents() : 'No response body'; + throw new AuthException($statusCode, 'RequestException', $e->getMessage()); + } } /** @@ -181,18 +214,25 @@ public function logout(string $refreshToken = null): void */ public function logoutAll(string $refreshToken = null): void { - $refreshToken = $refreshToken ?? $_COOKIE[EndpointsV1::REFRESH_COOKIE_NAME] ?? null; + $refreshToken = $refreshToken ?? $_COOKIE[EndpointsV1::$REFRESH_COOKIE_NAME] ?? null; if (!$refreshToken) { - throw new \InvalidArgumentException('Refresh token is required.'); + throw ValidationException::forMissingRefreshToken(); } - $this->api->doPost( - EndpointsV1::LOGOUT_ALL_PATH, - [], - false, - $refreshToken - ); + try { + $this->api->doPost( + EndpointsV1::$LOGOUT_ALL_PATH, + [], + false, + $refreshToken + ); + return; + } catch (RequestException $e) { + $statusCode = $e->getResponse() ? $e->getResponse()->getStatusCode() : 'N/A'; + $responseBody = $e->getResponse() ? $e->getResponse()->getBody()->getContents() : 'No response body'; + throw new AuthException($statusCode, 'RequestException', $e->getMessage()); + } } /** diff --git a/src/SDK/Exception/ValidationException.php b/src/SDK/Exception/ValidationException.php new file mode 100644 index 0000000..c9ec9a4 --- /dev/null +++ b/src/SDK/Exception/ValidationException.php @@ -0,0 +1,57 @@ +hash = $hash; + } + + /** + * Convert object data to an array format. + * + * @return array The password data as an associative array. + */ + public function toArray(): array + { + return [ + 'md5' => [ + 'hash' => $this->hash, + ], + ]; + } +} diff --git a/src/SDK/Management/Password/UserPasswordPHPass.php b/src/SDK/Management/Password/UserPasswordPHPass.php index 33a4257..ceb1098 100644 --- a/src/SDK/Management/Password/UserPasswordPHPass.php +++ b/src/SDK/Management/Password/UserPasswordPHPass.php @@ -49,4 +49,4 @@ public function toArray(): array ], ]; } -} \ No newline at end of file +} diff --git a/src/SDK/Management/Password/UserPasswordPbkdf2.php b/src/SDK/Management/Password/UserPasswordPbkdf2.php index 1de37dd..cec8bbb 100644 --- a/src/SDK/Management/Password/UserPasswordPbkdf2.php +++ b/src/SDK/Management/Password/UserPasswordPbkdf2.php @@ -53,4 +53,4 @@ public function toArray(): array ], ]; } -} \ No newline at end of file +} diff --git a/src/SDK/Management/Role.php b/src/SDK/Management/Role.php new file mode 100644 index 0000000..5e822be --- /dev/null +++ b/src/SDK/Management/Role.php @@ -0,0 +1,96 @@ +api = $api; + } + + /** + * Validates tenant permissions for a JWT response. + * + * @param array $jwtResponse JWT response data. + * @param string $tenant Tenant ID. + * @param array $permissions Permissions to validate. + * @return bool True if tenant permissions are valid, false otherwise. + * @throws AuthException If JWT response is invalid. + */ + public function validateTenantPermissions(array $jwtResponse, array $permissions, ?string $tenant = null): bool + { + $tenant = $tenant ?? ''; + + if (!is_array($permissions)) { + $permissions = [$permissions]; + } + + if (!is_array($jwtResponse)) { + throw new AuthException(400, 'Invalid JWT response hash'); + } + + $grantedPermissions = $jwtResponse['permissions'] ?? []; + if (!empty($tenant)) { + if (empty($jwtResponse['tenants'][$tenant])) { + return false; + } + $grantedPermissions = $jwtResponse['tenants'][$tenant]['permissions'] ?? []; + } + + return empty(array_diff($permissions, $grantedPermissions)); + } + + /** + * Validates roles for a JWT response. + * + * @param array $jwtResponse JWT response data. + * @param array $roles Roles to validate. + * @return bool True if roles are valid, false otherwise. + */ + public function validateRoles(array $jwtResponse, array $roles): bool + { + return $this->validateTenantRoles($jwtResponse, '', $roles); + } + + /** + * Validates tenant roles for a JWT response. + * + * @param array $jwtResponse JWT response data. + * @param string $tenant Tenant ID. + * @param array $roles Roles to validate. + * @return bool True if tenant roles are valid, false otherwise. + * @throws AuthException If JWT response is invalid. + */ + public function validateTenantRoles(array $jwtResponse, string $tenant, array $roles): bool + { + if (!is_array($roles)) { + $roles = [$roles]; + } + + if (!is_array($jwtResponse)) { + throw new AuthException(400, 'Invalid JWT response hash'); + } + + $grantedRoles = $jwtResponse['roles'] ?? []; + if (!empty($tenant)) { + if (empty($jwtResponse['tenants'][$tenant])) { + return false; + } + $grantedRoles = $jwtResponse['tenants'][$tenant]['roles'] ?? []; + } + + return empty(array_diff($roles, $grantedRoles)); + } +} diff --git a/src/SDK/Token/Extractor.php b/src/SDK/Token/Extractor.php index fcee856..4374f7d 100644 --- a/src/SDK/Token/Extractor.php +++ b/src/SDK/Token/Extractor.php @@ -5,13 +5,6 @@ use GuzzleHttp\Client; use GuzzleHttp\Exception\RequestException; use GuzzleHttp\Psr7\Request; -use Jose\Component\Core\Util\JsonConverter; -use Jose\Component\Core\AlgorithmManager; -use Jose\Component\Core\JWKSet; -use Jose\Component\Signature\Algorithm\RS256; -use Jose\Component\Signature\JWSVerifier; -use Jose\Component\Signature\Serializer\CompactSerializer; -use Jose\Component\Signature\Serializer\JWSSerializerManager; use Descope\SDK\Exception\TokenException; use Descope\SDK\Configuration\SDKConfig; @@ -32,68 +25,201 @@ public function __construct($config) /** * Return an array representing the Token's claims. * - * @return array|int|string> + * @return array */ - public function getClaims($sessionToken): array + public function getClaims(string $sessionToken): array { - $jws = $this->parseToken($sessionToken); - return json_decode($jws->getPayload(), true) ?? []; + $parts = $this->parseToken($sessionToken); + return $parts['payload'] ?? []; } /** - * Return all user information using /auth/me API endpoint. + * Parse and validate the JWT token. * - * @return json + * @throws TokenException if validation fails. */ - public function getUserDetails($refreshToken) + public function parseToken(string $sessionToken): array { - $client = $this->config->client; - $url = EndpointsV1::$ME_PATH; - $header = 'Bearer ' . $this->config->projectId . ":" . $refreshToken; - - try { - $response = $client->get($url, ['headers' => ['Authorization' => $header]]); - return json_decode($response->getBody(), true); - } catch (RequestException $e) { - throw new TokenException('Failed to retrieve user details: ' . $e->getMessage()); + $parts = explode('.', $sessionToken); + if (count($parts) !== 3) { + throw new TokenException('Invalid JWT format'); } + + $header = $this->decodeJWTPart($parts[0]); + $payload = $this->decodeJWTPart($parts[1]); + $signature = $this->base64UrlDecode($parts[2]); + + if (!isset($header['alg']) || $header['alg'] !== 'RS256') { + throw new TokenException('Unsupported algorithm. Only RS256 is supported.'); + } + + return [ + 'raw' => [ + 'header' => $parts[0], + 'payload' => $parts[1], + 'signature' => $parts[2] + ], + 'header' => $header, + 'payload' => $payload, + 'signature' => $signature + ]; } /** - * Parse a JWT string, returning the JWS Object with all of the claims if valid signature. - * - * @throws TokenException if signature verification fails or parsing fails. + * Validate a JWT using the provided JWK Set. */ - public function parseToken($sessionToken) + public function validateJWT(string $sessionToken): array { - try { - $jwkSets = $this->config->jwkSets; - $jwkSet = JWKSet::createFromKeyData($jwkSets); - - $jwsVerifier = new JWSVerifier( - new AlgorithmManager( - [ - new RS256(), - ] - ) - ); + $useRefreshedKey = false; + do { + try { + $jwkSet = $this->config->getJWKSets($useRefreshedKey); + $jwt = $this->parseToken($sessionToken); - $serializerManager = new JWSSerializerManager( - [ - new CompactSerializer(), - ] - ); - - $jws = $serializerManager->unserialize($sessionToken); - - $isVerified = $jwsVerifier->verifyWithKeySet($jws, $jwkSet, 0); - if ($isVerified) { - return $jws; - } else { - throw new TokenException(TokenException::MSG_SIGNATURE_INVALID); + if (!isset($jwt['header']['kid'])) { + throw new TokenException('Missing key ID in JWT header'); + } + + $matchingKey = null; + foreach ($jwkSet['keys'] as $key) { + if ($key['kid'] === $jwt['header']['kid']) { + $matchingKey = $key; + break; + } + } + + if (!$matchingKey) { + throw new TokenException('No matching key found in JWKS'); + } + + $publicKeyPEM = $this->convertJWKToPEM($matchingKey); + $signatureValid = $this->verifySignature( + $jwt['raw']['header'] . '.' . $jwt['raw']['payload'], + $jwt['signature'], + $publicKeyPEM + ); + + if (!$signatureValid) { + throw new TokenException('Invalid signature'); + } + + return $jwt['payload']; + } catch (TokenException $e) { + if ($useRefreshedKey) { + throw new TokenException('JWT validation failed after retry: ' . $e->getMessage()); + } + $useRefreshedKey = true; } - } catch (Exception $e) { - throw new TokenException(TokenException::MSG_COULD_NOT_PARSE); + } while ($useRefreshedKey); + + throw new TokenException('JWT validation failed'); + } + + /** + * Verify JWT signature. + */ + private function verifySignature(string $signedData, string $signature, string $publicKeyPEM): bool + { + $publicKey = openssl_pkey_get_public($publicKeyPEM); + if (!$publicKey) { + throw new TokenException('Invalid public key'); + } + + $result = openssl_verify( + $signedData, + $signature, + $publicKey, + OPENSSL_ALGO_SHA256 + ); + + return $result === 1; + } + + /** + * Convert JWK to PEM format. + */ + private function convertJWKToPEM(array $jwk): string + { + if (!isset($jwk['kty']) || $jwk['kty'] !== 'RSA') { + throw new TokenException('Invalid key type. Only RSA is supported.'); + } + + $modulus = $this->base64UrlDecode($jwk['n']); + $exponent = $this->base64UrlDecode($jwk['e']); + + // Remove leading null bytes from modulus + $modulus = ltrim($modulus, "\x00"); + + // Construct RSA public key in ASN.1 format + $modulus = pack('Ca*a*', 0x02, $this->encodeLength(strlen($modulus)), $modulus); + $exponent = pack('Ca*a*', 0x02, $this->encodeLength(strlen($exponent)), $exponent); + + $rsaPublicKey = pack('Ca*a*', 0x30, $this->encodeLength(strlen($modulus . $exponent)), $modulus . $exponent); + + // Add RSA public key algorithm identifier + $algorithmIdentifier = pack('H*', '300d06092a864886f70d0101010500'); + $bitString = pack('Ca*', 0x03, $this->encodeLength(strlen($rsaPublicKey) + 1) . "\x00" . $rsaPublicKey); + + $der = pack( + 'Ca*a*', + 0x30, + $this->encodeLength(strlen($algorithmIdentifier . $bitString)), + $algorithmIdentifier . $bitString + ); + + return sprintf( + "-----BEGIN PUBLIC KEY-----\n%s-----END PUBLIC KEY-----\n", + chunk_split(base64_encode($der), 64, "\n") + ); + } + + + /** + * Helper to encode the length in DER format. + */ + private function encodeLength(int $length): string + { + if ($length < 128) { + return chr($length); + } + + $temp = $length; + $bytes = ''; + while ($temp > 0) { + $bytes = chr($temp & 0xFF) . $bytes; + $temp >>= 8; + } + return chr(0x80 | strlen($bytes)) . $bytes; + } + + /** + * Decodes a Base64Url-encoded string. + */ + private function base64UrlDecode(string $data): string + { + $padded = str_pad( + strtr($data, '-_', '+/'), + strlen($data) + (4 - strlen($data) % 4) % 4, + '=' + ); + + $decoded = base64_decode($padded, true); + if ($decoded === false) { + throw new TokenException('Invalid base64url encoding'); + } + + return $decoded; + } + + private function decodeJWTPart(string $data): array + { + $decoded = $this->base64UrlDecode($data); + $result = json_decode($decoded, true); + + if (JSON_ERROR_NONE !== json_last_error()) { + throw new TokenException('Invalid JWT part encoding'); } + + return $result; } } diff --git a/src/SDK/Token/Verifier.php b/src/SDK/Token/Verifier.php index addc1e8..bafb996 100644 --- a/src/SDK/Token/Verifier.php +++ b/src/SDK/Token/Verifier.php @@ -4,30 +4,28 @@ use GuzzleHttp\Client; use GuzzleHttp\Exception\RequestException; +use Descope\SDK\Exception\TokenException; use GuzzleHttp\Psr7\Request; -use Jose\Component\Core\Util\JsonConverter; -use Jose\Component\Core\AlgorithmManager; -use Jose\Component\Core\JWKSet; -use Jose\Component\Signature\Algorithm\RS256; -use Jose\Component\Signature\JWSVerifier; -use Jose\Component\Signature\Serializer\CompactSerializer; -use Jose\Component\Signature\Serializer\JWSSerializerManager; use Descope\SDK\Token\Extractor; use Descope\SDK\Configuration\SDKConfig; use Descope\SDK\EndpointsV1; +use Descope\SDK\API; final class Verifier { private SDKConfig $config; + private API $api; /** * Constructor for Verifier class. * * @param SDKConfig $config Base configuration options for the SDK. */ - public function __construct($config) + public function __construct(SDKConfig $config, API $api) { $this->config = $config; + $this->api = $api; + $this->extractor = new Extractor($this->config); } /** @@ -37,149 +35,44 @@ public function __construct($config) * @return boolean Token signature is valid and not expired. * @throws AuthException If the refresh operation fails. */ - public function verify($sessionToken, ?string $audience = null) + public function verify(string $sessionToken, ?string $audience = null): bool { try { - $extractor = new Extractor($this->config); - $jws = $extractor->parseToken($sessionToken); - - // If JWT signature is valid - if (isset($jws)) { - $payload = json_decode($jws->getPayload()); + // First validate the token signature + $validatedToken = $this->extractor->validateJWT($sessionToken); + if (!$validatedToken) { + throw new TokenException('Invalid token signature'); + } + + // Verify expiration + if (isset($validatedToken['exp']) && time() > $validatedToken['exp']) { + throw new TokenException('Token has expired'); + } - // Check to make sure JWT is not expired - if (isset($payload->exp) && time() < $payload->exp) { - if ($audience && (!isset($payload->aud) || $payload->aud !== $audience)) { - return false; + // Verify audience if provided + if ($audience !== null) { + if (!isset($validatedToken['aud'])) { + throw new TokenException('Token is missing audience claim'); + } + + // Handle both string and array audience claims + $tokenAudience = $validatedToken['aud']; + if (is_array($tokenAudience)) { + if (!in_array($audience, $tokenAudience, true)) { + throw new TokenException('Token audience does not match expected value'); + } + } else { + if ($tokenAudience !== $audience) { + throw new TokenException('Token audience does not match expected value'); } - return true; } } - return false; - } catch (TokenException $te) { - throw TokenException::MSG_SIGNATURE_INVALID; - } - } - - /** - * Refreshes the session token, with the provided refresh token. - * - * @param string $refreshToken The refresh token. - * @return array The refreshed JWT response. - * @throws AuthException If the refresh operation fails. - */ - public function refreshSession(string $refreshToken): array - { - $this->validateRefreshTokenNotNil($refreshToken); - $this->validateToken($refreshToken); - $uri = EndpointsV1::$REFRESH_TOKEN_PATH; - $response = $this->doPost($uri, [], $refreshToken); - return $this->generateJwtResponse($response, $refreshToken); - } - - /** - * Verifies the session token, and automatically refreshes when expired. - * - * @param string $sessionToken The session token. - * @param string $refreshToken The refresh token. - * @return array The JWT response. - * @throws AuthException If both tokens are missing or verification fails. - */ - public function verifyAndRefreshSession(string $sessionToken, string $refreshToken): array - { - if (empty($sessionToken)) { - throw new AuthException(400, 'Session token is missing'); - } - - try { - $this->validateToken($sessionToken); - return $this->generateJwtResponse($sessionToken, $refreshToken); - } catch (AuthException $e) { - return $this->refreshSession($refreshToken); - } - } - - /** - * Validates permissions for a JWT response. - * - * @param array $jwtResponse JWT response data. - * @param array $permissions Permissions to validate. - * @return bool True if permissions are valid, false otherwise. - */ - public function validatePermissions(array $jwtResponse, array $permissions): bool - { - return $this->validateTenantPermissions($jwtResponse, '', $permissions); - } - - /** - * Validates tenant permissions for a JWT response. - * - * @param array $jwtResponse JWT response data. - * @param string $tenant Tenant ID. - * @param array $permissions Permissions to validate. - * @return bool True if tenant permissions are valid, false otherwise. - * @throws AuthException If JWT response is invalid. - */ - public function validateTenantPermissions(array $jwtResponse, string $tenant, array $permissions): bool - { - if (!is_array($permissions)) { - $permissions = [$permissions]; - } - - if (!is_array($jwtResponse)) { - throw new AuthException(400, 'Invalid JWT response hash'); - } - - $grantedPermissions = $jwtResponse['permissions'] ?? []; - if (!empty($tenant)) { - if (empty($jwtResponse['tenants'][$tenant])) { - return false; - } - $grantedPermissions = $jwtResponse['tenants'][$tenant]['permissions'] ?? []; - } - - return empty(array_diff($permissions, $grantedPermissions)); - } - - /** - * Validates roles for a JWT response. - * - * @param array $jwtResponse JWT response data. - * @param array $roles Roles to validate. - * @return bool True if roles are valid, false otherwise. - */ - public function validateRoles(array $jwtResponse, array $roles): bool - { - return $this->validateTenantRoles($jwtResponse, '', $roles); - } - /** - * Validates tenant roles for a JWT response. - * - * @param array $jwtResponse JWT response data. - * @param string $tenant Tenant ID. - * @param array $roles Roles to validate. - * @return bool True if tenant roles are valid, false otherwise. - * @throws AuthException If JWT response is invalid. - */ - public function validateTenantRoles(array $jwtResponse, string $tenant, array $roles): bool - { - if (!is_array($roles)) { - $roles = [$roles]; - } - - if (!is_array($jwtResponse)) { - throw new AuthException(400, 'Invalid JWT response hash'); + // All validations passed + return true; + } catch (TokenException $e) { + // You might want to throw a specific error or return false depending on your needs + throw new TokenException('Token validation failed: ' . $e->getMessage()); } - - $grantedRoles = $jwtResponse['roles'] ?? []; - if (!empty($tenant)) { - if (empty($jwtResponse['tenants'][$tenant])) { - return false; - } - $grantedRoles = $jwtResponse['tenants'][$tenant]['roles'] ?? []; - } - - return empty(array_diff($roles, $grantedRoles)); } } diff --git a/src/tests/DescopeSDKTest.php b/src/tests/DescopeSDKTest.php index 3325b50..9b8d7c7 100644 --- a/src/tests/DescopeSDKTest.php +++ b/src/tests/DescopeSDKTest.php @@ -4,42 +4,42 @@ use PHPUnit\Framework\TestCase; use Descope\SDK\DescopeSDK; +use Descope\SDK\API; +use Descope\SDK\Auth\Password; +use Descope\SDK\Auth\SSO; +use Descope\SDK\Management\Management; +use Descope\SDK\Exception\ValidationException; final class DescopeSDKTest extends TestCase { - public $descopeSDK; + private $config; + private $sdk; - public function setUp(): void + protected function setUp(): void { - // $descopeSDK = new DescopeSDK( - // [ - // 'projectId' => "", - // 'managementKey' => "" // This can be optional - // ] - // ); + $this->config = [ + 'projectId' => 'test_project_id', + 'managementKey' => 'test_management_key' + ]; + $this->sdk = new DescopeSDK($this->config); } - public function testVerify(): void + public function testConstructorInitializesComponents() { - $token = '...'; - // $this->assertTrue($this->descopeSDK->verify($token)); + $this->assertInstanceOf(Password::class, $this->sdk->password()); + $this->assertInstanceOf(SSO::class, $this->sdk->sso()); + $this->assertInstanceOf(Management::class, $this->sdk->management()); } - public function getClaims(): void + public function testVerifyThrowsExceptionWithoutToken() { - $token = '...'; - // $this->assertNotEmpty($this->descopeSDK->getClaims($token)); + $this->expectException(ValidationException::class); + $this->sdk->verify(null); } - public function testUserDetails(): void + public function testRefreshSessionThrowsExceptionWithoutToken() { - $refresh_token = '...'; - // $this->assertIsArray($this->descopeSDK->getUser($refresh_token)); - } - - public function testPassword(): void - { - // $result = $this->descopeSDK->password->signUp("example@descope.com", "Password123!", []); - // $this->assertIsArray($this->descopeSDK->password->signUp("example@descope.com", "Password123!", [])); + $this->expectException(ValidationException::class); + $this->sdk->refreshSession(null); } } diff --git a/src/tests/Management/UserPwdTest.php b/src/tests/Management/UserPwdTest.php index ec6eeb6..c5832b9 100644 --- a/src/tests/Management/UserPwdTest.php +++ b/src/tests/Management/UserPwdTest.php @@ -8,6 +8,7 @@ use Descope\SDK\Management\Password\UserPasswordFirebase; use Descope\SDK\Management\Password\UserPasswordPbkdf2; use Descope\SDK\Management\Password\UserPasswordDjango; +use Descope\SDK\Management\Password\UserPasswordMD5; class UserPwdTest extends TestCase { @@ -81,6 +82,19 @@ public function testUserPasswordDjango() $this->assertEquals($expectedArray, $userPasswordDjango->toArray()); } + public function testUserPasswordMD5() + { + $md5Hash = '87f77988ccb5aa917c93201ba314fcd4'; + $userPasswordMD5 = new UserPasswordMD5($md5Hash); + $expectedArray = [ + 'md5' => [ + 'hash' => $md5Hash, + ], + ]; + + $this->assertEquals($expectedArray, $userPasswordMD5->toArray()); + } + public function testUserPasswordWithCleartext() { $cleartextPassword = 'mypassword'; diff --git a/src/tests/Management/UserTest.php b/src/tests/Management/UserTest.php index c3eb1eb..5d98fb9 100644 --- a/src/tests/Management/UserTest.php +++ b/src/tests/Management/UserTest.php @@ -6,9 +6,6 @@ use Descope\SDK\DescopeSDK; use Descope\SDK\Management\Password\UserPassword; use Descope\SDK\Management\Password\UserPasswordBcrypt; -use Descope\SDK\Management\Password\UserPasswordFirebase; -use Descope\SDK\Management\Password\UserPasswordPbkdf2; -use Descope\SDK\Management\Password\UserPasswordDjango; use Descope\SDK\Management\User; use Descope\SDK\Management\AssociatedTenant; use Descope\SDK\Management\UserObj;