diff --git a/README-internal.md b/README-internal.md index 0301e0f..d8049c4 100644 --- a/README-internal.md +++ b/README-internal.md @@ -2,7 +2,7 @@ ## Project Requirements -- PHP 7.4 or higher +- PHP 8.2 or higher (CI runs the tests on 8.2, 8.3, 8.4 and 8.5) - Composer ## Setup Instructions @@ -55,5 +55,31 @@ (Requires Xdebug or PCOV extension) +## Releasing + +Releases run from the **Release QueueToken PHP** pipeline (`pipelines/release.yml`). Do not +change `version` in `composer.json` by hand: it holds the last released version, and the +pipeline calculates the next one from it. + +Before running it: + +- Write `docs/release-notes/.md` and add its row to `docs/release-notes/README.md` + (see that README for the standard), and merge that to `master`. The pipeline fails if + either is missing. A prerelease (`-rc.`) uses the notes of ``. + +Then: + +1. Run the pipeline with **ReleaseType** `major`, `minor`, `patch` or `prerelease`. It tests + on the minimum supported PHP version, sets the new version in `composer.json`, pushes + `release/v` to `github.com/queueit/QueueToken.V1.PHP` and commits the version + bump back to `master`. +2. Pushing the branch opens the release PR on GitHub. Its description is the release notes + file. +3. Review and merge the PR. The merge runs the **Final GitHub Release** workflow, which + tags the merged commit `v` and creates the GitHub release with the PR + description as its text. +4. Packagist publishes the tag through its GitHub hook within a few minutes: + https://packagist.org/packages/queueit/queuetoken-v1-php + --- This file is for internal use and documents the setup and test process for developers working on this project. diff --git a/README.md b/README.md index 9fceafb..2121326 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,13 @@ The `Queue-it` Queue Token SDK is used to ensure that end users cannot enter the queue without a valid token and to be a container which can carry sensitive user information from integrating system into the queue. +## Requirements + +- PHP 8.2 or later. The library is tested on PHP 8.2, 8.3, 8.4 and 8.5. +- The OpenSSL extension (`ext-openssl`). + +Applications running PHP 7.4, 8.0 or 8.1 must stay on version 1.1.x. Composer selects it automatically on those PHP versions. + ## The Token The token consists of two parts. Firstly, a header containing non-sensitive metadata. Secondly the payload of the token. @@ -52,21 +59,22 @@ Both header and payload are in JSON format. ## Usage ```php -const secretKey = '...'; -const token = Token - .enqueue("ticketania") - .withPayload(Payload - .enqueue() - .withKey("XKDI42W") - .withRelativeQuality(0.4578) - .withCustomData("size", "medium") - .generate()) - .withEventId("demoevent") - .withIpAddress("75.86.129.4", "45.67.2.4,34.56.3.2") - .withValidity(60000) - .generate(secretKey); - -const tokenValue = token.Token; +use QueueIT\QueueToken\Payload; +use QueueIT\QueueToken\Token; + +$secretKey = '...'; +$token = Token::enqueue("ticketania") + ->withPayload(Payload::enqueue() + ->withKey("XKDI42W") + ->withRelativeQuality(0.4578) + ->withCustomData("size", "medium") + ->generate()) + ->withEventId("demoevent") + ->withIpAddress("75.86.129.4", "45.67.2.4,34.56.3.2") + ->withValidity(60000) + ->generate($secretKey); + +$tokenValue = $token->getToken(); ``` ### Specifying token identifier prefix @@ -74,13 +82,12 @@ const tokenValue = token.Token; A prefix for the token identifier can optionally be provided to restrict the user session after getting through the queue to the one used before entering the queue. Once the user is through the queue the token identifier is provided to the target application in the Known User token. The format of the token identifier is then `[YOUR PREFIX]~[GUID]`, e.g: AnfTDnpwazllYmnmgaCJ8tErV80YHv77ni5NgqQNhfWwxNqrNcHb~e937ef0d-48ec-4ff7-866e-52033273cb3d. ```php -const tokenIdentifierPrefix = "AnfTDnpwazllYmnmgaCJ8tErV80YHv77ni5NgqQNhfWwxNqrNcHb"; -const token = Token - .enqueue("ticketania", tokenIdentifierPrefix) - .generate(secretKey); +$tokenIdentifierPrefix = "AnfTDnpwazllYmnmgaCJ8tErV80YHv77ni5NgqQNhfWwxNqrNcHb"; +$token = Token::enqueue("ticketania", $tokenIdentifierPrefix) + ->generate($secretKey); -const tokenIdentifier = token.TokenIdentifier; -// tokenIdentifier example: AnfTDnpwazllYmnmgaCJ8tErV80YHv77ni5NgqQNhfWwxNqrNcHb~e937ef0d-48ec-4ff7-866e-52033273cb3d +$tokenIdentifier = $token->getTokenIdentifier(); +// $tokenIdentifier example: AnfTDnpwazllYmnmgaCJ8tErV80YHv77ni5NgqQNhfWwxNqrNcHb~e937ef0d-48ec-4ff7-866e-52033273cb3d ``` ## Serialized Token @@ -88,7 +95,7 @@ const tokenIdentifier = token.TokenIdentifier; > eyJ0eXAiOiJRVDEiLCJlbmMiOiJBRVMyNTYiLCJpc3MiOjE1MzQ3MjMyMDAwMDAsImV4cCI6MTUzOTEyOTYwMDAwMCwidGkiOiJhMjFkNDIzYS00M2ZkLTQ4MjEtODRmYS00MzkwZjZhMmZkM2UiLCJjIjoidGlja2V0YW5pYSIsImUiOiJteWV2ZW50In0.0rDlI69F1Dx4Twps5qD4cQrbXbCRiezBd6fH1PVm6CnVY456FALkAhN3rgVrh_PGCJHcEXN5zoqFg65MH8WZc_CQdD63hJre3Sedu0-9zIs.aZgzkJm57etFaXjjME_-9LjOgPNTTqkp1aJ057HuEiU The format of the token is [header].[payload].[hash] where each part is Base64Url encoded. -The payload is AES 256 encrypted with the secret key supplied in the `.Generate(secretKey)` method. +The payload is AES 256 encrypted with the secret key supplied to the `generate($secretKey)` method. If the "e" key is provided in the header, the secret key on the event must be used. If no "e" key is provided the default key on the customer account must be used. The token is signed with SHA 256 using the same secret key. diff --git a/composer.json b/composer.json index b994ea6..a2a8cfb 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "queueit/queuetoken-v1-php", - "version": "1.1.0", + "version": "1.2.0", "description": "Queue-it PHP queue token library for managing enqueue tokens", "type": "library", "keywords": [ @@ -19,12 +19,11 @@ } ], "require": { - "php": ">=7.4", - "ext-json": "*", + "php": ">=8.2", "ext-openssl": "*" }, "require-dev": { - "phpunit/phpunit": "^9.5" + "phpunit/phpunit": "^9.6.11" }, "autoload": { "psr-4": { @@ -36,12 +35,17 @@ "QueueIT\\QueueToken\\Tests\\": "tests/" } }, + "config": { + "platform": { + "php": "8.2.0" + } + }, "extra": { "branch-alias": { "dev-main": "1.x-dev" } }, "scripts": { - "test": "vendor\\bin\\phpunit tests --testdox" + "test": "phpunit --testdox" } } diff --git a/composer.lock b/composer.lock index f1306d2..80909a1 100644 --- a/composer.lock +++ b/composer.lock @@ -4,35 +4,35 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "2d977581d45ce562de2d593e1c7c2abf", + "content-hash": "94c556c2938bf6e9d31d88d6a000a74b", "packages": [], "packages-dev": [ { "name": "doctrine/instantiator", - "version": "1.5.0", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/doctrine/instantiator.git", - "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b" + "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/0a0fa9780f5d4e507415a065172d26a98d02047b", - "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", + "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", "shasum": "" }, "require": { - "php": "^7.1 || ^8.0" + "php": "^8.1" }, "require-dev": { - "doctrine/coding-standard": "^9 || ^11", + "doctrine/coding-standard": "^11", "ext-pdo": "*", "ext-phar": "*", - "phpbench/phpbench": "^0.16 || ^1", - "phpstan/phpstan": "^1.4", - "phpstan/phpstan-phpunit": "^1", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", - "vimeo/psalm": "^4.30 || ^5.4" + "phpbench/phpbench": "^1.2", + "phpstan/phpstan": "^1.9.4", + "phpstan/phpstan-phpunit": "^1.3", + "phpunit/phpunit": "^9.5.27", + "vimeo/psalm": "^5.4" }, "type": "library", "autoload": { @@ -59,7 +59,7 @@ ], "support": { "issues": "https://github.com/doctrine/instantiator/issues", - "source": "https://github.com/doctrine/instantiator/tree/1.5.0" + "source": "https://github.com/doctrine/instantiator/tree/2.0.0" }, "funding": [ { @@ -75,24 +75,25 @@ "type": "tidelift" } ], - "time": "2022-12-30T00:15:36+00:00" + "time": "2022-12-30T00:23:10+00:00", + "published-time": "2022-12-30T14:13:42+00:00" }, { "name": "myclabs/deep-copy", - "version": "1.13.4", + "version": "1.14.0", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" + "reference": "8680aa248f8e07bc8fb43f56f0f5fc77a0c96aae" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", - "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/8680aa248f8e07bc8fb43f56f0f5fc77a0c96aae", + "reference": "8680aa248f8e07bc8fb43f56f0f5fc77a0c96aae", "shasum": "" }, "require": { - "php": "^7.1 || ^8.0" + "php": "^8.0" }, "conflict": { "doctrine/collections": "<1.6.8", @@ -127,32 +128,32 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" + "source": "https://github.com/myclabs/DeepCopy/tree/1.14.0" }, "funding": [ { - "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", - "type": "tidelift" + "url": "https://github.com/mnapoli", + "type": "github" } ], - "time": "2025-08-01T08:46:24+00:00" + "time": "2026-08-11T10:17:44+00:00", + "published-time": "2026-08-11T10:29:20+00:00" }, { "name": "nikic/php-parser", - "version": "v5.7.0", + "version": "v5.9.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" + "reference": "9e33da9553fe7786f0962b35f4e4ecf01be89def" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", - "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/9e33da9553fe7786f0962b35f4e4ecf01be89def", + "reference": "9e33da9553fe7786f0962b35f4e4ecf01be89def", "shasum": "" }, "require": { - "ext-ctype": "*", "ext-json": "*", "ext-tokenizer": "*", "php": ">=7.4" @@ -191,9 +192,10 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.9.0" }, - "time": "2025-12-06T11:56:16+00:00" + "time": "2026-09-13T18:51:52+00:00", + "published-time": "2026-09-13T18:53:16+00:00" }, { "name": "phar-io/manifest", @@ -260,7 +262,8 @@ "type": "github" } ], - "time": "2024-03-03T12:33:53+00:00" + "time": "2024-03-03T12:33:53+00:00", + "published-time": "2024-03-03T12:39:44+00:00" }, { "name": "phar-io/version", @@ -311,7 +314,8 @@ "issues": "https://github.com/phar-io/version/issues", "source": "https://github.com/phar-io/version/tree/3.2.1" }, - "time": "2022-02-21T01:04:05+00:00" + "time": "2022-02-21T01:04:05+00:00", + "published-time": "2022-02-21T01:04:30+00:00" }, { "name": "phpunit/php-code-coverage", @@ -389,7 +393,8 @@ "type": "github" } ], - "time": "2024-08-22T04:23:01+00:00" + "time": "2024-08-22T04:23:01+00:00", + "published-time": "2024-08-22T04:23:58+00:00" }, { "name": "phpunit/php-file-iterator", @@ -449,7 +454,8 @@ "type": "github" } ], - "time": "2021-12-02T12:48:52+00:00" + "time": "2021-12-02T12:48:52+00:00", + "published-time": "2021-12-02T12:54:16+00:00" }, { "name": "phpunit/php-invoker", @@ -512,7 +518,8 @@ "type": "github" } ], - "time": "2020-09-28T05:58:55+00:00" + "time": "2020-09-28T05:58:55+00:00", + "published-time": "2020-09-28T05:59:12+00:00" }, { "name": "phpunit/php-text-template", @@ -571,7 +578,8 @@ "type": "github" } ], - "time": "2020-10-26T05:33:50+00:00" + "time": "2020-10-26T05:33:50+00:00", + "published-time": "2020-10-26T05:34:19+00:00" }, { "name": "phpunit/php-timer", @@ -630,29 +638,30 @@ "type": "github" } ], - "time": "2020-10-26T13:16:10+00:00" + "time": "2020-10-26T13:16:10+00:00", + "published-time": "2020-10-26T13:16:41+00:00" }, { "name": "phpunit/phpunit", - "version": "9.6.31", + "version": "9.6.38", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "945d0b7f346a084ce5549e95289962972c4272e5" + "reference": "8b5f757e8e92071bfdbfa93943f94f84c4e31007" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/945d0b7f346a084ce5549e95289962972c4272e5", - "reference": "945d0b7f346a084ce5549e95289962972c4272e5", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/8b5f757e8e92071bfdbfa93943f94f84c4e31007", + "reference": "8b5f757e8e92071bfdbfa93943f94f84c4e31007", "shasum": "" }, "require": { "doctrine/instantiator": "^1.5.0 || ^2", "ext-dom": "*", + "ext-filter": "*", "ext-json": "*", "ext-libxml": "*", "ext-mbstring": "*", - "ext-xml": "*", "ext-xmlwriter": "*", "myclabs/deep-copy": "^1.13.4", "phar-io/manifest": "^2.0.4", @@ -665,10 +674,10 @@ "phpunit/php-timer": "^5.0.3", "sebastian/cli-parser": "^1.0.2", "sebastian/code-unit": "^1.0.8", - "sebastian/comparator": "^4.0.9", + "sebastian/comparator": "^4.0.10", "sebastian/diff": "^4.0.6", "sebastian/environment": "^5.1.5", - "sebastian/exporter": "^4.0.8", + "sebastian/exporter": "^4.0.9", "sebastian/global-state": "^5.0.8", "sebastian/object-enumerator": "^4.0.4", "sebastian/resource-operations": "^3.0.4", @@ -717,31 +726,16 @@ "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.31" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.38" }, "funding": [ { - "url": "https://phpunit.de/sponsors.html", - "type": "custom" - }, - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - }, - { - "url": "https://liberapay.com/sebastianbergmann", - "type": "liberapay" - }, - { - "url": "https://thanks.dev/u/gh/sebastianbergmann", - "type": "thanks_dev" - }, - { - "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", - "type": "tidelift" + "url": "https://phpunit.de/sponsoring.html", + "type": "other" } ], - "time": "2025-12-06T07:45:52+00:00" + "time": "2026-10-05T05:44:26+00:00", + "published-time": "2026-10-05T05:45:23+00:00" }, { "name": "sebastian/cli-parser", @@ -797,7 +791,8 @@ "type": "github" } ], - "time": "2024-03-02T06:27:43+00:00" + "time": "2024-03-02T06:27:43+00:00", + "published-time": "2024-03-02T06:28:53+00:00" }, { "name": "sebastian/code-unit", @@ -853,7 +848,8 @@ "type": "github" } ], - "time": "2020-10-26T13:08:54+00:00" + "time": "2020-10-26T13:08:54+00:00", + "published-time": "2020-10-26T13:09:16+00:00" }, { "name": "sebastian/code-unit-reverse-lookup", @@ -908,20 +904,21 @@ "type": "github" } ], - "time": "2020-09-28T05:30:19+00:00" + "time": "2020-09-28T05:30:19+00:00", + "published-time": "2020-09-28T05:30:37+00:00" }, { "name": "sebastian/comparator", - "version": "4.0.9", + "version": "4.0.10", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "67a2df3a62639eab2cc5906065e9805d4fd5dfc5" + "reference": "e4df00b9b3571187db2831ae9aada2c6efbd715d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/67a2df3a62639eab2cc5906065e9805d4fd5dfc5", - "reference": "67a2df3a62639eab2cc5906065e9805d4fd5dfc5", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/e4df00b9b3571187db2831ae9aada2c6efbd715d", + "reference": "e4df00b9b3571187db2831ae9aada2c6efbd715d", "shasum": "" }, "require": { @@ -974,7 +971,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", - "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.9" + "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.10" }, "funding": [ { @@ -994,7 +991,8 @@ "type": "tidelift" } ], - "time": "2025-08-10T06:51:50+00:00" + "time": "2026-01-24T09:22:56+00:00", + "published-time": "2026-01-24T09:23:33+00:00" }, { "name": "sebastian/complexity", @@ -1051,7 +1049,8 @@ "type": "github" } ], - "time": "2023-12-22T06:19:30+00:00" + "time": "2023-12-22T06:19:30+00:00", + "published-time": "2023-12-22T06:19:59+00:00" }, { "name": "sebastian/diff", @@ -1117,7 +1116,8 @@ "type": "github" } ], - "time": "2024-03-02T06:30:58+00:00" + "time": "2024-03-02T06:30:58+00:00", + "published-time": "2024-03-02T06:31:28+00:00" }, { "name": "sebastian/environment", @@ -1180,20 +1180,21 @@ "type": "github" } ], - "time": "2023-02-03T06:03:51+00:00" + "time": "2023-02-03T06:03:51+00:00", + "published-time": "2023-02-03T07:02:10+00:00" }, { "name": "sebastian/exporter", - "version": "4.0.8", + "version": "4.0.9", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c" + "reference": "4352c1a3df741a7ba9e61af6fed51d1fee41cbf7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/14c6ba52f95a36c3d27c835d65efc7123c446e8c", - "reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/4352c1a3df741a7ba9e61af6fed51d1fee41cbf7", + "reference": "4352c1a3df741a7ba9e61af6fed51d1fee41cbf7", "shasum": "" }, "require": { @@ -1249,7 +1250,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.8" + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.9" }, "funding": [ { @@ -1269,7 +1270,8 @@ "type": "tidelift" } ], - "time": "2025-09-24T06:03:27+00:00" + "time": "2026-08-11T04:55:59+00:00", + "published-time": "2026-08-11T04:56:24+00:00" }, { "name": "sebastian/global-state", @@ -1345,7 +1347,8 @@ "type": "tidelift" } ], - "time": "2025-08-10T07:10:35+00:00" + "time": "2025-08-10T07:10:35+00:00", + "published-time": "2025-08-10T07:11:13+00:00" }, { "name": "sebastian/lines-of-code", @@ -1402,7 +1405,8 @@ "type": "github" } ], - "time": "2023-12-22T06:20:34+00:00" + "time": "2023-12-22T06:20:34+00:00", + "published-time": "2023-12-22T06:20:48+00:00" }, { "name": "sebastian/object-enumerator", @@ -1459,7 +1463,8 @@ "type": "github" } ], - "time": "2020-10-26T13:12:34+00:00" + "time": "2020-10-26T13:12:34+00:00", + "published-time": "2020-10-26T13:12:54+00:00" }, { "name": "sebastian/object-reflector", @@ -1514,20 +1519,21 @@ "type": "github" } ], - "time": "2020-10-26T13:14:26+00:00" + "time": "2020-10-26T13:14:26+00:00", + "published-time": "2020-10-26T13:14:45+00:00" }, { "name": "sebastian/recursion-context", - "version": "4.0.6", + "version": "4.0.7", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "539c6691e0623af6dc6f9c20384c120f963465a0" + "reference": "c85be6922b7fd365942b986b9a50397d65407611" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/539c6691e0623af6dc6f9c20384c120f963465a0", - "reference": "539c6691e0623af6dc6f9c20384c120f963465a0", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/c85be6922b7fd365942b986b9a50397d65407611", + "reference": "c85be6922b7fd365942b986b9a50397d65407611", "shasum": "" }, "require": { @@ -1569,7 +1575,7 @@ "homepage": "https://github.com/sebastianbergmann/recursion-context", "support": { "issues": "https://github.com/sebastianbergmann/recursion-context/issues", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.6" + "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.7" }, "funding": [ { @@ -1589,7 +1595,8 @@ "type": "tidelift" } ], - "time": "2025-08-10T06:57:39+00:00" + "time": "2026-08-11T05:25:24+00:00", + "published-time": "2026-08-11T05:25:42+00:00" }, { "name": "sebastian/resource-operations", @@ -1643,7 +1650,8 @@ "type": "github" } ], - "time": "2024-03-14T16:00:52+00:00" + "time": "2024-03-14T16:00:52+00:00", + "published-time": "2024-03-14T16:01:11+00:00" }, { "name": "sebastian/type", @@ -1699,7 +1707,8 @@ "type": "github" } ], - "time": "2023-02-03T06:13:03+00:00" + "time": "2023-02-03T06:13:03+00:00", + "published-time": "2023-02-03T07:09:48+00:00" }, { "name": "sebastian/version", @@ -1752,7 +1761,8 @@ "type": "github" } ], - "time": "2020-09-28T06:39:44+00:00" + "time": "2020-09-28T06:39:44+00:00", + "published-time": "2020-09-28T06:40:08+00:00" }, { "name": "theseer/tokenizer", @@ -1802,7 +1812,8 @@ "type": "github" } ], - "time": "2025-11-17T20:03:58+00:00" + "time": "2025-11-17T20:03:58+00:00", + "published-time": "2025-11-17T20:04:52+00:00" } ], "aliases": [], @@ -1811,10 +1822,12 @@ "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": ">=7.4", - "ext-json": "*", + "php": ">=8.2", "ext-openssl": "*" }, "platform-dev": {}, - "plugin-api-version": "2.9.0" + "platform-overrides": { + "php": "8.2.0" + }, + "plugin-api-version": "2.11.0" } diff --git a/phpunit.xml b/phpunit.xml index 952f1a1..5d0037a 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,5 +1,5 @@ - + ./tests diff --git a/src/EnqueueToken.php b/src/EnqueueToken.php index ea04ddc..a388f58 100644 --- a/src/EnqueueToken.php +++ b/src/EnqueueToken.php @@ -123,7 +123,7 @@ private function generateTokenIdentifier(?string $tokenIdentifierPrefix): string : Utils::generateUUID(); } - public function generate(string $secretKey, bool $resetTokenIdentifier = true): void + public function generate(#[\SensitiveParameter] string $secretKey, bool $resetTokenIdentifier = true): void { if ($resetTokenIdentifier) { $this->setTokenIdentifier($this->generateTokenIdentifier($this->tokenIdentifierPrefix)); @@ -161,14 +161,14 @@ private function createHeaderDto(): HeaderDto return $dto; } - public static function parse(string $tokenString, string $secretKey): IEnqueueToken + public static function parse(string $tokenString, #[\SensitiveParameter] string $secretKey): IEnqueueToken { self::validateParseInputs($tokenString, $secretKey); $tokenParts = explode(".", $tokenString); $headerPart = $tokenParts[0]; $payloadPart = $tokenParts[1] ?? ''; - $hashPart = $tokenParts[2]; + $hashPart = $tokenParts[2] ?? ''; if (empty($headerPart) || empty($hashPart)) { throw new ArgumentException("Invalid token"); @@ -184,7 +184,7 @@ public static function parse(string $tokenString, string $secretKey): IEnqueueTo } } - private static function validateParseInputs(string $tokenString, string $secretKey): void + private static function validateParseInputs(string $tokenString, #[\SensitiveParameter] string $secretKey): void { if (empty($secretKey)) { throw new ArgumentException("Invalid secret key"); @@ -194,7 +194,7 @@ private static function validateParseInputs(string $tokenString, string $secretK } } - private static function validateTokenHash(string $token, string $hashPart, string $secretKey): void + private static function validateTokenHash(string $token, string $hashPart, #[\SensitiveParameter] string $secretKey): void { $hash = ShaHashing::generateHash($secretKey, $token); $expectedHash = Base64UrlEncoding::encode($hash); @@ -203,7 +203,7 @@ private static function validateTokenHash(string $token, string $hashPart, strin } } - private static function deserializeToken(string $headerPart, string $payloadPart, string $token, string $hashPart, string $secretKey): IEnqueueToken + private static function deserializeToken(string $headerPart, string $payloadPart, string $token, string $hashPart, #[\SensitiveParameter] string $secretKey): IEnqueueToken { $headerModel = HeaderDto::deserializeHeader($headerPart); $payload = !empty($payloadPart) diff --git a/src/EnqueueTokenGenerator.php b/src/EnqueueTokenGenerator.php index d5fec17..9582550 100644 --- a/src/EnqueueTokenGenerator.php +++ b/src/EnqueueTokenGenerator.php @@ -22,7 +22,7 @@ public function withEventId(string $eventId): self public function withValidity(int $validityMilliSeconds): self { $currentTimestamp = $this->token->issued->getTimestamp(); - $expiryTimestamp = $currentTimestamp + ($validityMilliSeconds / 1000); + $expiryTimestamp = $currentTimestamp + intdiv($validityMilliSeconds, 1000); $this->token->expires = (new DateTime())->setTimestamp($expiryTimestamp); return $this; } @@ -46,7 +46,7 @@ public function withIpAddress(string $ip, ?string $xForwardedFor): self return $this; } - public function generate(string $secretKey): IEnqueueToken + public function generate(#[\SensitiveParameter] string $secretKey): IEnqueueToken { $this->token->generate($secretKey); $clone = $this->token; diff --git a/src/EnqueueTokenPayload.php b/src/EnqueueTokenPayload.php index b6c089e..70f58d6 100644 --- a/src/EnqueueTokenPayload.php +++ b/src/EnqueueTokenPayload.php @@ -12,21 +12,12 @@ class EnqueueTokenPayload implements IEnqueueTokenPayload { - private array $customData; - private ?string $key; - private ?float $relativeQuality; - private string $origin; - public function __construct( - ?string $key = null, - ?float $relativeQuality = null, - array $customData = [], - string $origin = TokenOrigin::CONNECTOR + private ?string $key = null, + private ?float $relativeQuality = null, + private array $customData = [], + private string $origin = TokenOrigin::CONNECTOR ) { - $this->key = $key; - $this->relativeQuality = $relativeQuality; - $this->customData = $customData; - $this->origin = $origin; } public function getKey(): ?string @@ -73,7 +64,7 @@ public function setTokenOrigin(string $origin): EnqueueTokenPayload return $this; } - public function addCustomData(string $key, $value): EnqueueTokenPayload + public function addCustomData(string $key, mixed $value): EnqueueTokenPayload { $this->customData[$key] = $value; return $this; @@ -90,7 +81,7 @@ public function serialize(): array return $dto->serialize(); } - public static function deserialize(string $input, string $secretKey, string $tokenIdentifier): ?EnqueueTokenPayload + public static function deserialize(string $input, #[\SensitiveParameter] string $secretKey, string $tokenIdentifier): ?EnqueueTokenPayload { $dto = PayloadDto::deserializePayload($input, $secretKey, $tokenIdentifier); if ($dto === null) { @@ -105,7 +96,7 @@ public static function deserialize(string $input, string $secretKey, string $tok ); } - public function encryptAndEncode(string $secretKey, string $tokenIdentifier): string + public function encryptAndEncode(#[\SensitiveParameter] string $secretKey, string $tokenIdentifier): string { try { $serializedPayload = $this->serialize(); diff --git a/src/EnqueueTokenPayloadGenerator.php b/src/EnqueueTokenPayloadGenerator.php index eeca3ea..6778697 100644 --- a/src/EnqueueTokenPayloadGenerator.php +++ b/src/EnqueueTokenPayloadGenerator.php @@ -4,7 +4,7 @@ class EnqueueTokenPayloadGenerator { - private $payload; + private EnqueueTokenPayload $payload; public function __construct() { diff --git a/src/Helpers/AESEncryption.php b/src/Helpers/AESEncryption.php index 05fc91c..0ebbaf6 100644 --- a/src/Helpers/AESEncryption.php +++ b/src/Helpers/AESEncryption.php @@ -9,7 +9,7 @@ class AESEncryption { private const CIPHER = 'aes-256-cbc'; - public static function encryptPayload(string $secretKey, string $tokenIdentifier, array $valueToEncrypt): string + public static function encryptPayload(#[\SensitiveParameter] string $secretKey, string $tokenIdentifier, array $valueToEncrypt): string { $key = self::deriveKey($secretKey); $iv = self::generateIV($tokenIdentifier); @@ -24,7 +24,7 @@ public static function encryptPayload(string $secretKey, string $tokenIdentifier return $encryptedData; } - public static function decryptPayload(string $secretKey, string $tokenIdentifier, string $encryptedData): array + public static function decryptPayload(#[\SensitiveParameter] string $secretKey, string $tokenIdentifier, string $encryptedData): array { $key = self::deriveKey($secretKey); $iv = self::generateIV($tokenIdentifier); @@ -38,7 +38,7 @@ public static function decryptPayload(string $secretKey, string $tokenIdentifier return array_values(unpack('C*', $decryptedData)); } - private static function deriveKey(string $secretKey): string + private static function deriveKey(#[\SensitiveParameter] string $secretKey): string { return ShaHashing::generateHash($secretKey, ''); } diff --git a/src/Helpers/ShaHashing.php b/src/Helpers/ShaHashing.php index 4405cc9..d523733 100644 --- a/src/Helpers/ShaHashing.php +++ b/src/Helpers/ShaHashing.php @@ -4,7 +4,7 @@ class ShaHashing { - public static function generateHash(string $secretKey, string $tokenString): string + public static function generateHash(#[\SensitiveParameter] string $secretKey, string $tokenString): string { $combinedString = $tokenString . $secretKey; return hash('sha256', $combinedString, true); diff --git a/src/IEnqueueToken.php b/src/IEnqueueToken.php index dfa5eef..af49698 100644 --- a/src/IEnqueueToken.php +++ b/src/IEnqueueToken.php @@ -2,6 +2,8 @@ namespace QueueIT\QueueToken; +use QueueIT\QueueToken\IEnqueueTokenPayload; + interface IEnqueueToken { public function getTokenIdentifier(): string; diff --git a/src/IEnqueueTokenPayload.php b/src/IEnqueueTokenPayload.php index e2f0cc7..00954bb 100644 --- a/src/IEnqueueTokenPayload.php +++ b/src/IEnqueueTokenPayload.php @@ -12,7 +12,7 @@ public function getCustomData(): array; public function getTokenOrigin(): string; - public function encryptAndEncode(string $secretKey, string $tokenIdentifier): string; + public function encryptAndEncode(#[\SensitiveParameter] string $secretKey, string $tokenIdentifier): string; public function serialize(): array; } diff --git a/src/Models/PayloadDto.php b/src/Models/PayloadDto.php index 1c077c1..908238b 100644 --- a/src/Models/PayloadDto.php +++ b/src/Models/PayloadDto.php @@ -32,7 +32,7 @@ public function serialize(): array return array_values(unpack('C*', $jsonString)); } - public static function deserializePayload(string $input, string $secretKey, string $tokenIdentifier): ?PayloadDto + public static function deserializePayload(string $input, #[\SensitiveParameter] string $secretKey, string $tokenIdentifier): ?PayloadDto { $headerEncrypted = Base64UrlEncoding::decode($input); $decryptedBytes = AESEncryption::decryptPayload($secretKey, $tokenIdentifier, $headerEncrypted); diff --git a/src/Models/TokenOrigin.php b/src/Models/TokenOrigin.php index b199862..594c8b3 100644 --- a/src/Models/TokenOrigin.php +++ b/src/Models/TokenOrigin.php @@ -6,4 +6,5 @@ class TokenOrigin { const CONNECTOR = 'Connector'; const INVITE_ONLY = 'InviteOnly'; + const AKAMAI_BOT_MANAGER_HEADER_VALIDATOR = 'AkamaiBotManagerHeaderValidator'; } diff --git a/src/Payload.php b/src/Payload.php index 227b309..b3a232f 100644 --- a/src/Payload.php +++ b/src/Payload.php @@ -2,9 +2,11 @@ namespace QueueIT\QueueToken; +use QueueIT\QueueToken\EnqueueTokenPayloadGenerator; + class Payload { - public static function enqueue() + public static function enqueue(): EnqueueTokenPayloadGenerator { return new EnqueueTokenPayloadGenerator(); } diff --git a/src/Token.php b/src/Token.php index 3c346b5..6b61bf0 100644 --- a/src/Token.php +++ b/src/Token.php @@ -9,7 +9,7 @@ public static function enqueue(string $customerId, ?string $tokenIdentifierPrefi return new EnqueueTokenGenerator($customerId, $tokenIdentifierPrefix); } - public static function parse(string $token, string $secretKey): IEnqueueToken + public static function parse(string $token, #[\SensitiveParameter] string $secretKey): IEnqueueToken { return EnqueueToken::parse($token, $secretKey); } diff --git a/tests/EnqueueTokenPayloadTest.php b/tests/EnqueueTokenPayloadTest.php index 1fb68a9..b1cfd0e 100644 --- a/tests/EnqueueTokenPayloadTest.php +++ b/tests/EnqueueTokenPayloadTest.php @@ -4,9 +4,12 @@ use PHPUnit\Framework\TestCase; +use QueueIT\QueueToken\Helpers\AESEncryption; +use QueueIT\QueueToken\Helpers\Base64UrlEncoding; use QueueIT\QueueToken\Helpers\Utils; use QueueIT\QueueToken\Payload; use QueueIT\QueueToken\EnqueueTokenPayload; +use QueueIT\QueueToken\Models\TokenOrigin; class EnqueueTokenPayloadTest extends TestCase { @@ -243,4 +246,141 @@ public function testEncryptedCorrectly() $this->assertEquals($expectedEncryptedPayload, $actualEncryptedPayload); $this->assertEquals($payload, $decryptPayload); } + + public function testGeneratePayloadWithInviteOnlyOrigin() + { + $expectedKey = "myKey"; + $expectedOrigin = TokenOrigin::INVITE_ONLY; + + $instance = Payload::Enqueue() + ->WithKey($expectedKey) + ->WithOrigin($expectedOrigin) + ->Generate(); + + $this->assertEquals($expectedKey, $instance->getKey()); + $this->assertEquals($expectedOrigin, $instance->getTokenOrigin()); + } + + public function testGeneratePayloadWithAkamaiBotManagerHeaderValidatorOrigin() + { + $expectedKey = "myKey"; + $expectedOrigin = TokenOrigin::AKAMAI_BOT_MANAGER_HEADER_VALIDATOR; + + $instance = Payload::Enqueue() + ->WithKey($expectedKey) + ->WithOrigin($expectedOrigin) + ->Generate(); + + $this->assertEquals($expectedKey, $instance->getKey()); + $this->assertEquals($expectedOrigin, $instance->getTokenOrigin()); + } + + public function testGeneratePayloadWithConnectorOrigin() + { + $expectedKey = "myKey"; + $expectedOrigin = TokenOrigin::CONNECTOR; + + $instance = Payload::Enqueue() + ->WithKey($expectedKey) + ->WithOrigin($expectedOrigin) + ->Generate(); + + $this->assertEquals($expectedKey, $instance->getKey()); + $this->assertEquals($expectedOrigin, $instance->getTokenOrigin()); + } + + public function testSerializeWithInviteOnlyOrigin() + { + $expectedJson = '{"r":0.456,"k":"myKey","o":"InviteOnly"}'; + + $instance = Payload::Enqueue() + ->WithKey("myKey") + ->WithRelativeQuality(0.456) + ->WithOrigin(TokenOrigin::INVITE_ONLY) + ->Generate(); + $actualJson = Utils::uint8ArrayToString($instance->Serialize()); + + $this->assertEquals($expectedJson, $actualJson); + } + + public function testSerializeWithAkamaiBotManagerHeaderValidatorOrigin() + { + $expectedJson = '{"r":0.456,"k":"myKey","o":"AkamaiBotManagerHeaderValidator"}'; + + $instance = Payload::Enqueue() + ->WithKey("myKey") + ->WithRelativeQuality(0.456) + ->WithOrigin(TokenOrigin::AKAMAI_BOT_MANAGER_HEADER_VALIDATOR) + ->Generate(); + $actualJson = Utils::uint8ArrayToString($instance->Serialize()); + + $this->assertEquals($expectedJson, $actualJson); + } + + public function testSerializeWithConnectorOrigin() + { + $expectedJson = '{"r":0.456,"k":"myKey","o":"Connector"}'; + + $instance = Payload::Enqueue() + ->WithKey("myKey") + ->WithRelativeQuality(0.456) + ->WithOrigin(TokenOrigin::CONNECTOR) + ->Generate(); + $actualJson = Utils::uint8ArrayToString($instance->Serialize()); + + $this->assertEquals($expectedJson, $actualJson); + } + + public function testSerializeWithInviteOnlyOriginAndCustomData() + { + $expectedJson = '{"r":0.456,"k":"myKey","cd":{"key1":"Value1","key2":"Value2"},"o":"InviteOnly"}'; + + $instance = Payload::Enqueue() + ->WithKey("myKey") + ->WithRelativeQuality(0.456) + ->WithCustomData("key1", "Value1") + ->WithCustomData("key2", "Value2") + ->WithOrigin(TokenOrigin::INVITE_ONLY) + ->Generate(); + $actualJson = Utils::uint8ArrayToString($instance->Serialize()); + + $this->assertEquals($expectedJson, $actualJson); + } + + public function testSerializeWithAkamaiBotManagerOriginAndCustomData() + { + $expectedJson = '{"r":0.456,"k":"myKey","cd":{"key1":"Value1","key2":"Value2"},"o":"AkamaiBotManagerHeaderValidator"}'; + + $instance = Payload::Enqueue() + ->WithKey("myKey") + ->WithRelativeQuality(0.456) + ->WithCustomData("key1", "Value1") + ->WithCustomData("key2", "Value2") + ->WithOrigin(TokenOrigin::AKAMAI_BOT_MANAGER_HEADER_VALIDATOR) + ->Generate(); + $actualJson = Utils::uint8ArrayToString($instance->Serialize()); + + $this->assertEquals($expectedJson, $actualJson); + } + + public function testSetCustomDataReplacesExistingCustomData() + { + $instance = new EnqueueTokenPayload("myKey", null, ["old" => "value"]); + + $returned = $instance->setCustomData(["new" => "value"]); + + $this->assertSame($instance, $returned); + $this->assertEquals(["new" => "value"], $instance->getCustomData()); + } + + public function testDeserializeReturnsNullWhenPayloadIsNotJson() + { + $secretKey = "5ebbf794-1665-4d48-80d6-21ac34be7faedf9e10b3-551a-4682-bb77-fee59d6355d6"; + $identifier = "a21d423a-43fd-4821-84fa-4390f6a2fd3e"; + $encrypted = AESEncryption::encryptPayload($secretKey, $identifier, Utils::stringToUint8Array("not json")); + + $payload = EnqueueTokenPayload::deserialize(Base64UrlEncoding::encode($encrypted), $secretKey, $identifier); + + $this->assertNull($payload); + } } diff --git a/tests/EnqueueTokenTest.php b/tests/EnqueueTokenTest.php index e9ca44d..4a464be 100644 --- a/tests/EnqueueTokenTest.php +++ b/tests/EnqueueTokenTest.php @@ -8,11 +8,17 @@ use QueueIT\QueueToken\Token; use QueueIT\QueueToken\EnqueueToken; +use QueueIT\QueueToken\IEnqueueTokenPayload; use QueueIT\QueueToken\Payload; +use QueueIT\QueueToken\Exceptions\ArgumentException; +use QueueIT\QueueToken\Exceptions\InvalidHashException; +use QueueIT\QueueToken\Exceptions\TokenDeserializationException; +use QueueIT\QueueToken\Exceptions\TokenSerializationException; use QueueIT\QueueToken\Helpers\Utils; use QueueIT\QueueToken\Models\TokenVersion; use QueueIT\QueueToken\Models\EncryptionType; use QueueIT\QueueToken\Models\HeaderDto; +use QueueIT\QueueToken\Models\TokenOrigin; use QueueIT\QueueToken\Tests\SampleTokenValues; @@ -59,6 +65,15 @@ public function testCreateTokenWithValidityAsLong() $this->assertEquals($expectedExpiry->getTimestamp() * 1000, $token->expires->getTimestamp() * 1000); } + public function testCreateTokenWithValidityInFractionalSecondsTruncatesToWholeSeconds() + { + $token = Token::enqueue(SampleTokenValues::$customerId) + ->withValidity(1500) + ->generate(SampleTokenValues::$secretKey); + + $this->assertEquals($token->issued->getTimestamp() + 1, $token->expires->getTimestamp()); + } + public function testCreateTokenWithValidityAsDate() { $expectedValidity = new DateTime("2030-02-01 12:00:00"); @@ -298,38 +313,39 @@ public function testSignTokenWithMinimalHeader() $this->assertEquals($expectedSignedToken, $actualSignedToken); } - // public function testParseTokenWithAllFields() - // { - // $tokenString = "eyJ0eXAiOiJRVDEiLCJlbmMiOiJBRVMyNTYiLCJpc3MiOiIxNzYxMDUxODYzMDAxIiwiZXhwIjoiMTc5MjU4NzU5MzAwMCIsInRpIjoiNzBjYzQwMTEtODUwNS00NjA0LWFkYmItY2NjNThiMzI5ZWQyIiwiYyI6InRpY2tldGFuaWEiLCJlIjoiYnV5dGlja2V0dG9rZW4iLCJpcCI6IjgyLjE5Mi4xNzMuMzgiLCJ4ZmYiOiI4OC4xOTIuMTczLjg4In0.qOaYmEZ0KQEjLKPgj2X5XfdbZCpQ1tamDJZaP_hxOXTS6IxEV9fa7_pgQQopPEvsqUd7_VPLMhJRN3u6iKCgisWMO7UlLy1kkUqR8eP1k4QBBFOwiD_KIpfXGcul9yIHjvzQAxWgz9pF-qsmhQQjvg.ZGq8lnP3P92FEkQs3OPoxk--ED77QGs7fAm_QPKNXCM"; + public function testParseTokenWithAllFields() + { + $tokenString = "eyJ0eXAiOiJRVDEiLCJlbmMiOiJBRVMyNTYiLCJpc3MiOiIxNzYxMDUxODYzMDAxIiwiZXhwIjoiMTc5MjU4NzU5MzAwMCIsInRpIjoiNzBjYzQwMTEtODUwNS00NjA0LWFkYmItY2NjNThiMzI5ZWQyIiwiYyI6InRpY2tldGFuaWEiLCJlIjoiYnV5dGlja2V0dG9rZW4iLCJpcCI6IjgyLjE5Mi4xNzMuMzgiLCJ4ZmYiOiI4OC4xOTIuMTczLjg4In0.qOaYmEZ0KQEjLKPgj2X5XfdbZCpQ1tamDJZaP_hxOXTS6IxEV9fa7_pgQQopPEvsqUd7_VPLMhJRN3u6iKCgisWMO7UlLy1kkUqR8eP1k4QBBFOwiD_KIpfXGcul9yIHjvzQAxWgz9pF-qsmhQQjvg.ZGq8lnP3P92FEkQs3OPoxk--ED77QGs7fAm_QPKNXCM"; - // $enqueueToken = Token::parse($tokenString, SampleTokenValues::$secretKey); + $enqueueToken = Token::parse($tokenString, SampleTokenValues::$secretKey); - // // Validate parsed token - // $this->assertEquals('70cc4011-8505-4604-adbb-ccc58b329ed2', $enqueueToken->getTokenIdentifier()); - // $this->assertEquals(SampleTokenValues::$customerId, $enqueueToken->customerId); - // $this->assertEquals('buytickettoken', $enqueueToken->eventId); - // $this->assertEquals('82.192.173.38', $enqueueToken->ipAddress); - // $this->assertEquals('88.192.173.88', $enqueueToken->xForwardedFor); - // $this->assertInstanceOf(DateTime::class, $enqueueToken->issued); - // $this->assertInstanceOf(DateTime::class, $enqueueToken->expires); - // $this->assertEquals(TokenVersion::QT1, $enqueueToken->tokenVersion); - // $this->assertEquals(EncryptionType::AES256, $enqueueToken->encryption); + // Validate parsed token + $this->assertEquals('70cc4011-8505-4604-adbb-ccc58b329ed2', $enqueueToken->getTokenIdentifier()); + $this->assertEquals(SampleTokenValues::$customerId, $enqueueToken->customerId); + $this->assertEquals('buytickettoken', $enqueueToken->eventId); + $this->assertEquals('82.192.173.38', $enqueueToken->ipAddress); + $this->assertEquals('88.192.173.88', $enqueueToken->xForwardedFor); + $this->assertInstanceOf(DateTime::class, $enqueueToken->issued); + $this->assertInstanceOf(DateTime::class, $enqueueToken->expires); + $this->assertEquals(TokenVersion::QT1, $enqueueToken->tokenVersion); + $this->assertEquals(EncryptionType::AES256, $enqueueToken->encryption); - // $this->assertNotNull($enqueueToken->getPayload()); - // $payload = $enqueueToken->getPayload(); - // $this->assertEquals('V1', $payload->getCustomData()['K1']); - // $this->assertEquals('V2', $payload->getCustomData()['K2']); - // $this->assertEquals(0.34, $payload->getRelativeQuality()); - // $this->assertEquals('Connector', $payload->getTokenOrigin()); + $this->assertNotNull($enqueueToken->getPayload()); + $payload = $enqueueToken->getPayload(); + $this->assertEquals('V1', $payload->getCustomData()['K1']); + $this->assertEquals('V2', $payload->getCustomData()['K2']); + $this->assertEquals(0.34, $payload->getRelativeQuality()); + $this->assertEquals('Connector', $payload->getTokenOrigin()); - // $this->assertEquals('Key_3a76149d-3834-48d6-962a-d3362416854a', $payload->getKey()); + $this->assertEquals('Key_3a76149d-3834-48d6-962a-d3362416854a', $payload->getKey()); - // $this->assertEquals('2025-10-21 13:04:23', $enqueueToken->issued->format('Y-m-d H:i:s')); - // $this->assertEquals('2026-10-21 12:59:53', $enqueueToken->expires->format('Y-m-d H:i:s')); + // The header carries "iss" with millisecond precision, which DateTime only parses from PHP 8.0 + $this->assertEquals('2025-10-21 13:04:23.001', $enqueueToken->issued->format('Y-m-d H:i:s.v')); + $this->assertEquals('2026-10-21 12:59:53', $enqueueToken->expires->format('Y-m-d H:i:s')); - // $this->assertNotEmpty($enqueueToken->getTokenWithoutHash()); - // $this->assertEquals('ZGq8lnP3P92FEkQs3OPoxk--ED77QGs7fAm_QPKNXCM', $enqueueToken->getHashCode()); - // } + $this->assertNotEmpty($enqueueToken->getTokenWithoutHash()); + $this->assertEquals('ZGq8lnP3P92FEkQs3OPoxk--ED77QGs7fAm_QPKNXCM', $enqueueToken->getHashCode()); + } public function testCreateTokenAndParse() { @@ -361,9 +377,6 @@ public function testCreateTokenAndParse() $tokenString = $token->getToken(); - // Use this to verify manually if needed - echo "Generated token: " . $tokenString . PHP_EOL; - $parsed = Token::parse($tokenString, $secret); $this->assertEquals($token->getTokenIdentifier(), $parsed->getTokenIdentifier()); @@ -389,4 +402,208 @@ public function testSecondTokenGenerationDoesNotModifyFirstToken() $parsedToken1 = Token::parse($token1String, SampleTokenValues::$secretKey); $this->assertEquals("event1", $parsedToken1->eventId); } + + public function testCreateTokenWithInviteOnlyOriginAndParse() + { + $secret = SampleTokenValues::$secretKey; + + $payload = Payload::enqueue() + ->withKey('TestKey') + ->withRelativeQuality(0.5) + ->withOrigin(TokenOrigin::INVITE_ONLY) + ->generate(); + + $token = Token::enqueue(SampleTokenValues::$customerId) + ->withPayload($payload) + ->withEventId('testevent') + ->generate($secret); + + $tokenString = $token->getToken(); + $parsed = Token::parse($tokenString, $secret); + + $this->assertNotNull($parsed->getPayload()); + $this->assertEquals(TokenOrigin::INVITE_ONLY, $parsed->getPayload()->getTokenOrigin()); + $this->assertEquals('TestKey', $parsed->getPayload()->getKey()); + $this->assertEquals(0.5, $parsed->getPayload()->getRelativeQuality()); + } + + public function testCreateTokenWithAkamaiBotManagerOriginAndParse() + { + $secret = SampleTokenValues::$secretKey; + + $payload = Payload::enqueue() + ->withKey('BotManagerKey') + ->withRelativeQuality(0.75) + ->withCustomData('detector', 'bot-manager') + ->withOrigin(TokenOrigin::AKAMAI_BOT_MANAGER_HEADER_VALIDATOR) + ->generate(); + + $token = Token::enqueue(SampleTokenValues::$customerId) + ->withPayload($payload) + ->withEventId('bot-test-event') + ->generate($secret); + + $tokenString = $token->getToken(); + $parsed = Token::parse($tokenString, $secret); + + $this->assertNotNull($parsed->getPayload()); + $this->assertEquals(TokenOrigin::AKAMAI_BOT_MANAGER_HEADER_VALIDATOR, $parsed->getPayload()->getTokenOrigin()); + $this->assertEquals('BotManagerKey', $parsed->getPayload()->getKey()); + $this->assertEquals(0.75, $parsed->getPayload()->getRelativeQuality()); + $this->assertEquals('bot-manager', $parsed->getPayload()->getCustomData()['detector']); + } + + public function testCreateTokenWithConnectorOriginAndParse() + { + $secret = SampleTokenValues::$secretKey; + + $payload = Payload::enqueue() + ->withKey('ConnectorKey') + ->withRelativeQuality(0.25) + ->withOrigin(TokenOrigin::CONNECTOR) + ->generate(); + + $token = Token::enqueue(SampleTokenValues::$customerId) + ->withPayload($payload) + ->withEventId('connector-event') + ->generate($secret); + + $tokenString = $token->getToken(); + $parsed = Token::parse($tokenString, $secret); + + $this->assertNotNull($parsed->getPayload()); + $this->assertEquals(TokenOrigin::CONNECTOR, $parsed->getPayload()->getTokenOrigin()); + $this->assertEquals('ConnectorKey', $parsed->getPayload()->getKey()); + $this->assertEquals(0.25, $parsed->getPayload()->getRelativeQuality()); + } + + public function testTokenOriginDefaultsToConnector() + { + $secret = SampleTokenValues::$secretKey; + + $payload = Payload::enqueue() + ->withKey('DefaultKey') + ->generate(); + + $token = Token::enqueue(SampleTokenValues::$customerId) + ->withPayload($payload) + ->generate($secret); + + $tokenString = $token->getToken(); + $parsed = Token::parse($tokenString, $secret); + + $this->assertNotNull($parsed->getPayload()); + $this->assertEquals(TokenOrigin::CONNECTOR, $parsed->getPayload()->getTokenOrigin()); + } + + public function testParseRejectsEmptySecretKey() + { + $this->expectException(ArgumentException::class); + $this->expectExceptionMessage("Invalid secret key"); + + Token::parse(SampleTokenValues::$testToken, ""); + } + + public function testParseRejectsEmptyToken() + { + $this->expectException(ArgumentException::class); + $this->expectExceptionMessage("Invalid token"); + + Token::parse("", SampleTokenValues::$secretKey); + } + + public function testParseRejectsTokenWithoutHeader() + { + $tokenParts = explode(".", SampleTokenValues::$testToken); + + $this->expectException(ArgumentException::class); + $this->expectExceptionMessage("Invalid token"); + + Token::parse("." . $tokenParts[1] . "." . $tokenParts[2], SampleTokenValues::$secretKey); + } + + public function testParseRejectsTokenWithoutHash() + { + $tokenParts = explode(".", SampleTokenValues::$testToken); + + $this->expectException(ArgumentException::class); + $this->expectExceptionMessage("Invalid token"); + + Token::parse($tokenParts[0] . "." . $tokenParts[1], SampleTokenValues::$secretKey); + } + + public function testParseRejectsTokenWithOnlyAHeader() + { + $tokenParts = explode(".", SampleTokenValues::$testToken); + + $this->expectException(ArgumentException::class); + $this->expectExceptionMessage("Invalid token"); + + Token::parse($tokenParts[0], SampleTokenValues::$secretKey); + } + + public function testParseRejectsTamperedToken() + { + $tokenParts = explode(".", SampleTokenValues::$testToken); + $tamperedHeader = SignedTokens::encodeHeader([ + 'typ' => TokenVersion::QT1, + 'enc' => EncryptionType::AES256, + 'iss' => 1534723200000, + 'ti' => 'a21d423a-43fd-4821-84fa-4390f6a2fd3e', + 'c' => 'another-customer', + ]); + + $this->expectException(InvalidHashException::class); + $this->expectExceptionMessage("The token hash is invalid"); + + Token::parse($tamperedHeader . "." . $tokenParts[1] . "." . $tokenParts[2], SampleTokenValues::$secretKey); + } + + public function testParseRejectsTokenSignedWithAnotherSecretKey() + { + $this->expectException(InvalidHashException::class); + + Token::parse(SampleTokenValues::$testToken, "another-secret-key"); + } + + public function testParseWrapsInvalidHeaderInDeserializationException() + { + $tokenString = SignedTokens::sign(SignedTokens::encodeHeader(['typ' => TokenVersion::QT1]) . "."); + + try { + Token::parse($tokenString, SampleTokenValues::$secretKey); + $this->fail("Expected a TokenDeserializationException"); + } catch (TokenDeserializationException $ex) { + $this->assertEquals("Unable to deserialize token", $ex->getMessage()); + $this->assertInstanceOf(ArgumentException::class, $ex->internalException); + } + } + + public function testParseWrapsUndecryptablePayloadInDeserializationException() + { + try { + Token::parse(SignedTokens::withUndecryptablePayload(), SampleTokenValues::$secretKey); + $this->fail("Expected a TokenDeserializationException"); + } catch (TokenDeserializationException $ex) { + $this->assertEquals("Unable to deserialize token", $ex->getMessage()); + $this->assertEquals("Decryption failed.", $ex->internalException->getMessage()); + } + } + + public function testGenerateWrapsPayloadFailureInSerializationException() + { + $payloadFailure = new \Exception("payload failure"); + $payload = $this->createStub(IEnqueueTokenPayload::class); + $payload->method('encryptAndEncode')->willThrowException($payloadFailure); + + try { + Token::enqueue(SampleTokenValues::$customerId) + ->withPayload($payload) + ->generate(SampleTokenValues::$secretKey); + $this->fail("Expected a TokenSerializationException"); + } catch (TokenSerializationException $ex) { + $this->assertEquals("Exception serializing token", $ex->getMessage()); + $this->assertSame($payloadFailure, $ex->internalException); + } + } } diff --git a/tests/Helpers/AESEncryptionTest.php b/tests/Helpers/AESEncryptionTest.php index e962c76..c9577b7 100644 --- a/tests/Helpers/AESEncryptionTest.php +++ b/tests/Helpers/AESEncryptionTest.php @@ -40,6 +40,17 @@ public function testShouldDecryptAsciiText() $this->assertEquals($originalText, $decryptedText); } + public function testShouldThrowWhenDataIsNotACipherText() + { + $keyString = "123456789012345678901234567890123456789012345678901234567890123"; + $tokenIdentifier = "1234567890123456"; + + $this->expectException(\Exception::class); + $this->expectExceptionMessage("Decryption failed."); + + AESEncryption::decryptPayload($keyString, $tokenIdentifier, "not a cipher block"); + } + public function testMD5IVGeneration() { // Test MD5 IV generation (PHP-specific implementation detail with real value) diff --git a/tests/SecretKeyRedactionTest.php b/tests/SecretKeyRedactionTest.php new file mode 100644 index 0000000..f117da2 --- /dev/null +++ b/tests/SecretKeyRedactionTest.php @@ -0,0 +1,112 @@ +previousExceptionIgnoreArgs = ini_set('zend.exception_ignore_args', '0'); + } + + protected function tearDown(): void + { + if ($this->previousExceptionIgnoreArgs !== false) { + ini_set('zend.exception_ignore_args', $this->previousExceptionIgnoreArgs); + } + } + + public function testSecretKeyIsRedactedFromParseStackTraces() + { + try { + Token::parse(SignedTokens::withUndecryptablePayload(), SampleTokenValues::$secretKey); + $this->fail("Expected a TokenDeserializationException"); + } catch (TokenDeserializationException $ex) { + $this->assertSecretKeyRedacted($ex); + $this->assertSecretKeyRedacted($ex->internalException); + } + } + + public function testSecretKeyIsRedactedFromGenerateStackTraces() + { + $payload = $this->createStub(IEnqueueTokenPayload::class); + $payload->method('encryptAndEncode')->willThrowException(new \Exception("payload failure")); + + try { + Token::enqueue(SampleTokenValues::$customerId) + ->withPayload($payload) + ->generate(SampleTokenValues::$secretKey); + $this->fail("Expected a TokenSerializationException"); + } catch (TokenSerializationException $ex) { + $this->assertSecretKeyRedacted($ex); + } + } + + public static function secretKeyParameters(): array + { + $parameters = []; + $sourceDirectory = dirname(__DIR__) . '/src'; + $files = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($sourceDirectory)); + + foreach ($files as $file) { + if ($file->getExtension() !== 'php') { + continue; + } + + $relativePath = substr($file->getPathname(), strlen($sourceDirectory) + 1, -strlen('.php')); + $class = new \ReflectionClass('QueueIT\\QueueToken\\' . str_replace('/', '\\', $relativePath)); + foreach ($class->getMethods() as $method) { + foreach ($method->getParameters() as $parameter) { + if ($parameter->getName() === 'secretKey') { + $parameters[$class->getName() . '::' . $method->getName()] = [$parameter]; + } + } + } + } + + return $parameters; + } + + /** + * @dataProvider secretKeyParameters + */ + public function testEverySecretKeyParameterIsSensitive(\ReflectionParameter $parameter) + { + $this->assertNotEmpty($parameter->getAttributes(\SensitiveParameter::class)); + } + + private function assertSecretKeyRedacted(\Throwable $ex): void + { + $redactedArguments = 0; + + foreach ($ex->getTrace() as $frame) { + $class = $frame['class'] ?? ''; + if (!str_starts_with($class, 'QueueIT\\QueueToken\\') || str_starts_with($class, 'QueueIT\\QueueToken\\Tests\\')) { + continue; + } + + foreach ($frame['args'] ?? [] as $argument) { + $this->assertNotSame( + SampleTokenValues::$secretKey, + $argument, + "The secret key is in the stack trace of {$class}::{$frame['function']}" + ); + if ($argument instanceof \SensitiveParameterValue) { + $redactedArguments++; + } + } + } + + $this->assertGreaterThan(0, $redactedArguments, "No redacted argument in the stack trace"); + } +} diff --git a/tests/SignedTokens.php b/tests/SignedTokens.php new file mode 100644 index 0000000..ee309f5 --- /dev/null +++ b/tests/SignedTokens.php @@ -0,0 +1,41 @@ + TokenVersion::QT1, + 'enc' => EncryptionType::AES256, + 'iss' => 1534723200000, + 'ti' => 'a21d423a-43fd-4821-84fa-4390f6a2fd3e', + 'c' => SampleTokenValues::$customerId, + ]); + + return self::sign($header . "." . Base64UrlEncoding::encode("not a cipher block")); + } +}