Repository: lcobucci/jwt Branch: 6.0.x Commit: 9ffc631092e5 Files: 201 Total size: 378.5 KB Directory structure: gitextract_h82sgh5a/ ├── .composer-require-checker.config.json ├── .gitattributes ├── .github/ │ ├── CONTRIBUTING.md │ ├── FUNDING.yml │ ├── SECURITY.md │ └── workflows/ │ ├── backwards-compatibility.yml │ ├── benchmarks.yml │ ├── coding-standards.yml │ ├── composer-json-lint.yml │ ├── mutation-tests.yml │ ├── phpunit.yml │ ├── release-on-milestone-closed.yml │ └── static-analysis.yml ├── .gitignore ├── .readthedocs.yaml ├── .roave-backward-compatibility-check.json ├── LICENSE ├── Makefile ├── README.md ├── composer.json ├── docs/ │ ├── configuration.md │ ├── extending-the-library.md │ ├── index.md │ ├── installation.md │ ├── issuing-tokens.md │ ├── parsing-tokens.md │ ├── quick-start.md │ ├── rotating-keys.md │ ├── supported-algorithms.md │ ├── upgrading.md │ └── validating-tokens.md ├── infection.json.dist ├── mkdocs.yml ├── phpbench.json ├── phpcs.xml.dist ├── phpstan.neon.dist ├── phpunit.xml.dist ├── renovate.json ├── src/ │ ├── Builder.php │ ├── ClaimsFormatter.php │ ├── Configuration.php │ ├── Decoder.php │ ├── Encoder.php │ ├── Encoding/ │ │ ├── CannotDecodeContent.php │ │ ├── CannotEncodeContent.php │ │ ├── ChainedFormatter.php │ │ ├── JoseEncoder.php │ │ ├── MicrosecondBasedDateConversion.php │ │ ├── UnifyAudience.php │ │ └── UnixTimestampDates.php │ ├── Exception.php │ ├── JwtFacade.php │ ├── Parser.php │ ├── Signer/ │ │ ├── Blake2b.php │ │ ├── CannotSignPayload.php │ │ ├── Ecdsa/ │ │ │ ├── ConversionFailed.php │ │ │ ├── MultibyteStringConverter.php │ │ │ ├── Sha256.php │ │ │ ├── Sha384.php │ │ │ ├── Sha512.php │ │ │ └── SignatureConverter.php │ │ ├── Ecdsa.php │ │ ├── Eddsa.php │ │ ├── Hmac/ │ │ │ ├── Sha256.php │ │ │ ├── Sha384.php │ │ │ └── Sha512.php │ │ ├── Hmac.php │ │ ├── InvalidKeyProvided.php │ │ ├── Key/ │ │ │ ├── FileCouldNotBeRead.php │ │ │ └── InMemory.php │ │ ├── Key.php │ │ ├── OpenSSL.php │ │ ├── Rsa/ │ │ │ ├── Sha256.php │ │ │ ├── Sha384.php │ │ │ └── Sha512.php │ │ └── Rsa.php │ ├── Signer.php │ ├── SodiumBase64Polyfill.php │ ├── Token/ │ │ ├── Builder.php │ │ ├── DataSet.php │ │ ├── InvalidTokenStructure.php │ │ ├── Parser.php │ │ ├── Plain.php │ │ ├── RegisteredClaimGiven.php │ │ ├── RegisteredClaims.php │ │ ├── Signature.php │ │ └── UnsupportedHeaderFound.php │ ├── Token.php │ ├── UnencryptedToken.php │ ├── Validation/ │ │ ├── Constraint/ │ │ │ ├── CannotValidateARegisteredClaim.php │ │ │ ├── HasClaim.php │ │ │ ├── HasClaimWithValue.php │ │ │ ├── IdentifiedBy.php │ │ │ ├── IssuedBy.php │ │ │ ├── LeewayCannotBeNegative.php │ │ │ ├── LooseValidAt.php │ │ │ ├── PermittedFor.php │ │ │ ├── RelatedTo.php │ │ │ ├── SignedWith.php │ │ │ ├── SignedWithOneInSet.php │ │ │ ├── SignedWithUntilDate.php │ │ │ └── StrictValidAt.php │ │ ├── Constraint.php │ │ ├── ConstraintViolation.php │ │ ├── NoConstraintsGiven.php │ │ ├── RequiredConstraintsViolated.php │ │ ├── SignedWith.php │ │ ├── ValidAt.php │ │ └── Validator.php │ └── Validator.php └── tests/ ├── Benchmark/ │ ├── AlgorithmsBench.php │ ├── CreateSignatureBench.php │ ├── Ecdsa/ │ │ ├── private-256.key │ │ ├── private-384.key │ │ ├── private-521.key │ │ ├── public-256.key │ │ ├── public-384.key │ │ └── public-521.key │ ├── IssueTokenBench.php │ ├── ParseTokenBench.php │ ├── Rsa/ │ │ ├── private.key │ │ └── public.key │ └── VerifySignatureBench.php ├── ConfigurationTest.php ├── ES512TokenTest.php ├── EcdsaTokenTest.php ├── EddsaTokenTest.php ├── Encoding/ │ ├── ChainedFormatterTest.php │ ├── JoseEncoderTest.php │ ├── MicrosecondBasedDateConversionTest.php │ ├── UnifyAudienceTest.php │ └── UnixTimestampDatesTest.php ├── HmacTokenTest.php ├── JwtFacadeTest.php ├── KeyDumpSigner.php ├── Keys.php ├── MaliciousTamperingPreventionTest.php ├── RFC6978VectorTest.php ├── RsaTokenTest.php ├── Signer/ │ ├── Blake2bTest.php │ ├── Ecdsa/ │ │ ├── EcdsaTestCase.php │ │ ├── MultibyteStringConverterTest.php │ │ ├── Sha256Test.php │ │ ├── Sha384Test.php │ │ └── Sha512Test.php │ ├── EddsaTest.php │ ├── FakeSigner.php │ ├── Hmac/ │ │ ├── HmacTestCase.php │ │ ├── Sha256Test.php │ │ ├── Sha384Test.php │ │ └── Sha512Test.php │ ├── Key/ │ │ ├── InMemoryTest.php │ │ ├── empty.pem │ │ └── test.pem │ └── Rsa/ │ ├── KeyValidationSigner.php │ ├── KeyValidationTest.php │ ├── RsaTestCase.php │ ├── Sha256Test.php │ ├── Sha384Test.php │ └── Sha512Test.php ├── SodiumBase64PolyfillTest.php ├── TimeFractionPrecisionTest.php ├── Token/ │ ├── BuilderTest.php │ ├── DataSetTest.php │ ├── ParserTest.php │ ├── PlainTest.php │ └── SignatureTest.php ├── UnsignedTokenTest.php ├── UnsupportedParser.php ├── Validation/ │ ├── Constraint/ │ │ ├── ConstraintTestCase.php │ │ ├── HasClaimTest.php │ │ ├── HasClaimWithValueTest.php │ │ ├── IdentifiedByTest.php │ │ ├── IssuedByTest.php │ │ ├── LooseValidAtTest.php │ │ ├── PermittedForTest.php │ │ ├── RelatedToTest.php │ │ ├── SignedWithOneInSetTest.php │ │ ├── SignedWithTest.php │ │ ├── SignedWithUntilDateTest.php │ │ ├── StrictValidAtTest.php │ │ └── ValidAtTestCase.php │ ├── ConstraintViolationTest.php │ ├── RequiredConstraintsViolatedTest.php │ └── ValidatorTest.php └── _keys/ ├── ecdsa/ │ ├── private.key │ ├── private2.key │ ├── private_ec384.key │ ├── private_ec512.key │ ├── public1.key │ ├── public2.key │ ├── public2_ec512.key │ ├── public3.key │ ├── public_ec384.key │ └── public_ec512.key └── rsa/ ├── encrypted-private.key ├── encrypted-public.key ├── private.key ├── private_512.key ├── public.key └── public_512.key ================================================ FILE CONTENTS ================================================ ================================================ FILE: .composer-require-checker.config.json ================================================ { "symbol-whitelist" : [ "NoDiscard" ], "php-core-extensions" : [ "Core", "date", "json", "hash", "SPL", "standard" ] } ================================================ FILE: .gitattributes ================================================ /docs export-ignore /tests export-ignore /.gitattributes export-ignore /.github export-ignore /.gitignore export-ignore /.composer-require-checker.config.json export-ignore /*.yml export-ignore /CONTRIBUTING.md export-ignore /*.dist export-ignore /phpbench.json export-ignore /composer.lock export-ignore /README.md export-ignore /Makefile export-ignore /.roave-backward-compatibility-check.json export-ignore /.readthedocs.yaml export-ignore /renovate.json export-ignore ================================================ FILE: .github/CONTRIBUTING.md ================================================ # Contributing to lcobucci/jwt First off, thanks for taking the time to contribute! ## Reporting issues We accept bug and feature requests via issues created [here](https://github.com/lcobucci/jwt/issues). ### Prior to submitting a bug report - **Always search the issue or pull request list first** - The odds are good that if you've found a problem, someone else has found it, too; - **Always try the [master](https://github.com/lcobucci/jwt) branch** - to see if the reported bug has not already been fixed. ## Pull Requests We accept contributions via pull requests [here](https://github.com/lcobucci/jwt/pulls). - Follow [PSR-2 coding standards](http://www.php-fig.org/psr/psr-2); - Follow [PSR-4 autoloading standards](http://www.php-fig.org/psr/psr-4); - Follow [semver](http://semver.org); - Add tests (everything MUST be well tested); - Improve documentation (don't forget to update README.md); - Create topic branches (don't send a PR from your master); - One pull request per feature; - Send coherent history by rebasing your work before submitting; ### Running tests locally We provide a GNU-Make configuration that allows you to run the CI checks locally with a single command: `make`. ### Branches - **5.0.x**: used to the next major release (new features that breaks BC) - **4.4.x**: used to develop migration path for the next version - **4.3.x**: used to fix bugs - **4.2.x**: unmaintained - **4.1.x**: unmaintained - **4.0.x**: unmaintained - **3.4.x**: security issues only - **3.3**: unmaintained - **3.2**: unmaintained - **3.1**: unmaintained - **3.0**: unmaintained - **2.1**: unmaintained **Thank you and happy coding!** @lcobucci ================================================ FILE: .github/FUNDING.yml ================================================ github: lcobucci patreon: lcobucci ================================================ FILE: .github/SECURITY.md ================================================ # Security Policy ## Supported Versions | Version | Supported | | ----------- | ------------------ | | 4.2.x-dev | :white_check_mark: | | 4.1.x | :white_check_mark: | | 4.0.x | :white_check_mark: | | 3.4 | :white_check_mark: | | < 3.4 | :x: | ## Reporting a Vulnerability In case of a security vulnerability, please file an issue. It's shall be prioritised and ported back/forth to all supported versions. ================================================ FILE: .github/workflows/backwards-compatibility.yml ================================================ name: "Backwards compatibility check" on: pull_request: jobs: bc-check: name: "Backwards compatibility check" runs-on: "ubuntu-latest" steps: - name: "Checkout" uses: "actions/checkout@v6.0.2" with: fetch-depth: 0 - name: "Install PHP" uses: "shivammathur/setup-php@2.37.0" with: php-version: "8.3" ini-values: memory_limit=-1 tools: composer:v2, cs2pr env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Get composer cache directory id: composer-cache run: echo "composer_cache_dir=$(composer global config cache-files-dir)" >> $GITHUB_OUTPUT - name: "Cache dependencies" uses: "actions/cache@v5.0.4" with: path: ${{ steps.composer-cache.outputs.composer_cache_dir }} key: "php-8.3-bc-break-check-${{ hashFiles('.github/workflows/backwards-compatibility.yml') }}" restore-keys: "php-8.3-bc-break-check-" - name: "Install dependencies" run: composer global require roave/backward-compatibility-check - name: "BC Check" run: | ~/.composer/vendor/bin/roave-backward-compatibility-check --from=${{ github.event.pull_request.base.sha }} --format=github-actions ================================================ FILE: .github/workflows/benchmarks.yml ================================================ name: "Benchmarks" on: pull_request: push: jobs: benchmarks: name: "Run benchmarks" runs-on: ${{ matrix.operating-system }} strategy: matrix: dependencies: - "locked" php-version: - "8.3" operating-system: - "ubuntu-latest" steps: - name: "Checkout" uses: "actions/checkout@v6.0.2" - name: "Install PHP" uses: "shivammathur/setup-php@2.37.0" with: coverage: "none" php-version: "${{ matrix.php-version }}" ini-values: memory_limit=-1 tools: composer:v2, cs2pr - name: "Install dependencies" uses: "ramsey/composer-install@4.0.0" with: dependency-versions: "${{ matrix.dependencies }}" - name: "PhpBench" run: "make phpbench" ================================================ FILE: .github/workflows/coding-standards.yml ================================================ name: "Check Coding Standards" on: pull_request: push: jobs: coding-standards: name: "Check Coding Standards" runs-on: ${{ matrix.operating-system }} strategy: matrix: dependencies: - "locked" php-version: - "8.3" operating-system: - "ubuntu-latest" steps: - name: "Checkout" uses: "actions/checkout@v6.0.2" - name: "Install PHP" uses: "shivammathur/setup-php@2.37.0" with: coverage: "none" php-version: "${{ matrix.php-version }}" ini-values: memory_limit=-1 tools: composer:v2, cs2pr - name: "Install dependencies" uses: "ramsey/composer-install@4.0.0" with: dependency-versions: "${{ matrix.dependencies }}" - name: "Coding Standard" run: "make phpcs PHPCS_FLAGS='-q --report=checkstyle | cs2pr'" ================================================ FILE: .github/workflows/composer-json-lint.yml ================================================ name: "Lint composer.json" on: pull_request: push: jobs: coding-standards: name: "Lint composer.json" runs-on: ${{ matrix.operating-system }} strategy: matrix: dependencies: - "highest" php-version: - "8.3" operating-system: - "ubuntu-latest" steps: - name: "Checkout" uses: "actions/checkout@v6.0.2" - name: "Install PHP" uses: "shivammathur/setup-php@2.37.0" with: coverage: "none" php-version: "${{ matrix.php-version }}" ini-values: memory_limit=-1 tools: composer:v2, composer-normalize, composer-require-checker, composer-unused - name: "Install dependencies" uses: "ramsey/composer-install@4.0.0" with: dependency-versions: "${{ matrix.dependencies }}" - name: "Validate composer.json" run: "composer validate --strict" - name: "Normalize composer.json" run: "composer-normalize --dry-run" - name: "Check composer.json explicit dependencies" run: "composer-require-checker check --config-file=.composer-require-checker.config.json" # - name: "Check composer.json unused dependencies" # run: "composer-unused" ================================================ FILE: .github/workflows/mutation-tests.yml ================================================ name: "Mutation tests" on: pull_request: push: jobs: mutation-tests: name: "Mutation tests" runs-on: ${{ matrix.operating-system }} strategy: matrix: dependencies: - "locked" php-version: - "8.3" operating-system: - "ubuntu-latest" steps: - name: "Checkout" uses: "actions/checkout@v6.0.2" - name: "Install PHP" uses: "shivammathur/setup-php@2.37.0" with: coverage: "xdebug" php-version: "${{ matrix.php-version }}" ini-values: memory_limit=-1 tools: composer:v2, cs2pr - name: "Install dependencies" uses: "ramsey/composer-install@4.0.0" with: dependency-versions: "${{ matrix.dependencies }}" - name: "Infection" run: "make infection PHPUNIT_FLAGS=--coverage-clover=coverage.xml INFECTION_FLAGS=--logger-github" - name: "Upload Code Coverage" uses: "codecov/codecov-action@v5.5.3" ================================================ FILE: .github/workflows/phpunit.yml ================================================ name: "PHPUnit tests" on: pull_request: push: jobs: phpunit: name: "PHPUnit tests" runs-on: ${{ matrix.operating-system }} strategy: matrix: dependencies: - "lowest" - "highest" - "locked" php-version: - "8.3" - "8.4" - "8.5" operating-system: - "ubuntu-latest" steps: - name: "Checkout" uses: "actions/checkout@v6.0.2" - name: "Install PHP" uses: "shivammathur/setup-php@2.37.0" with: coverage: "none" php-version: "${{ matrix.php-version }}" ini-values: memory_limit=-1 tools: composer:v2, cs2pr - name: "Install dependencies" uses: "ramsey/composer-install@4.0.0" with: dependency-versions: "${{ matrix.dependencies }}" - name: "Tests" run: "make phpunit" ================================================ FILE: .github/workflows/release-on-milestone-closed.yml ================================================ # https://help.github.com/en/categories/automating-your-workflow-with-github-actions name: "Automatic Releases" on: milestone: types: - "closed" jobs: release: name: "GIT tag, release & create merge-up PR" runs-on: ubuntu-latest steps: - name: "Checkout" uses: "actions/checkout@v6.0.2" - name: "Release" uses: "laminas/automatic-releases@1.26.2" with: command-name: "laminas:automatic-releases:release" env: "SHELL_VERBOSITY": "3" "GITHUB_TOKEN": ${{ secrets.ORGANIZATION_ADMIN_TOKEN }} "SIGNING_SECRET_KEY": ${{ secrets.SIGNING_SECRET_KEY }} "GIT_AUTHOR_NAME": ${{ secrets.GIT_AUTHOR_NAME }} "GIT_AUTHOR_EMAIL": ${{ secrets.GIT_AUTHOR_EMAIL }} - name: "Create Merge-Up Pull Request" uses: "laminas/automatic-releases@1.26.2" with: command-name: "laminas:automatic-releases:create-merge-up-pull-request" env: "SHELL_VERBOSITY": "3" "GITHUB_TOKEN": ${{ secrets.GITHUB_TOKEN }} "SIGNING_SECRET_KEY": ${{ secrets.SIGNING_SECRET_KEY }} "GIT_AUTHOR_NAME": ${{ secrets.GIT_AUTHOR_NAME }} "GIT_AUTHOR_EMAIL": ${{ secrets.GIT_AUTHOR_EMAIL }} - name: "Create and/or Switch to new Release Branch" uses: "laminas/automatic-releases@1.26.2" with: command-name: "laminas:automatic-releases:switch-default-branch-to-next-minor" env: "SHELL_VERBOSITY": "3" "GITHUB_TOKEN": ${{ secrets.ORGANIZATION_ADMIN_TOKEN }} "SIGNING_SECRET_KEY": ${{ secrets.SIGNING_SECRET_KEY }} "GIT_AUTHOR_NAME": ${{ secrets.GIT_AUTHOR_NAME }} "GIT_AUTHOR_EMAIL": ${{ secrets.GIT_AUTHOR_EMAIL }} - name: "Bump Changelog Version On Originating Release Branch" uses: "laminas/automatic-releases@1.26.2" with: command-name: "laminas:automatic-releases:bump-changelog" env: "SHELL_VERBOSITY": "3" "GITHUB_TOKEN": ${{ secrets.GITHUB_TOKEN }} "SIGNING_SECRET_KEY": ${{ secrets.SIGNING_SECRET_KEY }} "GIT_AUTHOR_NAME": ${{ secrets.GIT_AUTHOR_NAME }} "GIT_AUTHOR_EMAIL": ${{ secrets.GIT_AUTHOR_EMAIL }} - name: "Create new milestones" uses: "laminas/automatic-releases@1.26.2" with: command-name: "laminas:automatic-releases:create-milestones" env: "SHELL_VERBOSITY": "3" "GITHUB_TOKEN": ${{ secrets.GITHUB_TOKEN }} "SIGNING_SECRET_KEY": ${{ secrets.SIGNING_SECRET_KEY }} "GIT_AUTHOR_NAME": ${{ secrets.GIT_AUTHOR_NAME }} "GIT_AUTHOR_EMAIL": ${{ secrets.GIT_AUTHOR_EMAIL }} ================================================ FILE: .github/workflows/static-analysis.yml ================================================ name: "Static Analysis" on: pull_request: push: jobs: static-analysis: name: "Static Analysis" runs-on: ${{ matrix.operating-system }} strategy: matrix: dependencies: - "locked" php-version: - "8.3" operating-system: - "ubuntu-latest" steps: - name: "Checkout" uses: "actions/checkout@v6.0.2" - name: "Install PHP" uses: "shivammathur/setup-php@2.37.0" with: coverage: "none" php-version: "${{ matrix.php-version }}" ini-values: memory_limit=-1 tools: composer:v2, cs2pr - name: "Install dependencies" uses: "ramsey/composer-install@4.0.0" with: dependency-versions: "${{ matrix.dependencies }}" - name: "PHPStan" run: "make phpstan" ================================================ FILE: .gitignore ================================================ vendor phpunit.xml infection.txt coverage phpcs.xml /.phpcs.cache /.phpunit.result.cache /.phpunit.cache /build ================================================ FILE: .readthedocs.yaml ================================================ version: 2 build: os: ubuntu-22.04 tools: python: "3" mkdocs: configuration: mkdocs.yml ================================================ FILE: .roave-backward-compatibility-check.json ================================================ { "baseline": [ "#\\[BC\\] SKIPPED: Unable to compile initializer in method Lcobucci\\\\.+#" ] } ================================================ FILE: LICENSE ================================================ Copyright (c) 2014, Luís Cobucci All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the {organization} nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: Makefile ================================================ PARALLELISM := $(shell nproc) .PHONY: all all: install phpcbf phpcs phpstan phpunit infection phpbench .PHONY: install install: vendor/composer/installed.json vendor/composer/installed.json: composer.json composer.lock @composer install $(INSTALL_FLAGS) @touch -c composer.json composer.lock vendor/composer/installed.json .PHONY: phpunit phpunit: @php -d assert.exception=1 -d zend.assertions=1 vendor/bin/phpunit $(PHPUNIT_FLAGS) .PHONY: infection infection: @php -d assert.exception=1 -d zend.assertions=1 -d xdebug.mode=coverage vendor/bin/phpunit --coverage-xml=build/coverage-xml --log-junit=build/junit.xml $(PHPUNIT_FLAGS) @php -d assert.exception=1 -d zend.assertions=1 vendor/bin/infection -v -s --threads=$(PARALLELISM) --coverage=build --skip-initial-tests $(INFECTION_FLAGS) .PHONY: phpcbf phpcbf: @vendor/bin/phpcbf --parallel=$(PARALLELISM) || true .PHONY: phpcs phpcs: @vendor/bin/phpcs --parallel=$(PARALLELISM) $(PHPCS_FLAGS) .PHONY: phpstan phpstan: @php -d xdebug.mode=off vendor/bin/phpstan analyse --memory-limit=-1 ifndef PHPBENCH_REPORT override PHPBENCH_REPORT = aggregate endif .PHONY: phpbench phpbench: @vendor/bin/phpbench run -l dots --retry-threshold=5 --report=$(PHPBENCH_REPORT) $(PHPBENCH_FLAGS) ================================================ FILE: README.md ================================================ # JWT [![Gitter]](https://gitter.im/lcobucci/jwt?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Total Downloads]](https://packagist.org/packages/lcobucci/jwt) [![Latest Stable Version]](https://packagist.org/packages/lcobucci/jwt) [![Unstable Version]](https://packagist.org/packages/lcobucci/jwt) [![Build Status]](https://github.com/lcobucci/jwt/actions?query=workflow%3A%22PHPUnit%20Tests%22+branch%3A4.1.x) [![Code Coverage]](https://codecov.io/gh/lcobucci/jwt) A simple library to work with JSON Web Token and JSON Web Signature based on the [RFC 7519](https://tools.ietf.org/html/rfc7519). ## Installation Package is available on [Packagist](https://packagist.org/packages/lcobucci/jwt), you can install it using [Composer](https://getcomposer.org). ```shell composer require lcobucci/jwt ``` ## Documentation The documentation is available at . [Gitter]: https://img.shields.io/badge/GITTER-JOIN%20CHAT%20%E2%86%92-brightgreen.svg?style=flat-square [Total Downloads]: https://img.shields.io/packagist/dt/lcobucci/jwt.svg?style=flat-square [Latest Stable Version]: https://img.shields.io/packagist/v/lcobucci/jwt.svg?style=flat-square [Unstable Version]: https://img.shields.io/packagist/vpre/lcobucci/jwt.svg?style=flat-square [Build Status]: https://img.shields.io/github/actions/workflow/status/lcobucci/jwt/phpunit.yml?branch=5.1.x&style=flat-square [Code Coverage]: https://codecov.io/gh/lcobucci/jwt/branch/5.1.x/graph/badge.svg ================================================ FILE: composer.json ================================================ { "name": "lcobucci/jwt", "description": "A simple library to work with JSON Web Token and JSON Web Signature", "license": [ "BSD-3-Clause" ], "type": "library", "keywords": [ "JWT", "JWS" ], "authors": [ { "name": "Luís Cobucci", "email": "lcobucci@gmail.com", "role": "Developer" } ], "require": { "php": "~8.3.0 || ~8.4.0 || ~8.5.0", "ext-openssl": "*", "ext-sodium": "*", "psr/clock": "^1.0" }, "require-dev": { "infection/infection": "^0.32.6", "lcobucci/clock": "^3.5.0", "lcobucci/coding-standard": "^11.2", "phpbench/phpbench": "^1.4.3", "phpstan/extension-installer": "^1.4.3", "phpstan/phpstan": "^2.1.40", "phpstan/phpstan-deprecation-rules": "^2.0.4", "phpstan/phpstan-phpunit": "^2.0.16", "phpstan/phpstan-strict-rules": "^2.0.10", "phpunit/phpunit": "^12.5.14 || ^13.0.5" }, "suggest": { "lcobucci/clock": ">= 3.2" }, "autoload": { "psr-4": { "Lcobucci\\JWT\\": "src" } }, "autoload-dev": { "psr-4": { "Lcobucci\\JWT\\Tests\\": "tests" } }, "config": { "allow-plugins": { "dealerdirect/phpcodesniffer-composer-installer": true, "infection/extension-installer": true, "ocramius/package-versions": true, "phpstan/extension-installer": true }, "preferred-install": "dist", "sort-packages": true } } ================================================ FILE: docs/configuration.md ================================================ # Configuration In order to simplify the setup of the library, we provide the class `Lcobucci\JWT\Configuration`. It's meant for: * Configuring the default algorithm (signer) and key(s) to be used * Configuring the default set of validation constraints * Providing custom implementation for the [extension points](extending-the-library.md) * Retrieving components (encoder, decoder, parser, validator, and builder) ## Initialisation The `Lcobucci\JWT\Signer\Key\InMemory` object is used for symmetric/asymmetric signature. To initialise it, you can pass the key content as a plain text: ```php withBuilderFactory( static function (ClaimsFormatter $formatter): Builder { // This assumes `MyCustomBuilder` is an existing class return new MyCustomBuilder(new JoseEncoder(), $formatter); } ); ``` ### Parser It configures how the token parser should be created. It's useful when you want to provide a [custom Parser](extending-the-library.md#parser). ```php withParser(new MyParser()); ``` ### Validator It configures how the token validator should be created. It's useful when you want to provide a [custom Validator](extending-the-library.md#validator). ```php withValidator(new MyValidator()); ``` ### Validation constraints It configures which are the base constraints to be used during validation. ```php withValidationConstraints( new SignedWith($configuration->signer(), $configuration->signingKey()), new StrictValidAt(SystemClock::fromUTC()), new IssuedBy('https://api.my-awesome-company.com') ); ``` ## Retrieve components Once you've made all the necessary configuration you can pass the configuration object around your code and use the getters to retrieve the components: These are the available getters: * `Lcobucci\JWT\Configuration#builder()`: retrieves the token builder (always creating a new instance) * `Lcobucci\JWT\Configuration#parser()`: retrieves the token parser * `Lcobucci\JWT\Configuration#signer()`: retrieves the signer * `Lcobucci\JWT\Configuration#signingKey()`: retrieves the key for signature creation * `Lcobucci\JWT\Configuration#verificationKey()`: retrieves the key for signature verification * `Lcobucci\JWT\Configuration#validator()`: retrieves the token validator * `Lcobucci\JWT\Configuration#validationConstraints()`: retrieves the default set of validation constraints ================================================ FILE: docs/extending-the-library.md ================================================ # Extending the library !!! Note The examples here fetch the configuration object from a hypothetical dependency injection container. You can create it in the same script or require it from a different file. It basically depends on how your system is bootstrapped. We've designed a few extension points in this library. These should enable people to easily customise our core components if they want to. ## Builder The token builder defines a fluent interface for plain token creation. To create your own builder of it you must implement the `Lcobucci\JWT\Builder` interface: ```php use Lcobucci\JWT\Builder; final class MyCustomTokenBuilder implements Builder { // implement all methods } ``` Then, register a custom factory in the [configuration object]: ```php use Lcobucci\JWT\Builder; use Lcobucci\JWT\ClaimsFormatter; use Lcobucci\JWT\Configuration; $config = $container->get(Configuration::class); assert($config instanceof Configuration); $configuration = $configuration->withBuilderFactory( static function (ClaimsFormatter $formatter): Builder { return new MyCustomTokenBuilder($formatter); } ); ``` ## Claims formatter By default, we provide formatters that: - unify the audience claim, making sure we use strings when there's only one item in that claim - format date based claims using microseconds (float) You may customise and even create your own formatters: ```php use Lcobucci\JWT\ClaimsFormatter; use Lcobucci\JWT\Configuration; use Serializable; final class ClaimSerializer implements ClaimsFormatter { /** @inheritdoc */ public function formatClaims(array $claims): array { foreach ($claims as $claim => $claimValue) { if ($claimValue instanceof Serializable) { $claims[$claim] = $claimValue->serialize(); } } return $claims; } } $config = $container->get(Configuration::class); assert($config instanceof Configuration); $builder = $config->builder(new ClaimSerializer()); ``` The class `Lcobucci\JWT\Encoding\ChainedFormatter` allows for users to combine multiple formatters. ## Parser The token parser defines how a JWT string should be converted into token objects. To create your own parser of it you must implement the `Lcobucci\JWT\Parser` interface: ```php use Lcobucci\JWT\Parser; final class MyCustomTokenParser implements Parser { // implement all methods } ``` Then register an instance in the [configuration object]: ```php use Lcobucci\JWT\Configuration; $config = $container->get(Configuration::class); assert($config instanceof Configuration); $configuration = $configuration->withParser(new MyCustomTokenParser()); ``` ## Signer The signer defines how to create and verify signatures. To create your own signer of it you must implement the `Lcobucci\JWT\Signer` interface: ```php use Lcobucci\JWT\Signer; final class SignerForAVeryCustomizedAlgorithm implements Signer { // implement all methods } ``` Then pass an instance of it while creating an instance of the [configuration object], [issuing a token](issuing-tokens.md), or [validating a token]. ## Key The key object is passed down to signers and provide the necessary information to create and verify signatures. To create your own signer of it you must implement the `Lcobucci\JWT\Signer\Key` interface: ```php use Lcobucci\JWT\Signer\Key; final class KeyWithSomeMagicalProperties implements Key { // implement all methods } ``` ## Validator The token validator defines how to apply validation constraint to either validate or assert tokens. To create your own validator of it you must implement the `Lcobucci\JWT\Validator` interface: ```php use Lcobucci\JWT\Validator; final class MyCustomTokenValidator implements Validator { // implement all methods } ``` Then register an instance in the [configuration object]: ```php use Lcobucci\JWT\Configuration; $config = $container->get(Configuration::class); assert($config instanceof Configuration); $configuration = $configuration->withValidator(new MyCustomTokenValidator()); ``` ## Validation constraints A validation constraint define how one or more claims/headers should be validated. Custom validation constraints are handy to provide advanced rules for the registered claims or to validate private claims. To create your own implementation of constraint you must implement the `Lcobucci\JWT\Validation\Constraint` interface: ```php use Lcobucci\JWT\Token; use Lcobucci\JWT\UnencryptedToken; use Lcobucci\JWT\Validation\Constraint; use Lcobucci\JWT\Validation\ConstraintViolation; final class SubjectMustBeAValidUser implements Constraint { public function assert(Token $token): void { if (! $token instanceof UnencryptedToken) { throw new ConstraintViolation('You should pass a plain token'); } if (! $this->existsInDatabase($token->claims()->get('sub'))) { throw new ConstraintViolation('Token related to an unknown user'); } } private function existsInDatabase(string $userId): bool { // ... } } ``` Then use it while [validating a token]. [configuration object]: configuration.md [validating a token]: validating-tokens.md ================================================ FILE: docs/index.md ================================================ # Overview `lcobucci/jwt` is a framework-agnostic PHP library that allows you to issue, parse, and validate JSON Web Tokens based on the [RFC 7519]. ## Support If you're having any issue to use the library, please [create a GH issue]. You can also reach us and other users of this library via our [Gitter channel]. ## License The project is licensed under the MIT license, see [LICENSE file]. [RFC 7519]: https://tools.ietf.org/html/rfc7519 [create a GH issue]: https://github.com/lcobucci/jwt/issues/new [Gitter channel]: https://gitter.im/lcobucci/jwt [LICENSE file]: https://github.com/lcobucci/jwt/blob/master/LICENSE ================================================ FILE: docs/installation.md ================================================ # Installation This package is available on [Packagist] and you can install it using [Composer]. By running the following command you'll add `lcobucci/jwt` as a dependency to your project: ```sh composer require lcobucci/jwt ``` ## Autoloading !!! Note We'll be omitting the autoloader from the code samples to simplify the documentation. In order to be able to use the classes provided by this library you're also required to include [Composer]'s autoloader in your application: ```php require 'vendor/autoload.php'; ``` !!! Tip If you're not familiar with how [composer] works, we highly recommend you to take some time to read it's documentation - especially the [autoloading section]. [Packagist]: https://packagist.org/packages/lcobucci/jwt [Composer]: https://getcomposer.org [autoloading section]: https://getcomposer.org/doc/01-basic-usage.md#autoloading ================================================ FILE: docs/issuing-tokens.md ================================================ # Issuing tokens To issue new tokens you must create a new token builder, customise it, and ask it to build the token: ```php issuedBy('http://example.com') // Configures the audience (aud claim) ->permittedFor('http://example.org') // Configures the subject of the token (sub claim) ->relatedTo('component1') // Configures the id (jti claim) ->identifiedBy('4f1g23a12aa') // Configures the time that the token was issue (iat claim) ->issuedAt($now) // Configures the time that the token can be used (nbf claim) ->canOnlyBeUsedAfter($now->modify('+1 minute')) // Configures the expiration time of the token (exp claim) ->expiresAt($now->modify('+1 hour')) // Configures a new claim, called "uid" ->withClaim('uid', 1) // Configures a new header, called "foo" ->withHeader('foo', 'bar') // Builds a new token ->getToken($algorithm, $signingKey); echo $token->toString(); ``` Once you've created a token, you're able to retrieve its data and convert it to its string representation: ```php issuedBy('http://example.com') ->withClaim('uid', 1) ->withHeader('foo', 'bar') ->getToken($algorithm, $signingKey); $token->headers(); // Retrieves the token headers $token->claims(); // Retrieves the token claims echo $token->headers()->get('foo'), PHP_EOL; // will print "bar" echo $token->claims()->get('iss'), PHP_EOL; // will print "http://example.com" echo $token->claims()->get('uid'), PHP_EOL; // will print "1" echo $token->toString(), PHP_EOL; // The string representation of the object is a JWT string ``` !!! Note Some systems make use of components to handle dependency injection. If your application follows that practice, using a [configuration object](configuration.md) might simplify the wiring of this library. ================================================ FILE: docs/parsing-tokens.md ================================================ # Parsing tokens To parse a token you must create a new parser and ask it to parse a string: ```php parse( 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.' . 'eyJzdWIiOiIxMjM0NTY3ODkwIn0.' . '2gSBz9EOsQRN9I-3iSxJoFt7NtgV6Rm0IL6a8CAwl3Q' ); } catch (CannotDecodeContent | InvalidTokenStructure | UnsupportedHeaderFound $e) { echo 'Oh no, an error: ' . $e->getMessage(); } assert($token instanceof UnencryptedToken); echo $token->claims()->get('sub'), PHP_EOL; // will print "1234567890" ``` !!! Note Some systems make use of components to handle dependency injection. If your application follows that practice, using a [configuration object](configuration.md) might simplify the wiring of this library. ================================================ FILE: docs/quick-start.md ================================================ # Quick start Once the library has been [installed](installation.md), you are able to issue and parse JWTs. The class `Lcobucci\JWT\JwtFacade` is the quickest way to perform these operations. Using that facade we also aim to make sure that every token is properly signed and has the recommended claims for date control. ## Issuing tokens The method `Lcobucci\JWT\JwtFacade#issue()` is available for quickly creating tokens. It uses the current time to generate the date claims (default expiration is **5 minutes**). To issue a token, call the method passing: an algorithm, a key, and a customisation function: ```php issue( new Sha256(), $key, static fn ( Builder $builder, DateTimeImmutable $issuedAt ): Builder => $builder ->issuedBy('https://api.my-awesome-app.io') ->permittedFor('https://client-app.io') ->expiresAt($issuedAt->modify('+10 minutes')) ); var_dump($token->claims()->all()); echo $token->toString(); ``` ### Creating tokens during tests To reduce the chance of having flaky tests on your test suite, the facade supports the usage of a clock object. That allows passing an implementation that always returns the same point in time. You can achieve that by specifying the `clock` constructor parameter: ```php issue( new Sha256(), $key, static fn ( Builder $builder, DateTimeImmutable $issuedAt ): Builder => $builder ); echo $token->claims()->get( RegisteredClaims::ISSUED_AT )->format(DateTimeImmutable::RFC3339); // 2022-06-24 22:51:10 ``` ## Parsing tokens The method `Lcobucci\JWT\JwtFacade#parse()` is the one for quickly parsing tokens. It also verifies the signature and date claims, throwing an exception in case of tokens in unexpected state. ```php parse( $jwt, new Constraint\SignedWith(new Sha256(), $key), new Constraint\StrictValidAt( new FrozenClock(new DateTimeImmutable('2022-07-24 20:55:10+00:00')) ) ); var_dump($token->claims()->all()); ``` !!! Warning The example above uses `FrozenClock` as clock implementation to make sure that code will always work. Use `SystemClock` on the production code of your application, allowing the parser to correctly verify the date claims. ================================================ FILE: docs/rotating-keys.md ================================================ # Rotating Keys Key rotation consists in retiring and replacing cryptographic keys with new ones. Performing that operation on a regular basis is an industry standard. ## Why should I rotate my keys? Rotating keys allows us to: 1. Limit the number of tokens signed with the same key, helping the prevention of attacks enabled by cryptanalysis 2. Adopt other algorithms or stronger keys 3. Limit the impact of eventual compromised keys ## The challenges After rotating keys, apps will likely receive requests with tokens issued with the previous key. If the key rotation of an app is done with a "hard cut", requests with non-expired tokens issued with the old key **will fail**! Imagine if you were the user who logged in just before a key rotation on that kind of app, you'd probably have to log in again! That's rather frustrating, right!? ## Preventing issues It's possible to handle key rotation in a smoother way by leveraging the `SignedWithOneInSet` validation constraint! Say your application uses the symmetric algorithm `HS256` with a not so secure key to issue tokens: ```php issue( new Signer\Hmac\Sha256(), InMemory::plainText( 'a-very-long-and-secure-key-that-should-actually-be-something-else' ), static fn (Builder $builder): Builder => $builder ->issuedBy('https://api.my-awesome-app.io') ->permittedFor('https://client-app.io') ); ``` !!! Sample Here's a token issued with the code above, if you want to test the script locally:
Sample token // line breaks added for readability eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9 .eyJpYXQiOjE2OTkxMzE5NjEsIm5iZiI6MTY5OTEzMTk2MSwiZXhwIjoxNjk5MTMyMjYxLCJpc3MiOiJ odHRwczovL2FwaS5teS1hd2Vzb21lLWFwcC5pbyIsImF1ZCI6Imh0dHBzOi8vY2xpZW50LWFwcC5pbyJ9 .IA9S0n8Q2O97lyR8KczVE8g-hxbbH6_TfJS-JWTQR4c
Your parsing logic (with validations) look like: ```php parse($jwt, ...$validationConstraints); ``` ### Performing a backwards compatible rotation Now Imagine that you want to adopt the new `BLAKE2B` symmetric algorithm. These are the changes to your issuing logic: ```diff issue( - new Signer\Hmac\Sha256(), + new Signer\Blake2b(), - InMemory::plainText( - 'a-very-long-and-secure-key-that-should-actually-be-something-else' + InMemory::base64Encoded( + 'GOu4rLyVCBxmxP+sbniU68ojAja5PkRdvv7vNvBCqDQ=' ), static fn (Builder $builder): Builder => $builder ->issuedBy('https://api.my-awesome-app.io') ->permittedFor('https://client-app.io') ); ``` !!! Sample Here's a token issued with the code above, if you want to test the script locally:
Sample token // line breaks added for readability eyJ0eXAiOiJKV1QiLCJhbGciOiJCTEFLRTJCIn0 .eyJpYXQiOjE2OTkxMzE5NjEsIm5iZiI6MTY5OTEzMTk2MSwiZXhwIjoxNjk5MTMyMjYxLCJpc3Mi OiJodHRwczovL2FwaS5teS1hd2Vzb21lLWFwcC5pbyIsImF1ZCI6Imh0dHBzOi8vY2xpZW50LWFwc C5pbyJ9.bD67s8IXpAJiBTIZn1et_M5WSS7kfmuNiacNRz5lArQ
So far, nothing different that a normal rotation. Now check the changes on the parsing and validation logic: ```diff parse($jwt, ...$validationConstraints); ``` Now the application is able to accept non-expired tokens issued with either old and new keys! In this case, the old key would automatically only be accepted until `2023-12-31 23:59:59+00:00`, even if engineers forget to remove it from the list. !!! Important The order of `SignedWithUntilDate` constraints given to `SignedWithOneInSet` does matter, and it's recommended to leave older keys at the end of the list. ================================================ FILE: docs/supported-algorithms.md ================================================ # Supported Algorithms This library supports signing and verifying tokens with both symmetric and asymmetric algorithms. Encryption is not yet supported. Each algorithm will produce signature with different length. If you have constraints regarding the length of the issued tokens, choose the algorithms that generate shorter output (`HS256`, `RS256`, `ES256`, and `BLAKE2B`). ## Symmetric algorithms Symmetric algorithms perform signature creation and verification using the very same key/secret. They're usually recommended for scenarios where these operations are handled by the very same component. | Name | Description | Class | Key length req. | |-----------|--------------------|------------------------------------|-----------------| | `HS256` | HMAC using SHA-256 | `\Lcobucci\JWT\Signer\Hmac\Sha256` | `>= 256 bits` | | `HS384` | HMAC using SHA-384 | `\Lcobucci\JWT\Signer\Hmac\Sha384` | `>= 384 bits` | | `HS512` | HMAC using SHA-512 | `\Lcobucci\JWT\Signer\Hmac\Sha512` | `>= 512 bits` | | `BLAKE2B` | Blake2b keyed Hash | `\Lcobucci\JWT\Signer\Blake2b` | `>= 256 bits` | !!! Warning Although `BLAKE2B` is fantastic due to its performance, it's not [JWT standard] and won't necessarily be offered by other libraries. ## Asymmetric algorithms Asymmetric algorithms perform signature creation with private/secret keys and verification with public keys. They're usually recommended for scenarios where creation is handled by a component and verification by many others. | Name | Description | Class | Key length req. | |---------|---------------------------------|-------------------------------------|-----------------| | `ES256` | ECDSA using P-256 and SHA-256 | `\Lcobucci\JWT\Signer\Ecdsa\Sha256` | `== 256 bits` | | `ES384` | ECDSA using P-384 and SHA-384 | `\Lcobucci\JWT\Signer\Ecdsa\Sha384` | `== 384 bits` | | `ES512` | ECDSA using P-521 and SHA-512 | `\Lcobucci\JWT\Signer\Ecdsa\Sha512` | `== 521 bits` | | `RS256` | RSASSA-PKCS1-v1_5 using SHA-256 | `\Lcobucci\JWT\Signer\Rsa\Sha256` | `>= 2048 bits` | | `RS384` | RSASSA-PKCS1-v1_5 using SHA-384 | `\Lcobucci\JWT\Signer\Rsa\Sha384` | `>= 2048 bits` | | `RS512` | RSASSA-PKCS1-v1_5 using SHA-512 | `\Lcobucci\JWT\Signer\Rsa\Sha512` | `>= 2048 bits` | | `EdDSA` | EdDSA signature algorithms | `\Lcobucci\JWT\Signer\Eddsa` | `>= 256 bits` | The following algorithms are implemented in a separate package `lcobucci/jwt-rsassa-pss` in order to keep dependencies low in the main package. Please see the installation instructions in the [RSASSA-PSS readme]. | Name | Description | Class | Key length req. | |---------|---------------------------------|--------------------------------------|-----------------| | `PS256` | RSASSA-PSS using SHA-256 | `\Lcobucci\JWT\Signer\RsaPss\Sha256` | `>= 2048 bits` | | `PS384` | RSASSA-PSS using SHA-384 | `\Lcobucci\JWT\Signer\RsaPss\Sha384` | `>= 2048 bits` | | `PS512` | RSASSA-PSS using SHA-512 | `\Lcobucci\JWT\Signer\RsaPss\Sha512` | `>= 2048 bits` | ## `none` algorithm The `none` algorithm as described by [JWT standard] is intentionally not implemented and not supported. The risk of misusing it is too high, and even where other means guarantee the token validity a symmetric algorithm shouldn't represent a computational bottleneck with modern hardware. [JWT standard]: https://www.iana.org/assignments/jose/jose.xhtml#web-signature-encryption-algorithms [RSASSA-PSS readme]: https://github.com/lcobucci/jwt-rsassa-pss ================================================ FILE: docs/upgrading.md ================================================ # Upgrading steps Here we'll keep a list of all steps you need to take to make sure your code is compatible with newer versions. ## v4.x to v5.x The release `v5.0.0` is a modernised version of the library, which requires PHP 8.1+ and drops all the deprecated components. We're adding a few deprecation annotations on the version `v4.3.0`. So, before going to `v5.0.0` please update to the latest 4.3.x version using composer: ```sh composer require lcobucci/jwt ^4.3 ``` Then run your tests and change all calls to deprecated methods, even if they are not triggering any notices. Tools like [`phpstan/phpstan-deprecation-rules`](https://github.com/phpstan/phpstan-deprecation-rules) can help finding them. ### Removal of `Ecdsa::create()` To promote symmetry on the instantiation of all algorithms (signers), we're dropping the named constructor in favour of the constructor. If you are using any variant of ECDSA, please change your code following this example: ```diff issue( new Sha256(), $key, static function ( Builder $builder, DateTimeImmutable $now ): Builder { - $builder->issuedBy('https://api.my-awesome-app.io'); - $builder->permittedFor('https://client-app.io'); - $builder->expiresAt($now->modify('+10 minutes')); + $builder = $builder->issuedBy('https://api.my-awesome-app.io'); + $builder = $builder->permittedFor('https://client-app.io'); + $builder = $builder->expiresAt($now->modify('+10 minutes')); return $builder; } ); ``` Or: ```diff $token = (new JwtFacade())->issue( new Sha256(), $key, - static function ( + static fn ( Builder $builder, DateTimeImmutable $now - ): Builder { - $builder->issuedBy('https://api.my-awesome-app.io'); - $builder->permittedFor('https://client-app.io'); - $builder->expiresAt($now->modify('+10 minutes')); - - return $builder; - } + ): Builder => $builder->issuedBy('https://api.my-awesome-app.io') + ->permittedFor('https://client-app.io') + ->expiresAt($now->modify('+10 minutes')) ); ``` ### `lcobucci/clock` is not installed by default anymore Thanks to [PSR-20](https://www.php-fig.org/psr/psr-20/), users can more easily plug-in other [clock implementations](https://packagist.org/providers/psr/clock-implementation) if they choose to do so. If you like and were already using `lcobucci/clock` on your system, you're required to explicitly add it as a production dependency: ```sh composer require lcobucci/clock ``` ## v3.x to v4.x The `v4.0.0` aggregates about 5 years of work and contains **several BC-breaks**. We're building on the version `v3.4.0` a forward compatibility layer to help users to migrate to `v4.0.0`. To help on the migration process, all deprecated components are being marked with `@deprecated` and deprecated behaviour will trigger a `E_USER_DEPRECATED` error. However, you can also find here the instructions on how to make your code compatible with both versions. ### General migration strategy Update your existing software to the latest 3.4.x version using composer: ```sh composer require lcobucci/jwt ^3.4 ``` Then run your tests and fix all deprecation notices. Also change all calls to deprecated methods, even if they are not triggering any notices. Tools like [`phpstan/phpstan-deprecation-rules`](https://github.com/phpstan/phpstan-deprecation-rules) can help finding them. Note that PHPUnit tests will only fail if you have the `E_USER_DEPRECATED` error level activated - it is a good practice to run tests using `E_ALL`. Data providers that trigger deprecation messages will not fail tests at all, only print the message to the console. Make sure you do not see any of them before you continue. Now you can upgrade to the latest 4.x version: ```sh composer require lcobucci/jwt ^4.0 ``` Remember that some deprecation messages from the 3.4 version may have notified you that things still are different in 4.0, so you may find you need to adapt your own code once more at this stage. While you are at it, consider using the new configuration object. The details are listed below. ### Inject the configuration object instead of builder/parser/key This object serves as a small service locator that centralises the JWT-related dependencies. The main goal is to simplify the injection of our components into downstream code. This step is quite important to at least have a single way to initialise the JWT components, even if the configuration object is thrown away. Check an example of how to migrate the injection of builder+signer+key to configuration object below: ```diff builder = $builder; - $this->signer = $signer; - $this->key = $key; - } + private Configuration $config; + + public function __construct(Configuration $config) + { + $this->config = $config; + } public function issueToken(): Token { - return $this->builder + return $this->config->builder() ->identifiedBy(bin2hex(random_bytes(16))) - ->getToken($this->signer, $this->key); + ->getToken($this->config->signer(), $this->config->signingKey()); } } ``` You can find more information on how to use the configuration object, [here](configuration.md). ### Use new `Key` objects `Lcobucci\JWT\Signer\Key` has been converted to an interface in `v4.0`. We provide `Lcobucci\JWT\Signer\Key\InMemory`, a drop-in replacement of the behaviour for `Lcobucci\JWT\Signer\Key` in `v3.x`. You will need to pick the appropriated named constructor to migrate your code: ```diff builder() - ->setIssuer('http://example.com', true) + ->issuedBy('http://example.com') + ->withHeader('iss', 'http://example.com') - ->setAudience('http://example.org') + ->permittedFor('http://example.org') - ->setId('4f1g23a12aa') + ->identifiedBy('4f1g23a12aa') - ->setSubject('user123') + ->relatedTo('user123') - ->setIssuedAt($now) + ->issuedAt($now) - ->setNotBefore($now + 60) + ->canOnlyBeUsedAfter($now->modify('+1 minute')) - ->setExpiration($now + 3600) + ->expiresAt($now->modify('+1 hour')) - ->set('uid', 1) + ->withClaim('uid', 1) - ->sign(new Sha256(), 'testing') - ->getToken(); + ->getToken($config->signer(), $config->signingKey()); ``` #### Date precision If you want to continue using Unix timestamps, you can use the `withUnixTimestampDates()`-formatter: ```diff builder(ChainedFormatter::withUnixTimestampDates()); ``` #### Support for multiple audiences Even though we didn't officially support multiple audiences, it was technically possible to achieve that by manually setting the `aud` claim to an array with multiple strings. If you parse a token with 3.4, and read its contents with `\Lcobucci\JWT\Token#getClaim()` or`\Lcobucci\JWT\Token#getClaims()`, you will only get the first element of such an array back. If the audience claim does only contain a string, or only contains one string in the array, nothing changes. Please [upgrade to the new Token API](#use-the-new-token-api) for accessing claims in order to get the full audience array again (e.g. call `Token#claims()->get('aud')`). When creating a token, use the new method `Builder#permittedFor()` as detailed below. ##### Multiple calls to `Builder#permittedFor()` ```diff builder() - ->withClaim('aud', ['one', 'two', 'three']) + ->permittedFor('one') + ->permittedFor('two') + ->permittedFor('three') - ->sign(new Sha256(), 'testing') - ->getToken(); + ->getToken($config->signer(), $config->signingKey()); ``` ##### Single call to `Builder#permittedFor()` with multiple arguments ```diff builder() - ->withClaim('aud', ['one', 'two', 'three']) + ->permittedFor('one', 'two', 'three') - ->sign(new Sha256(), 'testing') - ->getToken(); + ->getToken($config->signer(), $config->signingKey()); ``` ##### Single call to `Builder#permittedFor()` with argument unpacking ```diff builder() - ->withClaim('aud', ['one', 'two', 'three']) + ->permittedFor(...['one', 'two', 'three']) - ->sign(new Sha256(), 'testing') - ->getToken(); + ->getToken($config->signer(), $config->signingKey()); ``` ### Replace `Token#verify()` and `Token#validate()` with Validation API These methods were quite limited and brings multiple responsibilities to the `Token` class. On `v4.0` we provide another component to validate tokens, including their signature. Here's an example of how to modify that logic (considering [constraints have been configured](configuration.md#customisation)): ```diff parser = $parser; - $this->signer = $signer; - $this->key = $key; + $this->config = $config; } public function authenticate(string $jwt): void { - $token = $this->parser->parse($jwt); + $token = $this->config->parser()->parse($jwt); - if (! $token->validate(new ValidationData()) || $token->verify($this->signer, $this->key)) { + if (! $this->config->validator()->validate($token, ...$this->config->validationConstraints())) { throw new InvalidArgumentException('Invalid token provided'); } } } ``` Check [here](validating-tokens.md) for more information on how to validate tokens and what are the built-in constraints. ### Use the new `Token` API There some important differences on this new API: 1. We no longer use the `Lcobucci\JWT\Claim` objects 1. Headers and claims are now represented as `Lcobucci\JWT\Token\DataSet` 1. Different methods should be used to retrieve a header/claim 1. No exception is thrown when accessing missing header/claim, the default argument is always used 1. Tokens should be explicitly casted to string via method Your code should be adapted to manipulate tokens like this: ```diff getHeaders() +$token->headers()->all() -$token->hasHeader('typ') +$token->headers()->has('typ') -$token->getHeader('typ') +$token->headers()->get('typ') -$token->getClaims() +$token->claims()->all() -$token->hasClaim('iss') +$token->claims()->has('iss') -$token->getClaim('iss') +$token->claims()->get('iss') -echo (string) $token; +echo $token->toString(); ``` ================================================ FILE: docs/validating-tokens.md ================================================ # Validating tokens To validate a token you must create a new validator and assert or validate a token. ## Using `Lcobucci\JWT\Validator#assert()` This method goes through every single constraint in the set, groups all the violations, and throws an exception with the grouped violations: ```php parse( 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.' . 'eyJzdWIiOiIxMjM0NTY3ODkwIn0.' . '2gSBz9EOsQRN9I-3iSxJoFt7NtgV6Rm0IL6a8CAwl3Q' ); $validator = new Validator(); try { $validator->assert($token, new RelatedTo('1234567891')); // doesn't throw an exception $validator->assert($token, new RelatedTo('1234567890')); } catch (RequiredConstraintsViolated $e) { // list of constraints violation exceptions: var_dump($e->violations()); } ``` ## Using `Lcobucci\JWT\Validator#validate()` The difference here is that we'll always get a `boolean` result and stop in the very first violation: ```php parse( 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.' . 'eyJzdWIiOiIxMjM0NTY3ODkwIn0.' . '2gSBz9EOsQRN9I-3iSxJoFt7NtgV6Rm0IL6a8CAwl3Q' ); $validator = new Validator(); if (! $validator->validate($token, new RelatedTo('1234567891'))) { echo 'Invalid token (1)!', PHP_EOL; // will print this } if (! $validator->validate($token, new RelatedTo('1234567890'))) { echo 'Invalid token (2)!', PHP_EOL; // will not print this } ``` !!! Note Some systems make use of components to handle dependency injection. If your application follows that practice, using a [configuration object](configuration.md) might simplify the wiring of this library. ## Available constraints This library provides the following constraints: * `Lcobucci\JWT\Validation\Constraint\IdentifiedBy`: verifies if the claim `jti` matches the expected value * `Lcobucci\JWT\Validation\Constraint\IssuedBy`: verifies if the claim `iss` is listed as expected values * `Lcobucci\JWT\Validation\Constraint\PermittedFor`: verifies if the claim `aud` contains the expected value * `Lcobucci\JWT\Validation\Constraint\RelatedTo`: verifies if the claim `sub` matches the expected value * `Lcobucci\JWT\Validation\Constraint\SignedWith`: verifies if the token was signed with the expected signer and key * `Lcobucci\JWT\Validation\Constraint\SignedWithOneInSet`: verifies the token signature against multiple `SignedWithUntilDate` constraints * `Lcobucci\JWT\Validation\Constraint\SignedWithUntilDate`: verifies if the token was signed with the expected signer and key (until a certain date) * `Lcobucci\JWT\Validation\Constraint\StrictValidAt`: verifies presence and validity of the claims `iat`, `nbf`, and `exp` (supports leeway configuration) * `Lcobucci\JWT\Validation\Constraint\LooseValidAt`: verifies the claims `iat`, `nbf`, and `exp`, when present (supports leeway configuration) * `Lcobucci\JWT\Validation\Constraint\HasClaimWithValue`: verifies that a **custom claim** has the expected value (not recommended when comparing cryptographic hashes) * `Lcobucci\JWT\Validation\Constraint\HasClaim`: verifies that a **custom claim** is present You may also create your [own validation constraints](extending-the-library.md#validation-constraints). ================================================ FILE: infection.json.dist ================================================ { "source": { "directories": ["src"] }, "timeout": 3, "logs": { "text": "infection.txt" }, "mutators": { "@default": true, "@function_signature": true, "CastInt": { "ignore": [ "Lcobucci\\JWT\\Signer\\Ecdsa\\MultibyteStringConverter::octetLength", "Lcobucci\\JWT\\Signer\\Ecdsa\\MultibyteStringConverter::readAsn1Integer" ] }, "UnwrapSubstr": { "ignore": [ "Lcobucci\\JWT\\Signer\\Ecdsa\\MultibyteStringConverter::preparePositiveInteger" ] }, "GreaterThan": { "ignore": [ "Lcobucci\\JWT\\Signer\\Ecdsa\\MultibyteStringConverter::toAsn1", "Lcobucci\\JWT\\Signer\\Ecdsa\\MultibyteStringConverter::preparePositiveInteger", "Lcobucci\\JWT\\Signer\\Ecdsa\\MultibyteStringConverter::retrievePositiveInteger" ] }, "LessThanOrEqualTo": { "ignore": [ "Lcobucci\\JWT\\Signer\\Ecdsa\\MultibyteStringConverter::preparePositiveInteger" ] }, "LogicalNot": { "ignoreSourceCodeByRegex": [ "if \\(! function_exists\\('sodium_\\w+'\\)\\) \\{" ] } }, "minMsi": 100, "minCoveredMsi": 100 } ================================================ FILE: mkdocs.yml ================================================ site_name: lcobucci/jwt theme: readthedocs nav: - Intro: - 'index.md' - 'installation.md' - 'quick-start.md' - 'supported-algorithms.md' - Usage: - 'issuing-tokens.md' - 'parsing-tokens.md' - 'validating-tokens.md' - 'configuration.md' - Guides: - 'extending-the-library.md' - 'rotating-keys.md' - 'upgrading.md' markdown_extensions: - admonition - footnotes - toc: permalink: true ================================================ FILE: phpbench.json ================================================ { "runner.bootstrap": "vendor/autoload.php", "runner.path": "tests/Benchmark" } ================================================ FILE: phpcs.xml.dist ================================================ src tests tests ================================================ FILE: phpstan.neon.dist ================================================ parameters: level: 8 # not yet ready for all the `mixed` checks paths: - src - tests ================================================ FILE: phpunit.xml.dist ================================================ tests src ================================================ FILE: renovate.json ================================================ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": [ "local>lcobucci/.github:renovate-config" ] } ================================================ FILE: src/Builder.php ================================================ $claims * * @return array */ #[NoDiscard] public function formatClaims(array $claims): array; } ================================================ FILE: src/Configuration.php ================================================ parser = $parser ?? new Token\Parser($decoder); $this->validator = $validator ?? new Validation\Validator(); $this->builderFactory = $builderFactory ?? static function (ClaimsFormatter $claimFormatter) use ($encoder): Builder { return Token\Builder::new($encoder, $claimFormatter); }; $this->validationConstraints = $validationConstraints; } #[NoDiscard] public static function forAsymmetricSigner( Signer $signer, Key $signingKey, Key $verificationKey, Encoder $encoder = new JoseEncoder(), Decoder $decoder = new JoseEncoder(), ): self { return new self( $signer, $signingKey, $verificationKey, $encoder, $decoder, null, null, null, ); } #[NoDiscard] public static function forSymmetricSigner( Signer $signer, Key $key, Encoder $encoder = new JoseEncoder(), Decoder $decoder = new JoseEncoder(), ): self { return new self( $signer, $key, $key, $encoder, $decoder, null, null, null, ); } /** @param callable(ClaimsFormatter): Builder $builderFactory */ #[NoDiscard] public function withBuilderFactory(callable $builderFactory): self { return new self( $this->signer, $this->signingKey, $this->verificationKey, $this->encoder, $this->decoder, $this->parser, $this->validator, $builderFactory(...), ...$this->validationConstraints, ); } public function builder(?ClaimsFormatter $claimFormatter = null): Builder { return ($this->builderFactory)($claimFormatter ?? ChainedFormatter::default()); } public function parser(): Parser { return $this->parser; } #[NoDiscard] public function withParser(Parser $parser): self { return new self( $this->signer, $this->signingKey, $this->verificationKey, $this->encoder, $this->decoder, $parser, $this->validator, $this->builderFactory, ...$this->validationConstraints, ); } public function signer(): Signer { return $this->signer; } public function signingKey(): Key { return $this->signingKey; } public function verificationKey(): Key { return $this->verificationKey; } public function validator(): Validator { return $this->validator; } #[NoDiscard] public function withValidator(Validator $validator): self { return new self( $this->signer, $this->signingKey, $this->verificationKey, $this->encoder, $this->decoder, $this->parser, $validator, $this->builderFactory, ...$this->validationConstraints, ); } /** @return Constraint[] */ public function validationConstraints(): array { return $this->validationConstraints; } #[NoDiscard] public function withValidationConstraints(Constraint ...$validationConstraints): self { return new self( $this->signer, $this->signingKey, $this->verificationKey, $this->encoder, $this->decoder, $this->parser, $this->validator, $this->builderFactory, ...$validationConstraints, ); } } ================================================ FILE: src/Decoder.php ================================================ */ private array $formatters; public function __construct(ClaimsFormatter ...$formatters) { $this->formatters = $formatters; } public static function default(): self { return new self(new UnifyAudience(), new MicrosecondBasedDateConversion()); } public static function withUnixTimestampDates(): self { return new self(new UnifyAudience(), new UnixTimestampDates()); } /** @inheritdoc */ public function formatClaims(array $claims): array { foreach ($this->formatters as $formatter) { $claims = $formatter->formatClaims($claims); } return $claims; } } ================================================ FILE: src/Encoding/JoseEncoder.php ================================================ convertDate($claims[$claim]); } return $claims; } private function convertDate(DateTimeImmutable $date): int|float { if ($date->format('u') === '000000') { return (int) $date->format('U'); } return (float) $date->format('U.u'); } } ================================================ FILE: src/Encoding/UnifyAudience.php ================================================ convertDate($claims[$claim]); } return $claims; } private function convertDate(DateTimeImmutable $date): int { return $date->getTimestamp(); } } ================================================ FILE: src/Exception.php ================================================ clock = $clock ?? new class implements Clock { public function now(): DateTimeImmutable { return new DateTimeImmutable(); } }; } /** @param Closure(Builder, DateTimeImmutable):Builder $customiseBuilder */ #[NoDiscard] public function issue( Signer $signer, Key $signingKey, Closure $customiseBuilder, ): UnencryptedToken { $builder = Token\Builder::new(new JoseEncoder(), ChainedFormatter::withUnixTimestampDates()); $now = $this->clock->now(); $builder = $builder ->issuedAt($now) ->canOnlyBeUsedAfter($now) ->expiresAt($now->modify('+5 minutes')); return $customiseBuilder($builder, $now)->getToken($signer, $signingKey); } /** @param non-empty-string $jwt */ #[NoDiscard] public function parse( string $jwt, SignedWith $signedWith, ValidAt $validAt, Constraint ...$constraints, ): UnencryptedToken { $token = $this->parser->parse($jwt); assert($token instanceof UnencryptedToken); (new Validator())->assert( $token, $signedWith, $validAt, ...$constraints, ); return $token; } } ================================================ FILE: src/Parser.php ================================================ contents()); if ($actualKeyLength < self::MINIMUM_KEY_LENGTH_IN_BITS) { throw InvalidKeyProvided::tooShort(self::MINIMUM_KEY_LENGTH_IN_BITS, $actualKeyLength); } return sodium_crypto_generichash($payload, $key->contents()); } public function verify(string $expected, string $payload, Key $key): bool { return hash_equals($expected, $this->sign($payload, $key)); } } ================================================ FILE: src/Signer/CannotSignPayload.php ================================================ self::ASN1_MAX_SINGLE_BYTE ? self::ASN1_LENGTH_2BYTES : ''; $asn1 = hex2bin( self::ASN1_SEQUENCE . $lengthPrefix . dechex($totalLength) . self::ASN1_INTEGER . dechex($lengthR) . $pointR . self::ASN1_INTEGER . dechex($lengthS) . $pointS, ); assert(is_string($asn1)); assert($asn1 !== ''); return $asn1; } private static function octetLength(string $data): int { return (int) (strlen($data) / self::BYTE_SIZE); } private static function preparePositiveInteger(string $data): string { if (substr($data, 0, self::BYTE_SIZE) > self::ASN1_BIG_INTEGER_LIMIT) { return self::ASN1_NEGATIVE_INTEGER . $data; } while ( substr($data, 0, self::BYTE_SIZE) === self::ASN1_NEGATIVE_INTEGER && substr($data, 2, self::BYTE_SIZE) <= self::ASN1_BIG_INTEGER_LIMIT ) { $data = substr($data, 2, null); } return $data; } public function fromAsn1(string $signature, int $length): string { $message = bin2hex($signature); $position = 0; if (self::readAsn1Content($message, $position, self::BYTE_SIZE) !== self::ASN1_SEQUENCE) { throw ConversionFailed::incorrectStartSequence(); } // @phpstan-ignore-next-line if (self::readAsn1Content($message, $position, self::BYTE_SIZE) === self::ASN1_LENGTH_2BYTES) { $position += self::BYTE_SIZE; } $pointR = self::retrievePositiveInteger(self::readAsn1Integer($message, $position)); $pointS = self::retrievePositiveInteger(self::readAsn1Integer($message, $position)); $points = hex2bin(str_pad($pointR, $length, '0', STR_PAD_LEFT) . str_pad($pointS, $length, '0', STR_PAD_LEFT)); assert(is_string($points)); assert($points !== ''); return $points; } private static function readAsn1Content(string $message, int &$position, int $length): string { $content = substr($message, $position, $length); $position += $length; return $content; } private static function readAsn1Integer(string $message, int &$position): string { if (self::readAsn1Content($message, $position, self::BYTE_SIZE) !== self::ASN1_INTEGER) { throw ConversionFailed::integerExpected(); } $length = (int) hexdec(self::readAsn1Content($message, $position, self::BYTE_SIZE)); return self::readAsn1Content($message, $position, $length * self::BYTE_SIZE); } private static function retrievePositiveInteger(string $data): string { while ( substr($data, 0, self::BYTE_SIZE) === self::ASN1_NEGATIVE_INTEGER && substr($data, 2, self::BYTE_SIZE) > self::ASN1_BIG_INTEGER_LIMIT ) { $data = substr($data, 2, null); } return $data; } } ================================================ FILE: src/Signer/Ecdsa/Sha256.php ================================================ converter->fromAsn1( $this->createSignature($key, $payload), $this->pointLength(), ); } final public function verify(string $expected, string $payload, Key $key): bool { return $this->verifySignature( $this->converter->toAsn1($expected, $this->pointLength()), $payload, $key, ); } /** {@inheritDoc} */ final protected function guardAgainstIncompatibleKey(int $type, int $lengthInBits): void { if ($type !== OPENSSL_KEYTYPE_EC) { throw InvalidKeyProvided::incompatibleKeyType( self::KEY_TYPE_MAP[OPENSSL_KEYTYPE_EC], self::KEY_TYPE_MAP[$type] ?? 'unknown', ); } $expectedKeyLength = $this->expectedKeyLength(); if ($lengthInBits !== $expectedKeyLength) { throw InvalidKeyProvided::incompatibleKeyLength($expectedKeyLength, $lengthInBits); } } /** * @internal * * @return positive-int */ abstract public function expectedKeyLength(): int; /** * Returns the length of each point in the signature, so that we can calculate and verify R and S points properly * * @internal * * @return positive-int */ abstract public function pointLength(): int; } ================================================ FILE: src/Signer/Eddsa.php ================================================ contents()); } catch (SodiumException $sodiumException) { throw new InvalidKeyProvided($sodiumException->getMessage(), 0, $sodiumException); } } public function verify(string $expected, string $payload, Key $key): bool { try { return sodium_crypto_sign_verify_detached($expected, $payload, $key->contents()); } catch (SodiumException $sodiumException) { throw new InvalidKeyProvided($sodiumException->getMessage(), 0, $sodiumException); } } } ================================================ FILE: src/Signer/Hmac/Sha256.php ================================================ contents()); $expectedKeyLength = $this->minimumBitsLengthForKey(); if ($actualKeyLength < $expectedKeyLength) { throw InvalidKeyProvided::tooShort($expectedKeyLength, $actualKeyLength); } return hash_hmac($this->algorithm(), $payload, $key->contents(), true); } final public function verify(string $expected, string $payload, Key $key): bool { return hash_equals($expected, $this->sign($payload, $key)); } /** * @internal * * @return non-empty-string */ abstract public function algorithm(): string; /** * @internal * * @return positive-int */ abstract public function minimumBitsLengthForKey(): int; } ================================================ FILE: src/Signer/InvalidKeyProvided.php ================================================ getSize(); $contents = $fileSize > 0 ? $file->fread($file->getSize()) : ''; assert(is_string($contents)); self::guardAgainstEmptyKey($contents); return new self($contents, $passphrase); } /** @phpstan-assert non-empty-string $contents */ private static function guardAgainstEmptyKey(string $contents): void { if ($contents === '') { throw InvalidKeyProvided::cannotBeEmpty(); } } public function contents(): string { return $this->contents; } public function passphrase(): string { return $this->passphrase; } } ================================================ FILE: src/Signer/Key.php ================================================ 'RSA', OPENSSL_KEYTYPE_DSA => 'DSA', OPENSSL_KEYTYPE_DH => 'DH', OPENSSL_KEYTYPE_EC => 'EC', ]; /** * @return non-empty-string * * @throws CannotSignPayload * @throws InvalidKeyProvided */ final protected function createSignature( Key $key, string $payload, ): string { $opensslKey = $this->getPrivateKey($key); $signature = ''; if (! openssl_sign($payload, $signature, $opensslKey, $this->algorithm())) { throw CannotSignPayload::errorHappened($this->fullOpenSSLErrorString()); } return $signature; } /** @throws CannotSignPayload */ private function getPrivateKey( Key $key, ): OpenSSLAsymmetricKey { return $this->validateKey(openssl_pkey_get_private($key->contents(), $key->passphrase())); } /** @throws InvalidKeyProvided */ final protected function verifySignature( string $expected, string $payload, Key $key, ): bool { $opensslKey = $this->getPublicKey($key); $result = openssl_verify($payload, $expected, $opensslKey, $this->algorithm()); return $result === 1; } /** @throws InvalidKeyProvided */ private function getPublicKey(Key $key): OpenSSLAsymmetricKey { return $this->validateKey(openssl_pkey_get_public($key->contents())); } /** * Raises an exception when the key type is not the expected type * * @throws InvalidKeyProvided */ private function validateKey(OpenSSLAsymmetricKey|bool $key): OpenSSLAsymmetricKey { if (is_bool($key)) { throw InvalidKeyProvided::cannotBeParsed($this->fullOpenSSLErrorString()); } $details = openssl_pkey_get_details($key); assert(is_array($details)); assert(array_key_exists('bits', $details)); assert(is_int($details['bits'])); assert(array_key_exists('type', $details)); assert(is_int($details['type'])); $this->guardAgainstIncompatibleKey($details['type'], $details['bits']); return $key; } private function fullOpenSSLErrorString(): string { $error = ''; while ($msg = openssl_error_string()) { $error .= PHP_EOL . '* ' . $msg; } return $error; } /** @throws InvalidKeyProvided */ abstract protected function guardAgainstIncompatibleKey(int $type, int $lengthInBits): void; /** * Returns which algorithm to be used to create/verify the signature (using OpenSSL constants) * * @internal */ abstract public function algorithm(): int; } ================================================ FILE: src/Signer/Rsa/Sha256.php ================================================ createSignature($key, $payload); } final public function verify(string $expected, string $payload, Key $key): bool { return $this->verifySignature($expected, $payload, $key); } final protected function guardAgainstIncompatibleKey(int $type, int $lengthInBits): void { if ($type !== OPENSSL_KEYTYPE_RSA) { throw InvalidKeyProvided::incompatibleKeyType( self::KEY_TYPE_MAP[OPENSSL_KEYTYPE_RSA], self::KEY_TYPE_MAP[$type] ?? 'unknown', ); } if ($lengthInBits < self::MINIMUM_KEY_LENGTH) { throw InvalidKeyProvided::tooShort(self::MINIMUM_KEY_LENGTH, $lengthInBits); } } } ================================================ FILE: src/Signer.php ================================================ $headers * @param array $claims */ private function __construct( private Encoder $encoder, private ClaimsFormatter $claimFormatter, private array $headers = ['typ' => 'JWT', 'alg' => null], private array $claims = [], ) { } #[NoDiscard] public static function new(Encoder $encoder, ClaimsFormatter $claimFormatter): self { return new self($encoder, $claimFormatter); } public function permittedFor(string ...$audiences): BuilderInterface { $configured = $this->claims[RegisteredClaims::AUDIENCE] ?? []; $toAppend = array_diff($audiences, $configured); return $this->newWithClaim(RegisteredClaims::AUDIENCE, array_merge($configured, $toAppend)); } public function expiresAt(DateTimeImmutable $expiration): BuilderInterface { return $this->newWithClaim(RegisteredClaims::EXPIRATION_TIME, $expiration); } public function identifiedBy(string $id): BuilderInterface { return $this->newWithClaim(RegisteredClaims::ID, $id); } public function issuedAt(DateTimeImmutable $issuedAt): BuilderInterface { return $this->newWithClaim(RegisteredClaims::ISSUED_AT, $issuedAt); } public function issuedBy(string $issuer): BuilderInterface { return $this->newWithClaim(RegisteredClaims::ISSUER, $issuer); } public function canOnlyBeUsedAfter(DateTimeImmutable $notBefore): BuilderInterface { return $this->newWithClaim(RegisteredClaims::NOT_BEFORE, $notBefore); } public function relatedTo(string $subject): BuilderInterface { return $this->newWithClaim(RegisteredClaims::SUBJECT, $subject); } public function withHeader(string $name, mixed $value): BuilderInterface { $headers = $this->headers; $headers[$name] = $value; return new self( $this->encoder, $this->claimFormatter, $headers, $this->claims, ); } public function withClaim(string $name, mixed $value): BuilderInterface { if (in_array($name, RegisteredClaims::ALL, true)) { throw RegisteredClaimGiven::forClaim($name); } return $this->newWithClaim($name, $value); } /** @param non-empty-string $name */ private function newWithClaim(string $name, mixed $value): BuilderInterface { $claims = $this->claims; $claims[$name] = $value; return new self( $this->encoder, $this->claimFormatter, $this->headers, $claims, ); } /** * @param array $items * * @throws CannotEncodeContent When data cannot be converted to JSON. */ private function encode(array $items): string { return $this->encoder->base64UrlEncode( $this->encoder->jsonEncode($items), ); } public function getToken(Signer $signer, Key $key): UnencryptedToken { $headers = $this->headers; $headers['alg'] = $signer->algorithmId(); $encodedHeaders = $this->encode($headers); $encodedClaims = $this->encode($this->claimFormatter->formatClaims($this->claims)); $signature = $signer->sign($encodedHeaders . '.' . $encodedClaims, $key); $encodedSignature = $this->encoder->base64UrlEncode($signature); return new Plain( new DataSet($headers, $encodedHeaders), new DataSet($this->claims, $encodedClaims), new Signature($signature, $encodedSignature), ); } } ================================================ FILE: src/Token/DataSet.php ================================================ $data */ public function __construct(private array $data, private string $encoded) { } /** @param non-empty-string $name */ public function get(string $name, mixed $default = null): mixed { return $this->data[$name] ?? $default; } /** @param non-empty-string $name */ public function has(string $name): bool { return array_key_exists($name, $this->data); } /** @return array */ public function all(): array { return $this->data; } public function toString(): string { return $this->encoded; } } ================================================ FILE: src/Token/InvalidTokenStructure.php ================================================ splitJwt($jwt); if ($encodedHeaders === '') { throw InvalidTokenStructure::missingHeaderPart(); } if ($encodedClaims === '') { throw InvalidTokenStructure::missingClaimsPart(); } if ($encodedSignature === '') { throw InvalidTokenStructure::missingSignaturePart(); } $header = $this->parseHeader($encodedHeaders); return new Plain( new DataSet($header, $encodedHeaders), new DataSet($this->parseClaims($encodedClaims), $encodedClaims), $this->parseSignature($encodedSignature), ); } /** * Splits the JWT string into an array * * @param non-empty-string $jwt * * @return string[] * * @throws InvalidTokenStructure When JWT doesn't have all parts. */ private function splitJwt(string $jwt): array { $data = explode('.', $jwt); if (count($data) !== 3) { throw InvalidTokenStructure::missingOrNotEnoughSeparators(); } return $data; } /** * Parses the header from a string * * @param non-empty-string $data * * @return array * * @throws UnsupportedHeaderFound When an invalid header is informed. * @throws InvalidTokenStructure When parsed content isn't an array. */ private function parseHeader(string $data): array { $header = $this->decoder->jsonDecode($this->decoder->base64UrlDecode($data)); if (! is_array($header)) { throw InvalidTokenStructure::arrayExpected('headers'); } $this->guardAgainstEmptyStringKeys($header, 'headers'); if (array_key_exists('enc', $header)) { throw UnsupportedHeaderFound::encryption(); } if (! array_key_exists('typ', $header)) { $header['typ'] = 'JWT'; } return $header; } /** * Parses the claim set from a string * * @param non-empty-string $data * * @return array * * @throws InvalidTokenStructure When parsed content isn't an array or contains non-parseable dates. */ private function parseClaims(string $data): array { $claims = $this->decoder->jsonDecode($this->decoder->base64UrlDecode($data)); if (! is_array($claims)) { throw InvalidTokenStructure::arrayExpected('claims'); } $this->guardAgainstEmptyStringKeys($claims, 'claims'); if (array_key_exists(RegisteredClaims::AUDIENCE, $claims)) { $claims[RegisteredClaims::AUDIENCE] = (array) $claims[RegisteredClaims::AUDIENCE]; } foreach (RegisteredClaims::DATE_CLAIMS as $claim) { if (! array_key_exists($claim, $claims)) { continue; } $claims[$claim] = $this->convertDate($claims[$claim]); } return $claims; } /** * @param array $array * @param non-empty-string $part * * @phpstan-assert array $array */ private function guardAgainstEmptyStringKeys(array $array, string $part): void { foreach ($array as $key => $value) { if ($key === '') { throw InvalidTokenStructure::arrayExpected($part); } } } /** @throws InvalidTokenStructure */ private function convertDate(int|float|string $timestamp): DateTimeImmutable { if (! is_numeric($timestamp)) { throw InvalidTokenStructure::dateIsNotParseable($timestamp); } $normalizedTimestamp = number_format((float) $timestamp, self::MICROSECOND_PRECISION, '.', ''); $date = DateTimeImmutable::createFromFormat('U.u', $normalizedTimestamp); if ($date === false) { throw InvalidTokenStructure::dateIsNotParseable($normalizedTimestamp); } return $date; } /** * Returns the signature from given data * * @param non-empty-string $data */ private function parseSignature(string $data): Signature { $hash = $this->decoder->base64UrlDecode($data); return new Signature($hash, $data); } } ================================================ FILE: src/Token/Plain.php ================================================ headers; } public function claims(): DataSet { return $this->claims; } public function signature(): Signature { return $this->signature; } public function payload(): string { return $this->headers->toString() . '.' . $this->claims->toString(); } public function isPermittedFor(string $audience): bool { return in_array($audience, $this->claims->get(RegisteredClaims::AUDIENCE, []), true); } public function isIdentifiedBy(string $id): bool { return $this->claims->get(RegisteredClaims::ID) === $id; } public function isRelatedTo(string $subject): bool { return $this->claims->get(RegisteredClaims::SUBJECT) === $subject; } public function hasBeenIssuedBy(string ...$issuers): bool { return in_array($this->claims->get(RegisteredClaims::ISSUER), $issuers, true); } public function hasBeenIssuedBefore(DateTimeInterface $now): bool { return $now >= $this->claims->get(RegisteredClaims::ISSUED_AT); } public function isMinimumTimeBefore(DateTimeInterface $now): bool { return $now >= $this->claims->get(RegisteredClaims::NOT_BEFORE); } public function isExpired(DateTimeInterface $now): bool { if (! $this->claims->has(RegisteredClaims::EXPIRATION_TIME)) { return false; } return $now >= $this->claims->get(RegisteredClaims::EXPIRATION_TIME); } public function toString(): string { return $this->headers->toString() . '.' . $this->claims->toString() . '.' . $this->signature->toString(); } } ================================================ FILE: src/Token/RegisteredClaimGiven.php ================================================ hash; } /** * Returns the encoded version of the signature * * @return non-empty-string */ public function toString(): string { return $this->encoded; } } ================================================ FILE: src/Token/UnsupportedHeaderFound.php ================================================ claims(); if (! $claims->has($this->claim)) { throw ConstraintViolation::error('The token does not have the claim "' . $this->claim . '"', $this); } } } ================================================ FILE: src/Validation/Constraint/HasClaimWithValue.php ================================================ claims(); if (! $claims->has($this->claim)) { throw ConstraintViolation::error('The token does not have the claim "' . $this->claim . '"', $this); } if ($claims->get($this->claim) !== $this->expectedValue) { throw ConstraintViolation::error( 'The claim "' . $this->claim . '" does not have the expected value', $this, ); } } } ================================================ FILE: src/Validation/Constraint/IdentifiedBy.php ================================================ isIdentifiedBy($this->id)) { throw ConstraintViolation::error( 'The token is not identified with the expected ID', $this, ); } } } ================================================ FILE: src/Validation/Constraint/IssuedBy.php ================================================ issuers = $issuers; } public function assert(Token $token): void { if (! $token->hasBeenIssuedBy(...$this->issuers)) { throw ConstraintViolation::error( 'The token was not issued by the given issuers', $this, ); } } } ================================================ FILE: src/Validation/Constraint/LeewayCannotBeNegative.php ================================================ leeway = $this->guardLeeway($leeway); } private function guardLeeway(?DateInterval $leeway): DateInterval { if ($leeway === null) { return new DateInterval('PT0S'); } if ($leeway->invert === 1) { throw LeewayCannotBeNegative::create(); } return $leeway; } public function assert(Token $token): void { $now = $this->clock->now(); $this->assertIssueTime($token, $now->add($this->leeway)); $this->assertMinimumTime($token, $now->add($this->leeway)); $this->assertExpiration($token, $now->sub($this->leeway)); } /** @throws ConstraintViolation */ private function assertExpiration(Token $token, DateTimeInterface $now): void { if ($token->isExpired($now)) { throw ConstraintViolation::error('The token is expired', $this); } } /** @throws ConstraintViolation */ private function assertMinimumTime(Token $token, DateTimeInterface $now): void { if (! $token->isMinimumTimeBefore($now)) { throw ConstraintViolation::error('The token cannot be used yet', $this); } } /** @throws ConstraintViolation */ private function assertIssueTime(Token $token, DateTimeInterface $now): void { if (! $token->hasBeenIssuedBefore($now)) { throw ConstraintViolation::error('The token was issued in the future', $this); } } } ================================================ FILE: src/Validation/Constraint/PermittedFor.php ================================================ isPermittedFor($this->audience)) { throw ConstraintViolation::error( 'The token is not allowed to be used by this audience', $this, ); } } } ================================================ FILE: src/Validation/Constraint/RelatedTo.php ================================================ isRelatedTo($this->subject)) { throw ConstraintViolation::error( 'The token is not related to the expected subject', $this, ); } } } ================================================ FILE: src/Validation/Constraint/SignedWith.php ================================================ headers()->get('alg') !== $this->signer->algorithmId()) { throw ConstraintViolation::error('Token signer mismatch', $this); } if (! $this->signer->verify($token->signature()->hash(), $token->payload(), $this->key)) { throw ConstraintViolation::error('Token signature mismatch', $this); } } } ================================================ FILE: src/Validation/Constraint/SignedWithOneInSet.php ================================================ */ private array $constraints; public function __construct(SignedWithUntilDate ...$constraints) { $this->constraints = $constraints; } public function assert(Token $token): void { $errorMessage = 'It was not possible to verify the signature of the token, reasons:'; foreach ($this->constraints as $constraint) { try { $constraint->assert($token); return; } catch (ConstraintViolation $violation) { $errorMessage .= PHP_EOL . '- ' . $violation->getMessage(); } } throw ConstraintViolation::error($errorMessage, $this); } } ================================================ FILE: src/Validation/Constraint/SignedWithUntilDate.php ================================================ verifySignature = new SignedWith($signer, $key); $this->clock = $clock ?? new class () implements ClockInterface { public function now(): DateTimeImmutable { return new DateTimeImmutable(); } }; } public function assert(Token $token): void { if ($this->validUntil < $this->clock->now()) { throw ConstraintViolation::error( 'This constraint was only usable until ' . $this->validUntil->format(DateTimeInterface::RFC3339), $this, ); } $this->verifySignature->assert($token); } } ================================================ FILE: src/Validation/Constraint/StrictValidAt.php ================================================ leeway = $this->guardLeeway($leeway); } private function guardLeeway(?DateInterval $leeway): DateInterval { if ($leeway === null) { return new DateInterval('PT0S'); } if ($leeway->invert === 1) { throw LeewayCannotBeNegative::create(); } return $leeway; } public function assert(Token $token): void { if (! $token instanceof UnencryptedToken) { throw ConstraintViolation::error('You should pass a plain token', $this); } $now = $this->clock->now(); $this->assertIssueTime($token, $now->add($this->leeway)); $this->assertMinimumTime($token, $now->add($this->leeway)); $this->assertExpiration($token, $now->sub($this->leeway)); } /** @throws ConstraintViolation */ private function assertExpiration(UnencryptedToken $token, DateTimeInterface $now): void { if (! $token->claims()->has(Token\RegisteredClaims::EXPIRATION_TIME)) { throw ConstraintViolation::error('"Expiration Time" claim missing', $this); } if ($token->isExpired($now)) { throw ConstraintViolation::error('The token is expired', $this); } } /** @throws ConstraintViolation */ private function assertMinimumTime(UnencryptedToken $token, DateTimeInterface $now): void { if (! $token->claims()->has(Token\RegisteredClaims::NOT_BEFORE)) { throw ConstraintViolation::error('"Not Before" claim missing', $this); } if (! $token->isMinimumTimeBefore($now)) { throw ConstraintViolation::error('The token cannot be used yet', $this); } } /** @throws ConstraintViolation */ private function assertIssueTime(UnencryptedToken $token, DateTimeInterface $now): void { if (! $token->claims()->has(Token\RegisteredClaims::ISSUED_AT)) { throw ConstraintViolation::error('"Issued At" claim missing', $this); } if (! $token->hasBeenIssuedBefore($now)) { throw ConstraintViolation::error('The token was issued in the future', $this); } } } ================================================ FILE: src/Validation/Constraint.php ================================================ |null $constraint */ public function __construct( string $message = '', public readonly ?string $constraint = null, ) { parent::__construct($message); } /** @param non-empty-string $message */ public static function error(string $message, Constraint $constraint): self { return new self(message: $message, constraint: $constraint::class); } } ================================================ FILE: src/Validation/NoConstraintsGiven.php ================================================ getMessage(); }, $violations, ); $message = "The token violates some mandatory constraints, details:\n"; $message .= implode("\n", $violations); return $message; } /** @return ConstraintViolation[] */ public function violations(): array { return $this->violations; } } ================================================ FILE: src/Validation/SignedWith.php ================================================ checkConstraint($constraint, $token, $violations); } if ($violations !== []) { throw RequiredConstraintsViolated::fromViolations(...$violations); } } /** @param ConstraintViolation[] $violations */ private function checkConstraint( Constraint $constraint, Token $token, array &$violations, ): void { try { $constraint->assert($token); } catch (ConstraintViolation $e) { $violations[] = $e; } } public function validate(Token $token, Constraint ...$constraints): bool { if ($constraints === []) { throw new NoConstraintsGiven('No constraint given.'); } try { foreach ($constraints as $constraint) { $constraint->assert($token); } return true; } catch (ConstraintViolation) { return false; } } } ================================================ FILE: src/Validator.php ================================================ ['HS256', 'HS384', 'HS512'], 'rsa' => ['RS256', 'RS384', 'RS512'], 'ecdsa' => ['ES256', 'ES384', 'ES512'], 'eddsa' => ['EdDSA'], 'blake2b' => ['BLAKE2B'], ]; protected const string PAYLOAD = "It\xe2\x80\x99s a dangerous business, Frodo, going out your door. You step" . " onto the road, and if you don't keep your feet, there\xe2\x80\x99s no knowing where you might be swept" . ' off to.'; #[Bench\Subject] #[Bench\ParamProviders('hmacAlgorithms')] #[Bench\Groups(['hmac', 'symmetric'])] public function hmac(): void { $this->runBenchmark(); } /** @return iterable */ public function hmacAlgorithms(): iterable { yield from $this->iterateAlgorithms('hmac'); } #[Bench\Subject] #[Bench\ParamProviders('rsaAlgorithms')] #[Bench\Groups(['rsa', 'asymmetric'])] public function rsa(): void { $this->runBenchmark(); } /** @return iterable */ public function rsaAlgorithms(): iterable { yield from $this->iterateAlgorithms('rsa'); } #[Bench\Subject] #[Bench\ParamProviders('ecdsaAlgorithms')] #[Bench\Groups(['ecdsa', 'asymmetric'])] public function ecdsa(): void { $this->runBenchmark(); } /** @return iterable */ public function ecdsaAlgorithms(): iterable { yield from $this->iterateAlgorithms('ecdsa'); } #[Bench\Subject] #[Bench\ParamProviders('eddsaAlgorithms')] #[Bench\Groups(['eddsa', 'asymmetric'])] public function eddsa(): void { $this->runBenchmark(); } /** @return iterable */ public function eddsaAlgorithms(): iterable { yield from $this->iterateAlgorithms('eddsa'); } #[Bench\Subject] #[Bench\ParamProviders('blake2bAlgorithms')] #[Bench\Groups(['blake2b', 'symmetric'])] public function blake2b(): void { $this->runBenchmark(); } /** @return iterable */ public function blake2bAlgorithms(): iterable { yield from $this->iterateAlgorithms('blake2b'); } abstract protected function runBenchmark(): void; protected function resolveAlgorithm(string $name): Signer { return match ($name) { 'HS256' => new Signer\Hmac\Sha256(), 'HS384' => new Signer\Hmac\Sha384(), 'HS512' => new Signer\Hmac\Sha512(), 'RS256' => new Signer\Rsa\Sha256(), 'RS384' => new Signer\Rsa\Sha384(), 'RS512' => new Signer\Rsa\Sha512(), 'ES256' => new Signer\Ecdsa\Sha256(), 'ES384' => new Signer\Ecdsa\Sha384(), 'ES512' => new Signer\Ecdsa\Sha512(), 'EdDSA' => new Signer\Eddsa(), 'BLAKE2B' => new Signer\Blake2b(), default => throw new RuntimeException('Unknown algorithm'), }; } protected function resolveSigningKey(string $name): Key { return match ($name) { 'HS256' => InMemory::base64Encoded('n5p7sBK+dvBmSKNlQIFrsuB1cnmnwsxGyWXPgRSZtWY='), 'HS384' => InMemory::base64Encoded('kNUb8KvJC+fvhPzIuimwWHleES3AAnUjI+UIWZyor5HT33st9KIjfPkgtfu60UL2'), 'HS512' => InMemory::base64Encoded( 'OgXKIs+aZCQgXnDfi8mAFnWVo+Xn3JTR7BvT/j1Q1zP9oRx9xGg4jmpq00RsPPDclYi8+jRl664pu4d0zan2ow==', ), 'RS256', 'RS384', 'RS512' => InMemory::file(__DIR__ . '/Rsa/private.key'), 'ES256' => InMemory::file(__DIR__ . '/Ecdsa/private-256.key'), 'ES384' => InMemory::file(__DIR__ . '/Ecdsa/private-384.key'), 'ES512' => InMemory::file(__DIR__ . '/Ecdsa/private-521.key'), 'EdDSA' => InMemory::base64Encoded( 'K3NWT0XqaH+4jgi42gQmHnFE+HTPVhFYi3u4DFJ3OpRHRMt/aGRBoKD/Pt5H/iYgGCla7Q04CdjOUpLSrjZhtg==', ), 'BLAKE2B' => InMemory::base64Encoded('b6DNRcX2SFapbICe6lXWYoOZA+JXL/dvkfWiv2hJv3Y='), default => throw new RuntimeException('Unknown algorithm'), }; } protected function resolveVerificationKey(string $name): Key { return match ($name) { 'HS256', 'HS384', 'HS512', 'BLAKE2B' => $this->resolveSigningKey($name), 'RS256', 'RS384', 'RS512' => InMemory::file(__DIR__ . '/Rsa/public.key'), 'ES256' => InMemory::file(__DIR__ . '/Ecdsa/public-256.key'), 'ES384' => InMemory::file(__DIR__ . '/Ecdsa/public-384.key'), 'ES512' => InMemory::file(__DIR__ . '/Ecdsa/public-521.key'), 'EdDSA' => InMemory::base64Encoded('R0TLf2hkQaCg/z7eR/4mIBgpWu0NOAnYzlKS0q42YbY='), default => throw new RuntimeException('Unknown algorithm'), }; } /** @return iterable */ private function iterateAlgorithms(string $family): iterable { foreach (self::SUPPORTED_ALGORITHMS[$family] ?? [] as $algorithm) { yield $algorithm => ['algorithm' => $algorithm]; } } } ================================================ FILE: tests/Benchmark/CreateSignatureBench.php ================================================ algorithm = $this->resolveAlgorithm($params['algorithm']); $this->key = $this->resolveSigningKey($params['algorithm']); } protected function runBenchmark(): void { $void = $this->algorithm->sign(self::PAYLOAD, $this->key); } } ================================================ FILE: tests/Benchmark/Ecdsa/private-256.key ================================================ -----BEGIN EC PRIVATE KEY----- MHcCAQEEIK/5B8mfmtOq5sTN8hEivOK9aLUoPmkHFUrZEYQPogjPoAoGCCqGSM49 AwEHoUQDQgAEZe2loSV3wrroKUN/4zhwGhCqo3Xhu1td4QjeQ5wIVR0eUu11cBFj 9/nkDd+fNBs9ybqGCvfgynyn6e7NAITRnA== -----END EC PRIVATE KEY----- ================================================ FILE: tests/Benchmark/Ecdsa/private-384.key ================================================ -----BEGIN EC PRIVATE KEY----- MIGkAgEBBDDZi2q9Sdoiu0XNsrdD/PLBtgAr48L4MWy7XMiND8riSeJkTYnhPlra xMSRKkd2OhSgBwYFK4EEACKhZANiAAR+oJdVSn/ZrdLRzsad6Dv7bOVLdPkc0GZu n5//VFLgobl2lxEhRvWxW0tBRkJKj8tnVRWaWbe0L1C7QCtGN6NnSxpn6Y4FhtE2 prdfXjnjWAQI0+TdPeCvQtOFQjrLWrw= -----END EC PRIVATE KEY----- ================================================ FILE: tests/Benchmark/Ecdsa/private-521.key ================================================ -----BEGIN EC PRIVATE KEY----- MIHcAgEBBEIBCwZmxfodGCjbu5tgb4al9Qwv36dS9lCYk4Hjq6VMMneMH2tlDaS1 kEid5mVnJrznhLJFn5IO3mB+FC/V1q2RKQigBwYFK4EEACOhgYkDgYYABAGjhQzd +mwlkFVzY2Ak1fW+DMrqPxszQO6SR8cqpcAhb9BSR9whqghljOU1X9cJe/A6/2WF WqTRpj6RaRkzot6KbwEC0jo08XIXdyWkp4AsNbLKPDaO2DZH/8LMkeouNHxJJC/8 /nU+5kOPBeoZV9qodJYOhnkxiNjHJjrFL8YYRXgUTw== -----END EC PRIVATE KEY----- ================================================ FILE: tests/Benchmark/Ecdsa/public-256.key ================================================ -----BEGIN PUBLIC KEY----- MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEZe2loSV3wrroKUN/4zhwGhCqo3Xh u1td4QjeQ5wIVR0eUu11cBFj9/nkDd+fNBs9ybqGCvfgynyn6e7NAITRnA== -----END PUBLIC KEY----- ================================================ FILE: tests/Benchmark/Ecdsa/public-384.key ================================================ -----BEGIN PUBLIC KEY----- MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEfqCXVUp/2a3S0c7Gneg7+2zlS3T5HNBm bp+f/1RS4KG5dpcRIUb1sVtLQUZCSo/LZ1UVmlm3tC9Qu0ArRjejZ0saZ+mOBYbR Nqa3X14541gECNPk3T3gr0LThUI6y1q8 -----END PUBLIC KEY----- ================================================ FILE: tests/Benchmark/Ecdsa/public-521.key ================================================ -----BEGIN PUBLIC KEY----- MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBo4UM3fpsJZBVc2NgJNX1vgzK6j8b M0DukkfHKqXAIW/QUkfcIaoIZYzlNV/XCXvwOv9lhVqk0aY+kWkZM6Leim8BAtI6 NPFyF3clpKeALDWyyjw2jtg2R//CzJHqLjR8SSQv/P51PuZDjwXqGVfaqHSWDoZ5 MYjYxyY6xS/GGEV4FE8= -----END PUBLIC KEY----- ================================================ FILE: tests/Benchmark/IssueTokenBench.php ================================================ algorithm = $this->resolveAlgorithm($params['algorithm']); $this->key = $this->resolveSigningKey($params['algorithm']); } protected function runBenchmark(): void { $void = (new JwtFacade())->issue( $this->algorithm, $this->key, static fn (Builder $builder): Builder => $builder ->identifiedBy('token-1') ->issuedBy('lcobucci.jwt.benchmarks') ->relatedTo('user-1') ->permittedFor('lcobucci.jwt'), ); } } ================================================ FILE: tests/Benchmark/ParseTokenBench.php ================================================ algorithm = $this->resolveAlgorithm($params['algorithm']); $this->key = $this->resolveVerificationKey($params['algorithm']); $this->jwt = (new JwtFacade())->issue( $this->algorithm, $this->resolveSigningKey($params['algorithm']), static fn (Builder $builder): Builder => $builder ->identifiedBy('token-1') ->issuedBy('lcobucci.jwt.benchmarks') ->relatedTo('user-1') ->permittedFor('lcobucci.jwt'), )->toString(); } protected function runBenchmark(): void { $void = (new JwtFacade())->parse( $this->jwt, new Constraint\SignedWith($this->algorithm, $this->key), new Constraint\StrictValidAt(SystemClock::fromSystemTimezone()), new Constraint\IssuedBy('lcobucci.jwt.benchmarks'), new Constraint\RelatedTo('user-1'), new Constraint\PermittedFor('lcobucci.jwt'), new Constraint\IdentifiedBy('token-1'), ); } } ================================================ FILE: tests/Benchmark/Rsa/private.key ================================================ -----BEGIN RSA PRIVATE KEY----- MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCfgQ+0A4Jz0CWR 5Ac/MdK2ABuCzttNkvBQFl1Hz8q4o8Qct3isdVN5P475dXaNGiN02HElZMO813ue pDRUSJlAfP8AmZIKkxokxEFIUqspvbCpXAZT82xg5gv5C2JY3aVvNwR7pcLR0Cmv nJ1AuseqQceKDdEGit1pnoCP6gEeoUQdik97tOl7459V8d3UTpxLozUVlwPU00tg PmUUek8j1tPAmWx17e6EaoLRkK4QeDyWHPA4eu0hBtLQVVtv2Tf61VNTh+D/cv++ eJQUArC4IuoqdLYFjB2r+bNKdstjuH+qLGhHuOKDf/+RGG5rHBSRHPmJqJCSqBzm Ad2s0/nPAgMBAAECggEAbWUC9B+EFRIo8kpGfh0ZuyGPvMNKvYWNtB/ikiH9k20e T+O1q/I78eiZkpXxXQ0UTEs2LsNRS+8uJbvQ+A1irkwMSMkK1J3XTGgdrhCku9gR ldY7sNA/AKZGh+Q661/42rINLRCe8W+nZ34ui/qOfkLnK9QWDDqpaIsA+bMwWWSD Fu2MUBYwkHTMEzLYGqOe04noqeq1hExBTHBOBdkMXiuFhUq1BU6l+DqEiWxqg82s Xt2h+LMnT3046AOYJoRioz75tSUQfGCshWTBnP5uDjd18kKhyv07lhfSJdrPdM5P lyl21hsFf4L/mHCuoFau7gdsPfHPxxjVOcOpBrQzwQKBgQDdKXGD8PBNclxvrT3l GhfKBAIBnlGcC9mWejXKEe2dR7H9+nsBn/2dFo7sdf/5IV8ZB661qjZMOMMBZThW 6mTyvD0lHQDNnQ3Z++4gCav9YKyYal42pCd6/VPsjISyeHxQy36fkJp+GSKTOESy uad0fovE6u9EmWw+npm/xtSrSQKBgQC4oTZ2H5xN/oREXiTh7+PLvwZ89hQhpTKh JIm4HOncK5uTc4KqzqCtPxtH9y7QObUxnBaa12oPIj3ketR6rcw/Xm8ww43yUdN5 m7aWYq/CpbtqdXlTOEzWJnvPjIyS5TAVagG/Jjz3wRe9EP6F2pHEeVKoBnX3bMHe lUUnSzukVwKBgAfD1b15Lya49i/hmEO798va+isOYPUmoVwcLFlM6dfU1ZYCPmFf OatTSG9a8ULQ/iLF10d/k2p3r7kT0beTgTnYjBkKfKW7duoJY2HylPxPcZ/kVCx8 9PnnfRPYFyyg+FRp4Kc/j30P6tvaZOcVh6CadNPUH9R7woYsUV+fXoYpAoGACLDm DGduhylc9o7r84rEUVn7pzQ6PF83Y+iBZx5NT+TpnOZKF1pErAMVeKzFEl41DlHH qqBLSM0W1sOFbwTxYWZDm6sI6og5iTbwQGIC3gnJKbi/7k/vJgGHwHxgPaX2PnvP +zyEkDERuf+ry4c/Z11Cq9AqC2yeL6kdKT1cYF8CgYEA3PiqvXQN0zwMeE+sBvZg i289XP9XCQF3VWqPzMKnIgQp7/Tugo6+NZBKCQsMf3HaEGBjTVJs/jcK8+TRXvaK e+7ZMaQj8VfBdYkssbu0NKDDhjJ+GtiseaDVWt7dcH0cfwxgFUHpQh7FoCrjFJ6h 6ZEpMF6xmujs4qMpPz8aaI4= -----END RSA PRIVATE KEY----- ================================================ FILE: tests/Benchmark/Rsa/public.key ================================================ -----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAn4EPtAOCc9AlkeQHPzHS tgAbgs7bTZLwUBZdR8/KuKPEHLd4rHVTeT+O+XV2jRojdNhxJWTDvNd7nqQ0VEiZ QHz/AJmSCpMaJMRBSFKrKb2wqVwGU/NsYOYL+QtiWN2lbzcEe6XC0dApr5ydQLrH qkHHig3RBordaZ6Aj+oBHqFEHYpPe7Tpe+OfVfHd1E6cS6M1FZcD1NNLYD5lFHpP I9bTwJlsde3uhGqC0ZCuEHg8lhzwOHrtIQbS0FVbb9k3+tVTU4fg/3L/vniUFAKw uCLqKnS2BYwdq/mzSnbLY7h/qixoR7jig3//kRhuaxwUkRz5iaiQkqgc5gHdrNP5 zwIDAQAB -----END PUBLIC KEY----- ================================================ FILE: tests/Benchmark/VerifySignatureBench.php ================================================ algorithm = $this->resolveAlgorithm($params['algorithm']); $this->key = $this->resolveVerificationKey($params['algorithm']); $this->signature = $this->algorithm->sign( self::PAYLOAD, $this->resolveSigningKey($params['algorithm']), ); } protected function runBenchmark(): void { $void = $this->algorithm->verify($this->signature, self::PAYLOAD, $this->key); } } ================================================ FILE: tests/ConfigurationTest.php ================================================ signer = self::createStub(Signer::class); $this->encoder = self::createStub(Encoder::class); $this->decoder = self::createStub(Decoder::class); $this->parser = self::createStub(Parser::class); $this->validator = self::createStub(Validator::class); $this->validationConstraints = self::createStub(Constraint::class); } #[PHPUnit\Test] public function forAsymmetricSignerShouldConfigureSignerAndBothKeys(): void { $signingKey = InMemory::plainText('private'); $verificationKey = InMemory::plainText('public'); $config = Configuration::forAsymmetricSigner($this->signer, $signingKey, $verificationKey); self::assertSame($this->signer, $config->signer()); self::assertSame($signingKey, $config->signingKey()); self::assertSame($verificationKey, $config->verificationKey()); } #[PHPUnit\Test] public function forSymmetricSignerShouldConfigureSignerAndBothKeys(): void { $key = InMemory::plainText('private'); $config = Configuration::forSymmetricSigner($this->signer, $key); self::assertSame($this->signer, $config->signer()); self::assertSame($key, $config->signingKey()); self::assertSame($key, $config->verificationKey()); } #[PHPUnit\Test] public function builderShouldCreateABuilderWithDefaultEncoderAndClaimFactory(): void { $config = Configuration::forSymmetricSigner( new KeyDumpSigner(), InMemory::plainText('private'), ); $builder = $config->builder(); self::assertInstanceOf(BuilderImpl::class, $builder); self::assertNotEquals(BuilderImpl::new($this->encoder, ChainedFormatter::default()), $builder); self::assertEquals(BuilderImpl::new(new JoseEncoder(), ChainedFormatter::default()), $builder); } #[PHPUnit\Test] public function builderShouldCreateABuilderWithCustomizedEncoderAndClaimFactory(): void { $config = Configuration::forSymmetricSigner( new KeyDumpSigner(), InMemory::plainText('private'), $this->encoder, ); $builder = $config->builder(); self::assertInstanceOf(BuilderImpl::class, $builder); self::assertEquals(BuilderImpl::new($this->encoder, ChainedFormatter::default()), $builder); } #[PHPUnit\Test] public function builderShouldUseBuilderFactoryWhenThatIsConfigured(): void { $builder = self::createStub(Builder::class); $config = Configuration::forSymmetricSigner( new KeyDumpSigner(), InMemory::plainText('private'), ); $newConfig = $config->withBuilderFactory( static function () use ($builder): Builder { return $builder; }, ); self::assertNotSame($builder, $config->builder()); self::assertSame($builder, $newConfig->builder()); } #[PHPUnit\Test] public function parserShouldReturnAParserWithDefaultDecoder(): void { $config = Configuration::forSymmetricSigner( new KeyDumpSigner(), InMemory::plainText('private'), ); $parser = $config->parser(); self::assertNotEquals(new ParserImpl($this->decoder), $parser); } #[PHPUnit\Test] public function parserShouldReturnAParserWithCustomizedDecoder(): void { $config = Configuration::forSymmetricSigner( new KeyDumpSigner(), InMemory::plainText('private'), decoder: $this->decoder, ); $parser = $config->parser(); self::assertEquals(new ParserImpl($this->decoder), $parser); } #[PHPUnit\Test] public function parserShouldNotCreateAnInstanceIfItWasConfigured(): void { $config = Configuration::forSymmetricSigner( new KeyDumpSigner(), InMemory::plainText('private'), ); $newConfig = $config->withParser($this->parser); self::assertNotSame($this->parser, $config->parser()); self::assertSame($this->parser, $newConfig->parser()); } #[PHPUnit\Test] public function validatorShouldReturnTheDefaultWhenItWasNotConfigured(): void { $config = Configuration::forSymmetricSigner( new KeyDumpSigner(), InMemory::plainText('private'), ); $validator = $config->validator(); self::assertNotSame($this->validator, $validator); } #[PHPUnit\Test] public function validatorShouldReturnTheConfiguredValidator(): void { $config = Configuration::forSymmetricSigner( new KeyDumpSigner(), InMemory::plainText('private'), ); $newConfig = $config->withValidator($this->validator); self::assertNotSame($this->validator, $config->validator()); self::assertSame($this->validator, $newConfig->validator()); } #[PHPUnit\Test] public function validationConstraintsShouldReturnAnEmptyArrayWhenItWasNotConfigured(): void { $config = Configuration::forSymmetricSigner( new KeyDumpSigner(), InMemory::plainText('private'), ); self::assertSame([], $config->validationConstraints()); } #[PHPUnit\Test] public function validationConstraintsShouldReturnTheConfiguredValidator(): void { $config = Configuration::forSymmetricSigner( new KeyDumpSigner(), InMemory::plainText('private'), ); $newConfig = $config->withValidationConstraints($this->validationConstraints); self::assertNotSame([$this->validationConstraints], $config->validationConstraints()); self::assertSame([$this->validationConstraints], $newConfig->validationConstraints()); } #[PHPUnit\Test] public function customClaimFormatterCanBeUsed(): void { $formatter = self::createStub(ClaimsFormatter::class); $config = Configuration::forSymmetricSigner( new KeyDumpSigner(), InMemory::plainText('private'), ); self::assertEquals(BuilderImpl::new(new JoseEncoder(), $formatter), $config->builder($formatter)); } } ================================================ FILE: tests/ES512TokenTest.php ================================================ config = Configuration::forAsymmetricSigner( new Sha512(), static::$ecdsaKeys['private_ec512'], static::$ecdsaKeys['public_ec512'], ); } #[PHPUnit\Test] public function builderShouldRaiseExceptionWhenKeyIsInvalid(): void { $builder = $this->config->builder() ->identifiedBy('1') ->permittedFor('https://client.abc.com') ->issuedBy('https://api.abc.com') ->withClaim('user', ['name' => 'testing', 'email' => 'testing@abc.com']); $this->expectException(InvalidKeyProvided::class); $this->expectExceptionMessage('It was not possible to parse your key, reason:'); $void = $builder->getToken($this->config->signer(), InMemory::plainText('testing')); } #[PHPUnit\Test] public function builderShouldRaiseExceptionWhenKeyIsNotEcdsaCompatible(): void { $builder = $this->config->builder() ->identifiedBy('1') ->permittedFor('https://client.abc.com') ->issuedBy('https://api.abc.com') ->withClaim('user', ['name' => 'testing', 'email' => 'testing@abc.com']); $this->expectException(InvalidKeyProvided::class); $this->expectExceptionMessage('The type of the provided key is not "EC", "RSA" provided'); $void = $builder->getToken($this->config->signer(), static::$rsaKeys['private']); } #[PHPUnit\Test] public function builderCanGenerateAToken(): Token { $user = ['name' => 'testing', 'email' => 'testing@abc.com']; $builder = $this->config->builder(); $token = $builder->identifiedBy('1') ->permittedFor('https://client.abc.com') ->permittedFor('https://client2.abc.com') ->issuedBy('https://api.abc.com') ->withClaim('user', $user) ->withHeader('jki', '1234') ->getToken($this->config->signer(), $this->config->signingKey()); self::assertSame('1234', $token->headers()->get('jki')); self::assertSame('https://api.abc.com', $token->claims()->get(Token\RegisteredClaims::ISSUER)); self::assertSame($user, $token->claims()->get('user')); self::assertSame( ['https://client.abc.com', 'https://client2.abc.com'], $token->claims()->get(Token\RegisteredClaims::AUDIENCE), ); return $token; } #[PHPUnit\Test] #[PHPUnit\Depends('builderCanGenerateAToken')] public function parserCanReadAToken(Token $generated): void { $read = $this->config->parser()->parse($generated->toString()); assert($read instanceof Token\Plain); self::assertEquals($generated, $read); self::assertSame('testing', $read->claims()->get('user')['name']); } #[PHPUnit\Test] #[PHPUnit\Depends('builderCanGenerateAToken')] public function signatureAssertionShouldRaiseExceptionWhenKeyIsNotRight(Token $token): void { $this->expectException(RequiredConstraintsViolated::class); $this->expectExceptionMessage('The token violates some mandatory constraints'); $this->config->validator()->assert( $token, new SignedWith( $this->config->signer(), self::$ecdsaKeys['public2_ec512'], ), ); } #[PHPUnit\Test] #[PHPUnit\Depends('builderCanGenerateAToken')] public function signatureAssertionShouldRaiseExceptionWhenAlgorithmIsDifferent(Token $token): void { $this->expectException(RequiredConstraintsViolated::class); $this->expectExceptionMessage('The token violates some mandatory constraints'); $this->config->validator()->assert( $token, new SignedWith( new Sha256(), self::$ecdsaKeys['public_ec512'], ), ); } #[PHPUnit\Test] #[PHPUnit\Depends('builderCanGenerateAToken')] public function signatureAssertionShouldRaiseExceptionWhenKeyIsNotEcdsaCompatible(Token $token): void { $this->expectException(InvalidKeyProvided::class); $this->expectExceptionMessage('The type of the provided key is not "EC", "RSA" provided'); $this->config->validator()->assert( $token, new SignedWith($this->config->signer(), self::$rsaKeys['public']), ); } #[PHPUnit\Test] #[PHPUnit\Depends('builderCanGenerateAToken')] public function signatureValidationShouldSucceedWhenKeyIsRight(Token $token): void { $constraint = new SignedWith( $this->config->signer(), $this->config->verificationKey(), ); self::assertTrue($this->config->validator()->validate($token, $constraint)); } } ================================================ FILE: tests/EcdsaTokenTest.php ================================================ config = Configuration::forAsymmetricSigner( new Sha256(), static::$ecdsaKeys['private'], static::$ecdsaKeys['public1'], ); } #[PHPUnit\Test] public function builderShouldRaiseExceptionWhenKeyIsInvalid(): void { $builder = $this->config->builder() ->identifiedBy('1') ->permittedFor('https://client.abc.com') ->issuedBy('https://api.abc.com') ->withClaim('user', ['name' => 'testing', 'email' => 'testing@abc.com']); $this->expectException(InvalidKeyProvided::class); $this->expectExceptionMessage('It was not possible to parse your key, reason:'); $void = $builder->getToken($this->config->signer(), InMemory::plainText('testing')); } #[PHPUnit\Test] public function builderShouldRaiseExceptionWhenKeyIsNotEcdsaCompatible(): void { $builder = $this->config->builder() ->identifiedBy('1') ->permittedFor('https://client.abc.com') ->issuedBy('https://api.abc.com') ->withClaim('user', ['name' => 'testing', 'email' => 'testing@abc.com']); $this->expectException(InvalidKeyProvided::class); $this->expectExceptionMessage('The type of the provided key is not "EC", "RSA" provided'); $void = $builder->getToken($this->config->signer(), static::$rsaKeys['private']); } #[PHPUnit\Test] public function builderCanGenerateAToken(): Token { $user = ['name' => 'testing', 'email' => 'testing@abc.com']; $builder = $this->config->builder(); $token = $builder->identifiedBy('1') ->permittedFor('https://client.abc.com') ->permittedFor('https://client2.abc.com') ->issuedBy('https://api.abc.com') ->withClaim('user', $user) ->withHeader('jki', '1234') ->getToken($this->config->signer(), $this->config->signingKey()); self::assertSame('1234', $token->headers()->get('jki')); self::assertSame('https://api.abc.com', $token->claims()->get(Token\RegisteredClaims::ISSUER)); self::assertSame($user, $token->claims()->get('user')); self::assertSame( ['https://client.abc.com', 'https://client2.abc.com'], $token->claims()->get(Token\RegisteredClaims::AUDIENCE), ); return $token; } #[PHPUnit\Test] #[PHPUnit\Depends('builderCanGenerateAToken')] public function parserCanReadAToken(Token $generated): void { $read = $this->config->parser()->parse($generated->toString()); assert($read instanceof Token\Plain); self::assertEquals($generated, $read); self::assertSame('testing', $read->claims()->get('user')['name']); } #[PHPUnit\Test] #[PHPUnit\Depends('builderCanGenerateAToken')] public function signatureAssertionShouldRaiseExceptionWhenKeyIsNotRight(Token $token): void { $this->expectException(RequiredConstraintsViolated::class); $this->expectExceptionMessage('The token violates some mandatory constraints'); $this->config->validator()->assert( $token, new SignedWith( $this->config->signer(), self::$ecdsaKeys['public2'], ), ); } #[PHPUnit\Test] #[PHPUnit\Depends('builderCanGenerateAToken')] public function signatureAssertionShouldRaiseExceptionWhenAlgorithmIsDifferent(Token $token): void { $this->expectException(RequiredConstraintsViolated::class); $this->expectExceptionMessage('The token violates some mandatory constraints'); $this->config->validator()->assert( $token, new SignedWith( new Sha512(), self::$ecdsaKeys['public1'], ), ); } #[PHPUnit\Test] #[PHPUnit\Depends('builderCanGenerateAToken')] public function signatureAssertionShouldRaiseExceptionWhenKeyIsNotEcdsaCompatible(Token $token): void { $this->expectException(InvalidKeyProvided::class); $this->expectExceptionMessage('The type of the provided key is not "EC", "RSA" provided'); $this->config->validator()->assert( $token, new SignedWith($this->config->signer(), self::$rsaKeys['public']), ); } #[PHPUnit\Test] #[PHPUnit\Depends('builderCanGenerateAToken')] public function signatureValidationShouldSucceedWhenKeyIsRight(Token $token): void { $constraint = new SignedWith( $this->config->signer(), $this->config->verificationKey(), ); self::assertTrue($this->config->validator()->validate($token, $constraint)); } #[PHPUnit\Test] public function everythingShouldWorkWithAKeyWithParams(): void { $builder = $this->config->builder(); $signer = $this->config->signer(); $token = $builder->identifiedBy('1') ->permittedFor('https://client.abc.com') ->issuedBy('https://api.abc.com') ->withClaim('user', ['name' => 'testing', 'email' => 'testing@abc.com']) ->withHeader('jki', '1234') ->getToken($signer, static::$ecdsaKeys['private-params']); $constraint = new SignedWith( $this->config->signer(), static::$ecdsaKeys['public-params'], ); self::assertTrue($this->config->validator()->validate($token, $constraint)); } #[PHPUnit\Test] public function everythingShouldWorkWhenUsingATokenGeneratedByOtherLibs(): void { $data = 'eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJoZWxsbyI6IndvcmxkIn0.' . 'AQx1MqdTni6KuzfOoedg2-7NUiwe-b88SWbdmviz40GTwrM0Mybp1i1tVtm' . 'TSQ91oEXGXBdtwsN6yalzP9J-sp2YATX_Tv4h-BednbdSvYxZsYnUoZ--ZU' . 'dL10t7g8Yt3y9hdY_diOjIptcha6ajX8yzkDGYG42iSe3f5LywSuD6FO5c'; $key = '-----BEGIN PUBLIC KEY-----' . PHP_EOL . 'MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQAcpkss6wI7PPlxj3t7A1RqMH3nvL4' . PHP_EOL . 'L5Tzxze/XeeYZnHqxiX+gle70DlGRMqqOq+PJ6RYX7vK0PJFdiAIXlyPQq0B3KaU' . PHP_EOL . 'e86IvFeQSFrJdCc0K8NfiH2G1loIk3fiR+YLqlXk6FAeKtpXJKxR1pCQCAM+vBCs' . PHP_EOL . 'mZudf1zCUZ8/4eodlHU=' . PHP_EOL . '-----END PUBLIC KEY-----'; $token = $this->config->parser()->parse($data); assert($token instanceof Token\Plain); $constraint = new SignedWith(new Sha512(), InMemory::plainText($key)); self::assertTrue($this->config->validator()->validate($token, $constraint)); self::assertSame('world', $token->claims()->get('hello')); } } ================================================ FILE: tests/EddsaTokenTest.php ================================================ config = Configuration::forAsymmetricSigner( new Eddsa(), static::$eddsaKeys['private'], static::$eddsaKeys['public1'], ); } #[PHPUnit\Test] public function builderShouldRaiseExceptionWhenKeyIsInvalid(): void { $builder = $this->config->builder() ->identifiedBy('1') ->permittedFor('https://client.abc.com') ->issuedBy('https://api.abc.com') ->withClaim('user', ['name' => 'testing', 'email' => 'testing@abc.com']); $this->expectException(InvalidKeyProvided::class); $this->expectExceptionMessage('SODIUM_CRYPTO_SIGN_SECRETKEYBYTES'); $void = $builder->getToken($this->config->signer(), InMemory::plainText('testing')); } #[PHPUnit\Test] public function builderCanGenerateAToken(): Token { $user = ['name' => 'testing', 'email' => 'testing@abc.com']; $builder = $this->config->builder(); $token = $builder->identifiedBy('1') ->permittedFor('https://client.abc.com') ->permittedFor('https://client2.abc.com') ->issuedBy('https://api.abc.com') ->withClaim('user', $user) ->withHeader('jki', '1234') ->getToken($this->config->signer(), $this->config->signingKey()); self::assertSame('1234', $token->headers()->get('jki')); self::assertSame('https://api.abc.com', $token->claims()->get(Token\RegisteredClaims::ISSUER)); self::assertSame($user, $token->claims()->get('user')); self::assertSame( ['https://client.abc.com', 'https://client2.abc.com'], $token->claims()->get(Token\RegisteredClaims::AUDIENCE), ); return $token; } #[PHPUnit\Test] #[PHPUnit\Depends('builderCanGenerateAToken')] public function parserCanReadAToken(Token $generated): void { $read = $this->config->parser()->parse($generated->toString()); assert($read instanceof Token\Plain); self::assertEquals($generated, $read); self::assertSame('testing', $read->claims()->get('user')['name']); } #[PHPUnit\Test] #[PHPUnit\Depends('builderCanGenerateAToken')] public function signatureAssertionShouldRaiseExceptionWhenKeyIsNotRight(Token $token): void { $this->expectException(RequiredConstraintsViolated::class); $this->expectExceptionMessage('The token violates some mandatory constraints'); $this->config->validator()->assert( $token, new SignedWith( $this->config->signer(), self::$eddsaKeys['public2'], ), ); } #[PHPUnit\Test] #[PHPUnit\Depends('builderCanGenerateAToken')] public function signatureValidationShouldSucceedWhenKeyIsRight(Token $token): void { $constraint = new SignedWith( $this->config->signer(), $this->config->verificationKey(), ); self::assertTrue($this->config->validator()->validate($token, $constraint)); } } ================================================ FILE: tests/Encoding/ChainedFormatterTest.php ================================================ ['test'], RegisteredClaims::EXPIRATION_TIME => $expiration, ]; $formatter = ChainedFormatter::default(); $formatted = $formatter->formatClaims($claims); self::assertSame('test', $formatted[RegisteredClaims::AUDIENCE]); self::assertSame(1487285080.123456, $formatted[RegisteredClaims::EXPIRATION_TIME]); $formatter = ChainedFormatter::withUnixTimestampDates(); $formatted = $formatter->formatClaims($claims); self::assertSame('test', $formatted[RegisteredClaims::AUDIENCE]); self::assertSame(1487285080, $formatted[RegisteredClaims::EXPIRATION_TIME]); } } ================================================ FILE: tests/Encoding/JoseEncoderTest.php ================================================ jsonEncode(['test' => 'test'])); } #[PHPUnit\Test] public function jsonEncodeShouldNotEscapeUnicode(): void { $encoder = new JoseEncoder(); self::assertSame('"汉语"', $encoder->jsonEncode('汉语')); } #[PHPUnit\Test] public function jsonEncodeShouldNotEscapeSlashes(): void { $encoder = new JoseEncoder(); self::assertSame('"https://google.com"', $encoder->jsonEncode('https://google.com')); } #[PHPUnit\Test] public function jsonEncodeMustRaiseExceptionWhenAnErrorHasOccurred(): void { $encoder = new JoseEncoder(); $this->expectException(CannotEncodeContent::class); $this->expectExceptionCode(0); $this->expectExceptionMessage('Error while encoding to JSON'); $encoder->jsonEncode("\xB1\x31"); } #[PHPUnit\Test] public function jsonDecodeMustReturnTheDecodedData(): void { $decoder = new JoseEncoder(); self::assertSame( ['test' => ['test' => []]], $decoder->jsonDecode('{"test":{"test":{}}}'), ); } #[PHPUnit\Test] public function jsonDecodeMustRaiseExceptionWhenAnErrorHasOccurred(): void { $decoder = new JoseEncoder(); $this->expectException(CannotDecodeContent::class); $this->expectExceptionCode(0); $this->expectExceptionMessage('Error while decoding from JSON'); $decoder->jsonDecode('{"test":\'test\'}'); } #[PHPUnit\Test] public function base64UrlEncodeMustReturnAUrlSafeBase64(): void { $data = base64_decode('0MB2wKB+L3yvIdzeggmJ+5WOSLaRLTUPXbpzqUe0yuo=', true); assert(is_string($data)); $encoder = new JoseEncoder(); self::assertSame('0MB2wKB-L3yvIdzeggmJ-5WOSLaRLTUPXbpzqUe0yuo', $encoder->base64UrlEncode($data)); } #[PHPUnit\Test] public function base64UrlEncodeMustEncodeBilboMessageProperly(): void { /** @link https://tools.ietf.org/html/rfc7520#section-4 */ $message = 'It’s a dangerous business, Frodo, going out your door. You step ' . "onto the road, and if you don't keep your feet, there’s no knowing " . 'where you might be swept off to.'; $expected = 'SXTigJlzIGEgZGFuZ2Vyb3VzIGJ1c2luZXNzLCBGcm9kbywgZ29pbmcgb3V0IH' . 'lvdXIgZG9vci4gWW91IHN0ZXAgb250byB0aGUgcm9hZCwgYW5kIGlmIHlvdSBk' . 'b24ndCBrZWVwIHlvdXIgZmVldCwgdGhlcmXigJlzIG5vIGtub3dpbmcgd2hlcm' . 'UgeW91IG1pZ2h0IGJlIHN3ZXB0IG9mZiB0by4'; $encoder = new JoseEncoder(); self::assertSame($expected, $encoder->base64UrlEncode($message)); } #[PHPUnit\Test] public function base64UrlDecodeMustRaiseExceptionWhenInvalidBase64CharsAreUsed(): void { $decoder = new JoseEncoder(); $this->expectException(CannotDecodeContent::class); $this->expectExceptionCode(0); $this->expectExceptionMessage('Error while decoding from Base64Url, invalid base64 characters detected'); $decoder->base64UrlDecode('ááá'); } #[PHPUnit\Test] public function base64UrlDecodeMustReturnTheRightData(): void { $data = base64_decode('0MB2wKB+L3yvIdzeggmJ+5WOSLaRLTUPXbpzqUe0yuo=', true); $decoder = new JoseEncoder(); self::assertSame($data, $decoder->base64UrlDecode('0MB2wKB-L3yvIdzeggmJ-5WOSLaRLTUPXbpzqUe0yuo')); } #[PHPUnit\Test] public function base64UrlDecodeMustDecodeBilboMessageProperly(): void { /** @link https://tools.ietf.org/html/rfc7520#section-4 */ $message = 'SXTigJlzIGEgZGFuZ2Vyb3VzIGJ1c2luZXNzLCBGcm9kbywgZ29pbmcgb3V0IH' . 'lvdXIgZG9vci4gWW91IHN0ZXAgb250byB0aGUgcm9hZCwgYW5kIGlmIHlvdSBk' . 'b24ndCBrZWVwIHlvdXIgZmVldCwgdGhlcmXigJlzIG5vIGtub3dpbmcgd2hlcm' . 'UgeW91IG1pZ2h0IGJlIHN3ZXB0IG9mZiB0by4'; $expected = 'It’s a dangerous business, Frodo, going out your door. You step ' . "onto the road, and if you don't keep your feet, there’s no knowing " . 'where you might be swept off to.'; $encoder = new JoseEncoder(); self::assertSame($expected, $encoder->base64UrlDecode($message)); } } ================================================ FILE: tests/Encoding/MicrosecondBasedDateConversionTest.php ================================================ $issuedAt, RegisteredClaims::NOT_BEFORE => $notBefore, RegisteredClaims::EXPIRATION_TIME => $expiration, 'testing' => 'test', ]; $formatter = new MicrosecondBasedDateConversion(); $formatted = $formatter->formatClaims($claims); self::assertSame(1487285080, $formatted[RegisteredClaims::ISSUED_AT]); self::assertSame(1487285080.000123, $formatted[RegisteredClaims::NOT_BEFORE]); self::assertSame(1487285080.123456, $formatted[RegisteredClaims::EXPIRATION_TIME]); self::assertSame('test', $formatted['testing']); // this should remain untouched } #[PHPUnit\Test] public function notAllDateClaimsNeedToBeConfigured(): void { $issuedAt = new DateTimeImmutable('@1487285080'); $expiration = DateTimeImmutable::createFromFormat('U.u', '1487285080.123456'); $claims = [ RegisteredClaims::ISSUED_AT => $issuedAt, RegisteredClaims::EXPIRATION_TIME => $expiration, 'testing' => 'test', ]; $formatter = new MicrosecondBasedDateConversion(); $formatted = $formatter->formatClaims($claims); self::assertSame(1487285080, $formatted[RegisteredClaims::ISSUED_AT]); self::assertSame(1487285080.123456, $formatted[RegisteredClaims::EXPIRATION_TIME]); self::assertSame('test', $formatted['testing']); // this should remain untouched } } ================================================ FILE: tests/Encoding/UnifyAudienceTest.php ================================================ 'test']; $formatter = new UnifyAudience(); $formatted = $formatter->formatClaims($claims); self::assertSame('test', $formatted['testing']); } #[PHPUnit\Test] public function audienceShouldBeFormattedAsSingleStringWhenOneValueIsUsed(): void { $claims = [ RegisteredClaims::AUDIENCE => ['test1'], 'testing' => 'test', ]; $formatter = new UnifyAudience(); $formatted = $formatter->formatClaims($claims); self::assertSame('test1', $formatted[RegisteredClaims::AUDIENCE]); self::assertSame('test', $formatted['testing']); // this should remain untouched } #[PHPUnit\Test] public function audienceShouldBeFormattedAsArrayWhenMultipleValuesAreUsed(): void { $claims = [ RegisteredClaims::AUDIENCE => ['test1', 'test2', 'test3'], 'testing' => 'test', ]; $formatter = new UnifyAudience(); $formatted = $formatter->formatClaims($claims); self::assertSame(['test1', 'test2', 'test3'], $formatted[RegisteredClaims::AUDIENCE]); self::assertSame('test', $formatted['testing']); // this should remain untouched } } ================================================ FILE: tests/Encoding/UnixTimestampDatesTest.php ================================================ $issuedAt, RegisteredClaims::NOT_BEFORE => $notBefore, RegisteredClaims::EXPIRATION_TIME => $expiration, 'testing' => 'test', ]; $formatter = new UnixTimestampDates(); $formatted = $formatter->formatClaims($claims); self::assertSame(1487285080, $formatted[RegisteredClaims::ISSUED_AT]); self::assertSame(1487285080, $formatted[RegisteredClaims::NOT_BEFORE]); self::assertSame(1487285080, $formatted[RegisteredClaims::EXPIRATION_TIME]); self::assertSame('test', $formatted['testing']); // this should remain untouched } #[PHPUnit\Test] public function notAllDateClaimsNeedToBeConfigured(): void { $issuedAt = new DateTimeImmutable('@1487285080'); $expiration = DateTimeImmutable::createFromFormat('U.u', '1487285080.123456'); $claims = [ RegisteredClaims::ISSUED_AT => $issuedAt, RegisteredClaims::EXPIRATION_TIME => $expiration, 'testing' => 'test', ]; $formatter = new UnixTimestampDates(); $formatted = $formatter->formatClaims($claims); self::assertSame(1487285080, $formatted[RegisteredClaims::ISSUED_AT]); self::assertSame(1487285080, $formatted[RegisteredClaims::EXPIRATION_TIME]); self::assertSame('test', $formatted['testing']); // this should remain untouched } } ================================================ FILE: tests/HmacTokenTest.php ================================================ config = Configuration::forSymmetricSigner( new Sha256(), InMemory::base64Encoded('Z0Y6xrhjGQYrEDsP+7aQ3ZAKKERSBeQjP33M0H7Nq6s='), ); } #[PHPUnit\Test] public function builderCanGenerateAToken(): Token { $user = ['name' => 'testing', 'email' => 'testing@abc.com']; $builder = $this->config->builder(); $token = $builder->identifiedBy('1') ->permittedFor('https://client.abc.com') ->issuedBy('https://api.abc.com') ->withClaim('user', $user) ->withHeader('jki', '1234') ->getToken($this->config->signer(), $this->config->signingKey()); self::assertSame('1234', $token->headers()->get('jki')); self::assertSame(['https://client.abc.com'], $token->claims()->get(Token\RegisteredClaims::AUDIENCE)); self::assertSame('https://api.abc.com', $token->claims()->get(Token\RegisteredClaims::ISSUER)); self::assertSame($user, $token->claims()->get('user')); return $token; } #[PHPUnit\Test] #[PHPUnit\Depends('builderCanGenerateAToken')] public function parserCanReadAToken(Token $generated): void { $read = $this->config->parser()->parse($generated->toString()); assert($read instanceof Token\Plain); self::assertEquals($generated, $read); self::assertSame('testing', $read->claims()->get('user')['name']); } #[PHPUnit\Test] #[PHPUnit\Depends('builderCanGenerateAToken')] public function signatureAssertionShouldRaiseExceptionWhenKeyIsNotRight(Token $token): void { $this->expectException(RequiredConstraintsViolated::class); $this->expectExceptionMessage('The token violates some mandatory constraints'); $this->config->validator()->assert( $token, new SignedWith( $this->config->signer(), InMemory::base64Encoded('O0MpjL80kE382RyX0rfr9PrNfVclXcdnru2aryanR2o='), ), ); } #[PHPUnit\Test] #[PHPUnit\Depends('builderCanGenerateAToken')] public function signatureAssertionShouldRaiseExceptionWhenAlgorithmIsDifferent(Token $token): void { $this->expectException(RequiredConstraintsViolated::class); $this->expectExceptionMessage('The token violates some mandatory constraints'); $this->config->validator()->assert( $token, new SignedWith(new Sha512(), $this->config->verificationKey()), ); } #[PHPUnit\Test] #[PHPUnit\Depends('builderCanGenerateAToken')] public function signatureValidationShouldSucceedWhenKeyIsRight(Token $token): void { $constraint = new SignedWith($this->config->signer(), $this->config->verificationKey()); self::assertTrue($this->config->validator()->validate($token, $constraint)); } #[PHPUnit\Test] public function everythingShouldWorkWhenUsingATokenGeneratedByOtherLibs(): void { $config = Configuration::forSymmetricSigner( new Sha256(), InMemory::base64Encoded('FkL2+V+1k2auI3xxTz/2skChDQVVjT9PW1/grXafg3M='), ); $data = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJoZWxsbyI6IndvcmxkIn0.' . 'ZQfnc_iFebE--gXmnhJrqMXv3GWdH9uvdkFXTgBcMFw'; $token = $config->parser()->parse($data); assert($token instanceof Token\Plain); $constraint = new SignedWith($config->signer(), $config->verificationKey()); self::assertTrue($config->validator()->validate($token, $constraint)); self::assertSame('world', $token->claims()->get('hello')); } #[PHPUnit\Test] public function signatureValidationWithLocalFileKeyReferenceWillOperateWithKeyContents(): void { $key = tempnam(sys_get_temp_dir(), 'a-very-long-prefix-to-create-a-longer-key'); self::assertIsString($key); file_put_contents( $key, SodiumBase64Polyfill::base642bin( 'FkL2+V+1k2auI3xxTz/2skChDQVVjT9PW1/grXafg3M=', SodiumBase64Polyfill::SODIUM_BASE64_VARIANT_ORIGINAL, ), ); $validKey = InMemory::file($key); $invalidKey = InMemory::plainText('file://' . $key); $signer = new Sha256(); $configuration = Configuration::forSymmetricSigner($signer, $validKey); $validator = $configuration->validator(); $token = $configuration->builder() ->withClaim('foo', 'bar') ->getToken($configuration->signer(), $configuration->signingKey()); self::assertFalse( $validator->validate( $token, new SignedWith($signer, $invalidKey), ), 'Token cannot be validated against the **path** of the key', ); self::assertTrue( $validator->validate( $token, new SignedWith($signer, $validKey), ), 'Token can be validated against the **contents** of the key', ); } } ================================================ FILE: tests/JwtFacadeTest.php ================================================ clock = new FrozenClock(new DateTimeImmutable('2021-07-10')); $this->signer = new Hmac\Sha256(); $this->key = InMemory::base64Encoded('qOIXmZRqZKY80qg0BjtCrskM6OK7gPOea8mz1H7h/dE='); $this->issuer = 'bar'; } /** @return non-empty-string */ private function createToken(): string { return (new JwtFacade(clock: $this->clock))->issue( $this->signer, $this->key, fn (Builder $builder, DateTimeImmutable $issuedAt): Builder => $builder ->expiresAt($issuedAt->modify('+5 minutes')) ->issuedBy($this->issuer), )->toString(); } #[PHPUnit\Test] public function issueSetTimeValidity(): void { $token = (new JwtFacade(clock: $this->clock))->issue( $this->signer, $this->key, static fn (Builder $builder): Builder => $builder, ); $now = $this->clock->now(); self::assertTrue($token->hasBeenIssuedBefore($now)); self::assertTrue($token->isMinimumTimeBefore($now)); self::assertFalse($token->isExpired($now)); $aYearAgo = $now->modify('-1 year'); self::assertFalse($token->hasBeenIssuedBefore($aYearAgo)); self::assertFalse($token->isMinimumTimeBefore($aYearAgo)); self::assertFalse($token->isExpired($aYearAgo)); $inOneYear = $now->modify('+1 year'); self::assertTrue($token->hasBeenIssuedBefore($inOneYear)); self::assertTrue($token->isMinimumTimeBefore($inOneYear)); self::assertTrue($token->isExpired($inOneYear)); } #[PHPUnit\Test] public function issueAllowsTimeValidityOverwrite(): void { $then = new DateTimeImmutable('2001-02-03 04:05:06'); $token = (new JwtFacade())->issue( $this->signer, $this->key, static function (Builder $builder) use ($then): Builder { return $builder ->issuedAt($then) ->canOnlyBeUsedAfter($then) ->expiresAt($then->modify('+1 minute')); }, ); $now = $then->modify('+30 seconds'); self::assertTrue($token->hasBeenIssuedBefore($now)); self::assertTrue($token->isMinimumTimeBefore($now)); self::assertFalse($token->isExpired($now)); $aYearAgo = $then->modify('-1 year'); self::assertFalse($token->hasBeenIssuedBefore($aYearAgo)); self::assertFalse($token->isMinimumTimeBefore($aYearAgo)); self::assertFalse($token->isExpired($aYearAgo)); $inOneYear = $then->modify('+1 year'); self::assertTrue($token->hasBeenIssuedBefore($inOneYear)); self::assertTrue($token->isMinimumTimeBefore($inOneYear)); self::assertTrue($token->isExpired($inOneYear)); } #[PHPUnit\Test] public function goodJwt(): void { $token = (new JwtFacade())->parse( $this->createToken(), new Constraint\SignedWith($this->signer, $this->key), new Constraint\StrictValidAt($this->clock), new Constraint\IssuedBy($this->issuer), ); self::assertInstanceOf(Token\Plain::class, $token); } #[PHPUnit\Test] public function badSigner(): void { $this->expectException(RequiredConstraintsViolated::class); $this->expectExceptionMessage('Token signer mismatch'); $void = (new JwtFacade())->parse( $this->createToken(), new Constraint\SignedWith(new Hmac\Sha384(), $this->key), new Constraint\StrictValidAt($this->clock), new Constraint\IssuedBy($this->issuer), ); } #[PHPUnit\Test] public function badKey(): void { $this->expectException(RequiredConstraintsViolated::class); $this->expectExceptionMessage('Token signature mismatch'); $void = (new JwtFacade())->parse( $this->createToken(), new Constraint\SignedWith( $this->signer, InMemory::base64Encoded('czyPTpN595zVNSuvoNNlXCRFgXS2fHscMR36dGojaUE='), ), new Constraint\StrictValidAt($this->clock), new Constraint\IssuedBy($this->issuer), ); } #[PHPUnit\Test] public function badTime(): void { $token = $this->createToken(); $this->clock->setTo($this->clock->now()->modify('+30 days')); $this->expectException(RequiredConstraintsViolated::class); $this->expectExceptionMessage('The token is expired'); $void = (new JwtFacade())->parse( $token, new Constraint\SignedWith($this->signer, $this->key), new Constraint\StrictValidAt($this->clock), new Constraint\IssuedBy($this->issuer), ); } #[PHPUnit\Test] public function badIssuer(): void { $this->expectException(RequiredConstraintsViolated::class); $this->expectExceptionMessage('The token was not issued by the given issuers'); $void = (new JwtFacade())->parse( $this->createToken(), new Constraint\SignedWith($this->signer, $this->key), new Constraint\StrictValidAt($this->clock), new Constraint\IssuedBy('xyz'), ); } #[PHPUnit\Test] public function parserForNonUnencryptedTokens(): void { $this->expectException(AssertionError::class); $void = (new JwtFacade(new UnsupportedParser()))->parse( 'a.very-broken.token', new Constraint\SignedWith($this->signer, $this->key), new Constraint\StrictValidAt($this->clock), new Constraint\IssuedBy($this->issuer), ); } #[PHPUnit\Test] public function customPsrClock(): void { $clock = new class () implements ClockInterface { public function now(): DateTimeImmutable { return new DateTimeImmutable('2021-07-10'); } }; $facade = new JwtFacade(clock: $clock); $token = $facade->issue( $this->signer, $this->key, static fn (Builder $builder): Builder => $builder, ); self::assertEquals( $token, $facade->parse( $token->toString(), new Constraint\SignedWith($this->signer, $this->key), new Constraint\StrictValidAt($clock), ), ); } #[PHPUnit\Test] public function multipleKeys(): void { $clock = new FrozenClock(new DateTimeImmutable('2023-11-19 22:10:00')); $token = (new JwtFacade())->parse( $this->createToken(), new Constraint\SignedWithOneInSet( new Constraint\SignedWithUntilDate( $this->signer, InMemory::base64Encoded('czyPTpN595zVNSuvoNNlXCRFgXS2fHscMR36dGojaUE='), new DateTimeImmutable('2024-11-19 22:10:00'), $clock, ), new Constraint\SignedWithUntilDate( $this->signer, $this->key, new DateTimeImmutable('2025-11-19 22:10:00'), $clock, ), ), new Constraint\StrictValidAt($this->clock), new Constraint\IssuedBy($this->issuer), ); self::assertInstanceOf(Token\Plain::class, $token); } } ================================================ FILE: tests/KeyDumpSigner.php ================================================ contents(); } // phpcs:ignore SlevomatCodingStandard.Functions.UnusedParameter.UnusedParameter public function verify(string $expected, string $payload, Key $key): bool { return $expected === $key->contents(); } } ================================================ FILE: tests/Keys.php ================================================ */ protected static array $rsaKeys; /** @var array */ protected static array $ecdsaKeys; /** @var array */ protected static array $eddsaKeys; #[PHPUnit\BeforeClass] public static function createRsaKeys(): void { if (isset(static::$rsaKeys)) { return; } static::$rsaKeys = [ 'private' => Key\InMemory::file(__DIR__ . '/_keys/rsa/private.key'), 'public' => Key\InMemory::file(__DIR__ . '/_keys/rsa/public.key'), 'encrypted-private' => Key\InMemory::file(__DIR__ . '/_keys/rsa/encrypted-private.key', 'testing'), 'encrypted-public' => Key\InMemory::file(__DIR__ . '/_keys/rsa/encrypted-public.key'), 'private_short' => Key\InMemory::file(__DIR__ . '/_keys/rsa/private_512.key'), 'public_short' => Key\InMemory::file(__DIR__ . '/_keys/rsa/public_512.key'), ]; } #[PHPUnit\BeforeClass] public static function createEcdsaKeys(): void { if (isset(static::$ecdsaKeys)) { return; } static::$ecdsaKeys = [ 'private' => Key\InMemory::file(__DIR__ . '/_keys/ecdsa/private.key'), 'private-params' => Key\InMemory::file(__DIR__ . '/_keys/ecdsa/private2.key'), 'public1' => Key\InMemory::file(__DIR__ . '/_keys/ecdsa/public1.key'), 'public2' => Key\InMemory::file(__DIR__ . '/_keys/ecdsa/public2.key'), 'public-params' => Key\InMemory::file(__DIR__ . '/_keys/ecdsa/public3.key'), 'private_ec384' => Key\InMemory::file(__DIR__ . '/_keys/ecdsa/private_ec384.key'), 'public_ec384' => Key\InMemory::file(__DIR__ . '/_keys/ecdsa/public_ec384.key'), 'private_ec512' => Key\InMemory::file(__DIR__ . '/_keys/ecdsa/private_ec512.key'), 'public_ec512' => Key\InMemory::file(__DIR__ . '/_keys/ecdsa/public_ec512.key'), 'public2_ec512' => Key\InMemory::file(__DIR__ . '/_keys/ecdsa/public2_ec512.key'), ]; } #[PHPUnit\BeforeClass] public static function createEddsaKeys(): void { if (isset(static::$eddsaKeys)) { return; } static::$eddsaKeys = [ 'private' => Key\InMemory::base64Encoded( 'K3NWT0XqaH+4jgi42gQmHnFE+HTPVhFYi3u4DFJ3OpRHRMt/aGRBoKD/Pt5H/iYgGCla7Q04CdjOUpLSrjZhtg==', ), 'public1' => Key\InMemory::base64Encoded('R0TLf2hkQaCg/z7eR/4mIBgpWu0NOAnYzlKS0q42YbY='), 'public2' => Key\InMemory::base64Encoded('8uLLzCdMrIWcOrAxS/fteYyJhWIGH+wav2fNz8NZhvI='), ]; } } ================================================ FILE: tests/MaliciousTamperingPreventionTest.php ================================================ config = Configuration::forAsymmetricSigner( new ES512(), InMemory::plainText('my-private-key'), InMemory::plainText( '-----BEGIN PUBLIC KEY-----' . PHP_EOL . 'MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQAcpkss6wI7PPlxj3t7A1RqMH3nvL4' . PHP_EOL . 'L5Tzxze/XeeYZnHqxiX+gle70DlGRMqqOq+PJ6RYX7vK0PJFdiAIXlyPQq0B3KaU' . PHP_EOL . 'e86IvFeQSFrJdCc0K8NfiH2G1loIk3fiR+YLqlXk6FAeKtpXJKxR1pCQCAM+vBCs' . PHP_EOL . 'mZudf1zCUZ8/4eodlHU=' . PHP_EOL . '-----END PUBLIC KEY-----', ), ); } #[PHPUnit\Test] public function preventRegressionsThatAllowsMaliciousTampering(): void { $data = 'eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJoZWxsbyI6IndvcmxkIn0.' . 'AQx1MqdTni6KuzfOoedg2-7NUiwe-b88SWbdmviz40GTwrM0Mybp1i1tVtm' . 'TSQ91oEXGXBdtwsN6yalzP9J-sp2YATX_Tv4h-BednbdSvYxZsYnUoZ--ZU' . 'dL10t7g8Yt3y9hdY_diOjIptcha6ajX8yzkDGYG42iSe3f5LywSuD6FO5c'; // Let's let the attacker tamper with our message! $bad = $this->createMaliciousToken($data); /** * At this point, we have our forged message in $bad for testing... * * Now, if we allow the attacker to dictate what Signer we use * (e.g. HMAC-SHA512 instead of ECDSA), they can forge messages! */ $token = $this->config->parser()->parse($bad); assert($token instanceof Plain); self::assertSame('world', $token->claims()->get('hello'), 'The claim content should not be modified'); $validator = $this->config->validator(); self::assertFalse( $validator->validate($token, new SignedWith(new HS512(), $this->config->verificationKey())), 'Using the attackers signer should make things unsafe', ); self::assertFalse( $validator->validate( $token, new SignedWith( $this->config->signer(), $this->config->verificationKey(), ), ), 'But we know which Signer should be used so the attack fails', ); } /** @return non-empty-string */ private function createMaliciousToken(string $token): string { $dec = new JoseEncoder(); $asplode = explode('.', $token); // The user is lying; we insist that we're using HMAC-SHA512, with the // public key as the HMAC secret key. This just builds a forged message: $asplode[0] = $dec->base64UrlEncode('{"alg":"HS512","typ":"JWT"}'); $hmac = hash_hmac( 'sha512', $asplode[0] . '.' . $asplode[1], $this->config->verificationKey()->contents(), true, ); $asplode[2] = $dec->base64UrlEncode($hmac); return implode('.', $asplode); } } ================================================ FILE: tests/RFC6978VectorTest.php ================================================ verify($signature, $payload, $key)); } /** @return mixed[] */ public static function dataRFC6979(): iterable { yield from self::sha256Data(); yield from self::sha384Data(); yield from self::sha512Data(); } /** @return mixed[] */ public static function sha256Data(): iterable { $signer = new Sha256(); $key = InMemory::plainText( '-----BEGIN PUBLIC KEY-----' . PHP_EOL . 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEYP7UuiVanTHJYet0xjVtaMBJuJI7' . PHP_EOL . 'Yfps5mliLmDyn7Z5A/4QCLi8maQa6elWKLxk8vGyDC1+n1F3o8KU1EYimQ==' . PHP_EOL . '-----END PUBLIC KEY-----', ); yield 'SHA-256 (sample)' => [ $signer, $key, 'sample', 'EFD48B2AACB6A8FD1140DD9CD45E81D69D2C877B56AAF991C34D0EA84EAF3716', 'F7CB1C942D657C41D436C7A1B6E29F65F3E900DBB9AFF4064DC4AB2F843ACDA8', ]; yield 'SHA-256 (test)' => [ $signer, $key, 'test', 'F1ABB023518351CD71D881567B1EA663ED3EFCF6C5132B354F28D3B0B7D38367', '019F4113742A2B14BD25926B49C649155F267E60D3814B4C0CC84250E46F0083', ]; } /** @return mixed[] */ public static function sha384Data(): iterable { $signer = new Sha384(); $key = InMemory::plainText( '-----BEGIN PUBLIC KEY-----' . PHP_EOL . 'MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE7DpOQVtOGaRWhhgCn0J/pdqai8SukuAu' . PHP_EOL . 'BqrlKGswDGTe+PDqkFWGYGSiVFFUgLwTgBXZty19VyROqO+awMYhiWcIpZNn+d+5' . PHP_EOL . '9UyoSz8cnbEoiyMcOuDU/nNE/SUzJkcg' . PHP_EOL . '-----END PUBLIC KEY-----', ); yield 'SHA-384 (sample)' => [ $signer, $key, 'sample', '94EDBB92A5ECB8AAD4736E56C691916B3F88140666CE9FA73D64C4EA95AD133C81A648152E44ACF96E36DD1E80FABE46', '99EF4AEB15F178CEA1FE40DB2603138F130E740A19624526203B6351D0A3A94FA329C145786E679E7B82C71A38628AC8', ]; yield 'SHA-384 (test)' => [ $signer, $key, 'test', '8203B63D3C853E8D77227FB377BCF7B7B772E97892A80F36AB775D509D7A5FEB0542A7F0812998DA8F1DD3CA3CF023DB', 'DDD0760448D42D8A43AF45AF836FCE4DE8BE06B485E9B61B827C2F13173923E06A739F040649A667BF3B828246BAA5A5', ]; } /** @return mixed[] */ public static function sha512Data(): iterable { $signer = new Sha512(); $key = InMemory::plainText( '-----BEGIN PUBLIC KEY-----' . PHP_EOL . 'MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBiUVQ0HhZMuAOqiO2lPIT+MMSH4bc' . PHP_EOL . 'l6BOWnFn205bzTcRI9RuRdtrXVNwp/IPtjMVXTj/oW0r12HcrEdLmi9QI6QASTEB' . PHP_EOL . 'yWLNTS/d94IoXmRYQTnC+RtH+H/4I1TWYw90aiig2yV0G1s0qCgAiyKswj+ST6r7' . PHP_EOL . '1NM/gepmlW3+qiv9/PU=' . PHP_EOL . '-----END PUBLIC KEY-----', ); yield 'SHA-512 (sample)' => [ $signer, $key, 'sample', '00C328FAFCBD79DD77850370C46325D987CB525569FB63C5D3BC53950E6D4C5F174E25A1EE9017B5D450606ADD152B534931D7D4E8' . '455CC91F9B15BF05EC36E377FA', '00617CCE7CF5064806C467F678D3B4080D6F1CC50AF26CA209417308281B68AF282623EAA63E5B5C0723D8B8C37FF0777B1A20F8CC' . 'B1DCCC43997F1EE0E44DA4A67A', ]; yield 'SHA-512 (test)' => [ $signer, $key, 'test', '013E99020ABF5CEE7525D16B69B229652AB6BDF2AFFCAEF38773B4B7D08725F10CDB93482FDCC54EDCEE91ECA4166B2A7C6265EF0C' . 'E2BD7051B7CEF945BABD47EE6D', '01FBD0013C674AA79CB39849527916CE301C66EA7CE8B80682786AD60F98F7E78A19CA69EFF5C57400E3B3A0AD66CE0978214D13BA' . 'F4E9AC60752F7B155E2DE4DCE3', ]; } } ================================================ FILE: tests/RsaTokenTest.php ================================================ config = Configuration::forAsymmetricSigner( new Sha256(), static::$rsaKeys['private'], static::$rsaKeys['public'], ); } #[PHPUnit\Test] public function builderShouldRaiseExceptionWhenKeyIsInvalid(): void { $builder = $this->config->builder() ->identifiedBy('1') ->permittedFor('https://client.abc.com') ->issuedBy('https://api.abc.com') ->withClaim('user', ['name' => 'testing', 'email' => 'testing@abc.com']); $this->expectException(InvalidKeyProvided::class); $this->expectExceptionMessage('It was not possible to parse your key'); $void = $builder->getToken($this->config->signer(), InMemory::plainText('testing')); } #[PHPUnit\Test] public function builderShouldRaiseExceptionWhenKeyIsNotRsaCompatible(): void { $builder = $this->config->builder() ->identifiedBy('1') ->permittedFor('https://client.abc.com') ->issuedBy('https://api.abc.com') ->withClaim('user', ['name' => 'testing', 'email' => 'testing@abc.com']); $this->expectException(InvalidKeyProvided::class); $this->expectExceptionMessage('The type of the provided key is not "RSA", "EC" provided'); $void = $builder->getToken($this->config->signer(), static::$ecdsaKeys['private']); } #[PHPUnit\Test] public function builderCanGenerateAToken(): Token { $user = ['name' => 'testing', 'email' => 'testing@abc.com']; $builder = $this->config->builder(); $token = $builder->identifiedBy('1') ->permittedFor('https://client.abc.com') ->issuedBy('https://api.abc.com') ->withClaim('user', $user) ->withHeader('jki', '1234') ->getToken($this->config->signer(), $this->config->signingKey()); self::assertSame('1234', $token->headers()->get('jki')); self::assertSame(['https://client.abc.com'], $token->claims()->get(Token\RegisteredClaims::AUDIENCE)); self::assertSame('https://api.abc.com', $token->claims()->get(Token\RegisteredClaims::ISSUER)); self::assertSame($user, $token->claims()->get('user')); return $token; } #[PHPUnit\Test] #[PHPUnit\Depends('builderCanGenerateAToken')] public function parserCanReadAToken(Token $generated): void { $read = $this->config->parser()->parse($generated->toString()); assert($read instanceof Token\Plain); self::assertEquals($generated, $read); self::assertSame('testing', $read->claims()->get('user')['name']); } #[PHPUnit\Test] #[PHPUnit\Depends('builderCanGenerateAToken')] public function signatureAssertionShouldRaiseExceptionWhenKeyIsNotRight(Token $token): void { $this->expectException(RequiredConstraintsViolated::class); $this->expectExceptionMessage('The token violates some mandatory constraints'); $this->config->validator()->assert( $token, new SignedWith($this->config->signer(), self::$rsaKeys['encrypted-public']), ); } #[PHPUnit\Test] #[PHPUnit\Depends('builderCanGenerateAToken')] public function signatureAssertionShouldRaiseExceptionWhenAlgorithmIsDifferent(Token $token): void { $this->expectException(RequiredConstraintsViolated::class); $this->expectExceptionMessage('The token violates some mandatory constraints'); $this->config->validator()->assert( $token, new SignedWith(new Sha512(), $this->config->verificationKey()), ); } #[PHPUnit\Test] #[PHPUnit\Depends('builderCanGenerateAToken')] public function signatureAssertionShouldRaiseExceptionWhenKeyIsNotRsaCompatible(Token $token): void { $this->expectException(InvalidKeyProvided::class); $this->expectExceptionMessage('The type of the provided key is not "RSA", "EC" provided'); $this->config->validator()->assert( $token, new SignedWith( $this->config->signer(), self::$ecdsaKeys['public1'], ), ); } #[PHPUnit\Test] #[PHPUnit\Depends('builderCanGenerateAToken')] public function signatureValidationShouldSucceedWhenKeyIsRight(Token $token): void { $constraint = new SignedWith($this->config->signer(), $this->config->verificationKey()); self::assertTrue($this->config->validator()->validate($token, $constraint)); } #[PHPUnit\Test] public function everythingShouldWorkWhenUsingATokenGeneratedByOtherLibs(): void { $data = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXUyJ9.eyJoZWxsbyI6IndvcmxkIn0.s' . 'GYbB1KrmnESNfJ4D9hOe1Zad_BMyxdb8G4p4LNP7StYlOyBWck6q7XPpPj_6gB' . 'Bo1ohD3MA2o0HY42lNIrAStaVhfsFKGdIou8TarwMGZBPcif_3ThUV1pGS3fZc' . 'lFwF2SP7rqCngQis_xcUVCyqa8E1Wa_v28grnl1QZrnmQFO8B5JGGLqcrfUHJO' . 'nJCupP-Lqh4TmIhftIimSCgLNmJg80wyrpUEfZYReE7hPuEmY0ClTqAGIMQoNS' . '98ljwDxwhfbSuL2tAdbV4DekbTpWzspe3dOJ7RSzmPKVZ6NoezaIazKqyqkmHZfcMaHI1lQeGia6LTbHU1bp0gINi74Vw'; $token = $this->config->parser()->parse($data); assert($token instanceof Token\Plain); $constraint = new SignedWith($this->config->signer(), $this->config->verificationKey()); self::assertTrue($this->config->validator()->validate($token, $constraint)); self::assertSame('world', $token->claims()->get('hello')); } } ================================================ FILE: tests/Signer/Blake2bTest.php ================================================ keyOne = InMemory::base64Encoded(self::KEY_ONE); $this->keyTwo = InMemory::base64Encoded(self::KEY_TWO); $this->expectedHashWithKeyOne = SodiumBase64Polyfill::base642bin( self::EXPECTED_HASH_WITH_KEY_ONE, SodiumBase64Polyfill::SODIUM_BASE64_VARIANT_ORIGINAL, ); } #[PHPUnit\Test] public function algorithmIdMustBeCorrect(): void { $signer = new Blake2b(); self::assertSame('BLAKE2B', $signer->algorithmId()); } #[PHPUnit\Test] public function generatedSignatureMustBeSuccessfullyVerified(): void { $signer = new Blake2b(); self::assertTrue(hash_equals($this->expectedHashWithKeyOne, $signer->sign(self::CONTENTS, $this->keyOne))); self::assertTrue($signer->verify($this->expectedHashWithKeyOne, self::CONTENTS, $this->keyOne)); } #[PHPUnit\Test] public function signShouldRejectShortKeys(): void { $signer = new Blake2b(); $this->expectException(InvalidKeyProvided::class); $this->expectExceptionMessage('Key provided is shorter than 256 bits, only 128 bits provided'); $signer->sign(self::CONTENTS, InMemory::base64Encoded(self::SHORT_KEY)); } #[PHPUnit\Test] public function verifyShouldReturnFalseWhenExpectedHashWasNotCreatedWithSameInformation(): void { $signer = new Blake2b(); self::assertFalse(hash_equals($this->expectedHashWithKeyOne, $signer->sign(self::CONTENTS, $this->keyTwo))); self::assertFalse($signer->verify($this->expectedHashWithKeyOne, self::CONTENTS, $this->keyTwo)); } } ================================================ FILE: tests/Signer/Ecdsa/EcdsaTestCase.php ================================================ pointsManipulator = new MultibyteStringConverter(); } abstract protected function algorithm(): Ecdsa; abstract protected function algorithmId(): string; abstract protected function signatureAlgorithm(): int; abstract protected function pointLength(): int; abstract protected function keyLength(): int; abstract protected function verificationKey(): Key; abstract protected function signingKey(): Key; #[PHPUnit\Test] final public function algorithmIdMustBeCorrect(): void { self::assertSame($this->algorithmId(), $this->algorithm()->algorithmId()); } #[PHPUnit\Test] final public function signatureAlgorithmMustBeCorrect(): void { self::assertSame($this->signatureAlgorithm(), $this->algorithm()->algorithm()); } #[PHPUnit\Test] final public function pointLengthMustBeCorrect(): void { self::assertSame($this->pointLength(), $this->algorithm()->pointLength()); } #[PHPUnit\Test] final public function expectedKeyLengthMustBeCorrect(): void { self::assertSame($this->keyLength(), $this->algorithm()->expectedKeyLength()); } #[PHPUnit\Test] public function signShouldReturnTheAHashBasedOnTheOpenSslSignature(): void { $payload = 'testing'; $signer = $this->algorithm(); $signature = $signer->sign($payload, $this->signingKey()); $publicKey = openssl_pkey_get_public($this->verificationKey()->contents()); assert($publicKey instanceof OpenSSLAsymmetricKey); self::assertSame( 1, openssl_verify( $payload, $this->pointsManipulator->toAsn1($signature, $signer->pointLength()), $publicKey, $this->signatureAlgorithm(), ), ); } #[PHPUnit\Test] #[PHPUnit\DataProvider('incompatibleKeys')] public function signShouldRaiseAnExceptionWhenKeyLengthIsNotTheExpectedOne( string $keyId, int $keyLength, ): void { self::assertArrayHasKey($keyId, self::$ecdsaKeys); $this->expectException(InvalidKeyProvided::class); $this->expectExceptionMessage( 'The length of the provided key is different than ' . $this->keyLength() . ' bits, ' . $keyLength . ' bits provided', ); $this->algorithm()->sign('testing', self::$ecdsaKeys[$keyId]); } /** @return iterable */ abstract public static function incompatibleKeys(): iterable; #[PHPUnit\Test] public function signShouldRaiseAnExceptionWhenKeyTypeIsNotEC(): void { $this->expectException(InvalidKeyProvided::class); $this->expectExceptionMessage('The type of the provided key is not "EC", "RSA" provided'); $this->algorithm()->sign('testing', self::$rsaKeys['private']); } #[PHPUnit\Test] public function verifyShouldDelegateToEcdsaSignerUsingPublicKey(): void { $payload = 'testing'; $privateKey = openssl_pkey_get_private($this->signingKey()->contents()); assert($privateKey instanceof OpenSSLAsymmetricKey); $signature = ''; openssl_sign($payload, $signature, $privateKey, $this->signatureAlgorithm()); $signer = $this->algorithm(); self::assertTrue( $signer->verify( $this->pointsManipulator->fromAsn1($signature, $signer->pointLength()), $payload, $this->verificationKey(), ), ); } } ================================================ FILE: tests/Signer/Ecdsa/MultibyteStringConverterTest.php ================================================ toAsn1($message, strlen($r)))); } #[PHPUnit\Test] public function toAsn1ShouldRaiseExceptionWhenPointsDoNotHaveCorrectLength(): void { $converter = new MultibyteStringConverter(); $this->expectException(ConversionFailed::class); $this->expectExceptionMessage('Invalid signature length'); $converter->toAsn1('a very wrong string', 64); } #[PHPUnit\Test] #[PHPUnit\DataProvider('pointsConversionData')] public function fromAsn1ShouldReturnTheConcatenatedPoints(string $r, string $s, string $asn1): void { $converter = new MultibyteStringConverter(); $message = hex2bin($asn1); self::assertIsString($message); self::assertNotSame('', $message); self::assertSame($r . $s, bin2hex($converter->fromAsn1($message, strlen($r)))); } /** @return string[][] */ public static function pointsConversionData(): iterable { yield [ 'efd48b2aacb6a8fd1140dd9cd45e81d69d2c877b56aaf991c34d0ea84eaf3716', 'f7cb1c942d657c41d436c7a1b6e29f65f3e900dbb9aff4064dc4ab2f843acda8', '3046022100efd48b2aacb6a8fd1140dd9cd45e81d69d2c877b56aaf991c34d0ea84eaf3716022100f7cb1c942d657c41d436c7' . 'a1b6e29f65f3e900dbb9aff4064dc4ab2f843acda8', ]; yield [ '94edbb92a5ecb8aad4736e56c691916b3f88140666ce9fa73d64c4ea95ad133c81a648152e44acf96e36dd1e80fabe46', '99ef4aeb15f178cea1fe40db2603138f130e740a19624526203b6351d0a3a94fa329c145786e679e7b82c71a38628ac8', '306602310094edbb92a5ecb8aad4736e56c691916b3f88140666ce9fa73d64c4ea95ad133c81a648152e44acf96e36dd1e80fa' . 'be4602310099ef4aeb15f178cea1fe40db2603138f130e740a19624526203b6351d0a3a94fa329c145786e679e7b82c71a38' . '628ac8', ]; yield [ '00c328fafcbd79dd77850370c46325d987cb525569fb63c5d3bc53950e6d4c5f174e25a1ee9017b5d450606add152b534931d7' . 'd4e8455cc91f9b15bf05ec36e377fa', '00617cce7cf5064806c467f678d3b4080d6f1cc50af26ca209417308281b68af282623eaa63e5b5c0723d8b8c37ff0777b1a20' . 'f8ccb1dccc43997f1ee0e44da4a67a', '308187024200c328fafcbd79dd77850370c46325d987cb525569fb63c5d3bc53950e6d4c5f174e25a1ee9017b5d450606add15' . '2b534931d7d4e8455cc91f9b15bf05ec36e377fa0241617cce7cf5064806c467f678d3b4080d6f1cc50af26ca20941730828' . '1b68af282623eaa63e5b5c0723d8b8c37ff0777b1a20f8ccb1dccc43997f1ee0e44da4a67a', ]; } #[PHPUnit\Test] #[PHPUnit\DataProvider('invalidAsn1Structures')] public function fromAsn1ShouldRaiseExceptionOnInvalidMessage(string $message, string $expectedMessage): void { $converter = new MultibyteStringConverter(); $message = hex2bin($message); self::assertIsString($message); $this->expectException(ConversionFailed::class); $this->expectExceptionMessage($expectedMessage); $converter->fromAsn1($message, 64); } /** @return string[][] */ public static function invalidAsn1Structures(): iterable { yield 'Not a sequence' => ['', 'Should start with a sequence']; yield 'Sequence without length' => ['30', 'Should contain an integer']; yield 'Only one string element' => ['3006030204f0', 'Should contain an integer']; yield 'Only one integer element' => ['3004020101', 'Should contain an integer']; yield 'Integer+string elements' => ['300a020101030204f0', 'Should contain an integer']; } } ================================================ FILE: tests/Signer/Ecdsa/Sha256Test.php ================================================ pointsManipulator); } protected function algorithmId(): string { return 'ES256'; } protected function signatureAlgorithm(): int { return OPENSSL_ALGO_SHA256; } protected function pointLength(): int { return 64; } protected function keyLength(): int { return 256; } protected function verificationKey(): Key { return self::$ecdsaKeys['public1']; } protected function signingKey(): Key { return self::$ecdsaKeys['private']; } /** {@inheritDoc} */ public static function incompatibleKeys(): iterable { yield '384 bits' => ['private_ec384', 384]; yield '521 bits' => ['private_ec512', 521]; } } ================================================ FILE: tests/Signer/Ecdsa/Sha384Test.php ================================================ pointsManipulator); } protected function algorithmId(): string { return 'ES384'; } protected function signatureAlgorithm(): int { return OPENSSL_ALGO_SHA384; } protected function pointLength(): int { return 96; } protected function keyLength(): int { return 384; } protected function verificationKey(): Key { return self::$ecdsaKeys['public_ec384']; } protected function signingKey(): Key { return self::$ecdsaKeys['private_ec384']; } /** {@inheritDoc} */ public static function incompatibleKeys(): iterable { yield '256 bits' => ['private', 256]; yield '521 bits' => ['private_ec512', 521]; } } ================================================ FILE: tests/Signer/Ecdsa/Sha512Test.php ================================================ pointsManipulator); } protected function algorithmId(): string { return 'ES512'; } protected function signatureAlgorithm(): int { return OPENSSL_ALGO_SHA512; } protected function pointLength(): int { return 132; } protected function keyLength(): int { return 521; } protected function verificationKey(): Key { return self::$ecdsaKeys['public_ec512']; } protected function signingKey(): Key { return self::$ecdsaKeys['private_ec512']; } /** {@inheritDoc} */ public static function incompatibleKeys(): iterable { yield '256 bits' => ['private', 256]; yield '384 bits' => ['private_ec384', 384]; } } ================================================ FILE: tests/Signer/EddsaTest.php ================================================ algorithmId()); } #[PHPUnit\Test] public function signShouldReturnAValidEddsaSignature(): void { $payload = 'testing'; $signer = new Eddsa(); $signature = $signer->sign($payload, self::$eddsaKeys['private']); $publicKey = self::$eddsaKeys['public1']->contents(); self::assertTrue(sodium_crypto_sign_verify_detached($signature, $payload, $publicKey)); } #[PHPUnit\Test] public function signShouldRaiseAnExceptionWhenKeyIsInvalid(): void { $signer = new Eddsa(); $this->expectException(InvalidKeyProvided::class); $this->expectExceptionCode(0); $this->expectExceptionMessage('SODIUM_CRYPTO_SIGN_SECRETKEYBYTES'); $signer->sign('testing', InMemory::plainText('tooshort')); } #[PHPUnit\Test] public function verifyShouldReturnTrueWhenSignatureIsValid(): void { $payload = 'testing'; $signature = sodium_crypto_sign_detached($payload, self::$eddsaKeys['private']->contents()); $signer = new Eddsa(); self::assertTrue($signer->verify($signature, $payload, self::$eddsaKeys['public1'])); } #[PHPUnit\Test] public function verifyShouldRaiseAnExceptionWhenKeyIsNotParseable(): void { $signer = new Eddsa(); $this->expectException(InvalidKeyProvided::class); $this->expectExceptionCode(0); $this->expectExceptionMessage('SODIUM_CRYPTO_SIGN_BYTES'); $signer->verify('testing', 'testing', InMemory::plainText('blablabla')); } /** @see https://tools.ietf.org/html/rfc8037#appendix-A.4 */ #[PHPUnit\Test] public function signatureOfRfcExample(): void { $signer = new Eddsa(); $encoder = new JoseEncoder(); $decoded = $encoder->base64UrlDecode('nWGxne_9WmC6hEr0kuwsxERJxWl7MmkZcDusAxyuf2A'); $key = InMemory::plainText( $decoded . $encoder->base64UrlDecode('11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo'), ); $payload = $encoder->base64UrlEncode('{"alg":"EdDSA"}') . '.' . $encoder->base64UrlEncode('Example of Ed25519 signing'); $signature = $signer->sign($payload, $key); self::assertSame('eyJhbGciOiJFZERTQSJ9.RXhhbXBsZSBvZiBFZDI1NTE5IHNpZ25pbmc', $payload); self::assertSame( 'hgyY0il_MGCjP0JzlnLWG1PPOt7-09PGcvMg3AIbQR6dWbhijcNR4ki4iylGjg5BhVsPt9g7sVvpAr_MuM0KAg', $encoder->base64UrlEncode($signature), ); } /** @see https://tools.ietf.org/html/rfc8037#appendix-A.5 */ #[PHPUnit\Test] public function verificationOfRfcExample(): void { $signer = new Eddsa(); $encoder = new JoseEncoder(); $decoded = $encoder->base64UrlDecode('11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo'); $key = InMemory::plainText($decoded); $payload = 'eyJhbGciOiJFZERTQSJ9.RXhhbXBsZSBvZiBFZDI1NTE5IHNpZ25pbmc'; $signature = $encoder->base64UrlDecode( 'hgyY0il_MGCjP0JzlnLWG1PPOt7-09PGcvMg3AIbQR6dWbhijcNR4ki4iylGjg5BhVsPt9g7sVvpAr_MuM0KAg', ); self::assertTrue($signer->verify($signature, $payload, $key)); } } ================================================ FILE: tests/Signer/FakeSigner.php ================================================ signature; } public function sign(string $payload, Key $key): string { return $this->signature . '-' . $key->contents(); } public function verify(string $expected, string $payload, Key $key): bool { return $this->signature . '-' . $key->contents() === $expected; } } ================================================ FILE: tests/Signer/Hmac/HmacTestCase.php ================================================ expectedAlgorithmId(), $this->algorithm()->algorithmId()); } #[PHPUnit\Test] public function signMustReturnAHashAccordingWithTheAlgorithm(): void { $secret = $this->generateSecret(); $expectedHash = hash_hmac($this->hashAlgorithm(), 'test', $secret, true); $signature = $this->algorithm()->sign('test', InMemory::plainText($secret)); self::assertTrue(hash_equals($expectedHash, $signature)); } #[PHPUnit\Test] public function verifyMustReturnTrueWhenContentWasSignedWithTheSameKey(): void { $secret = $this->generateSecret(); $signature = hash_hmac($this->hashAlgorithm(), 'test', $secret, true); self::assertTrue($this->algorithm()->verify($signature, 'test', InMemory::plainText($secret))); } #[PHPUnit\Test] public function verifyMustReturnTrueWhenContentWasSignedWithADifferentKey(): void { $signature = hash_hmac( $this->hashAlgorithm(), 'test', $this->generateSecret(), true, ); self::assertFalse( $this->algorithm()->verify( $signature, 'test', InMemory::plainText($this->generateSecret()), ), ); } #[PHPUnit\Test] public function keyMustFulfillMinimumLengthRequirement(): void { $secret = $this->generateSecret(($this->expectedMinimumBits() / 8) - 1); $this->expectException(InvalidKeyProvided::class); $this->expectExceptionMessage( sprintf( 'Key provided is shorter than %d bits, only %d bits provided', $this->expectedMinimumBits(), strlen($secret) * 8, ), ); $this->algorithm()->sign('test', InMemory::plainText($secret)); } /** @return non-empty-string */ private function generateSecret(?int $length = null): string { $length ??= $this->expectedMinimumBits() / 8; assert(is_int($length)); assert($length > 1); return random_bytes($length); } } ================================================ FILE: tests/Signer/Hmac/Sha256Test.php ================================================ expectException(CannotDecodeContent::class); $this->expectExceptionMessage('Error while decoding from Base64Url, invalid base64 characters detected'); InMemory::base64Encoded('ááá'); } #[PHPUnit\Test] public function base64EncodedShouldDecodeKeyContents(): void { $key = InMemory::base64Encoded(base64_encode('testing')); self::assertSame('testing', $key->contents()); } #[PHPUnit\Test] public function exceptionShouldBeRaisedWhenFileDoesNotExists(): void { $path = __DIR__ . '/not-found.pem'; $this->expectException(FileCouldNotBeRead::class); $this->expectExceptionMessage('The path "' . $path . '" does not contain a valid key file'); $this->expectExceptionCode(0); InMemory::file($path); } #[PHPUnit\Test] public function exceptionShouldBeRaisedWhenFileIsEmpty(): void { $this->expectException(InvalidKeyProvided::class); $this->expectExceptionMessage('Key cannot be empty'); InMemory::file(__DIR__ . '/empty.pem'); } #[PHPUnit\Test] public function contentsShouldReturnConfiguredData(): void { $key = InMemory::plainText('testing', 'test'); self::assertSame('testing', $key->contents()); } #[PHPUnit\Test] public function contentsShouldReturnFileContentsWhenFilePathHasBeenPassed(): void { $key = InMemory::file(__DIR__ . '/test.pem'); self::assertSame('testing', $key->contents()); } #[PHPUnit\Test] public function passphraseShouldReturnConfiguredData(): void { $key = InMemory::plainText('testing', 'test'); self::assertSame('test', $key->passphrase()); } #[PHPUnit\Test] public function emptyPlainTextContentShouldRaiseException(): void { $this->expectException(InvalidKeyProvided::class); // @phpstan-ignore-next-line InMemory::plainText(''); } #[PHPUnit\Test] public function emptyBase64ContentShouldRaiseException(): void { $this->expectException(InvalidKeyProvided::class); // @phpstan-ignore-next-line InMemory::base64Encoded(''); } } ================================================ FILE: tests/Signer/Key/empty.pem ================================================ ================================================ FILE: tests/Signer/Key/test.pem ================================================ testing ================================================ FILE: tests/Signer/Rsa/KeyValidationSigner.php ================================================ createSignature($key, $payload); } public function verify(string $expected, string $payload, Key $key): bool { return $this->verifySignature($expected, $payload, $key); } } ================================================ FILE: tests/Signer/Rsa/KeyValidationTest.php ================================================ expectException(CannotSignPayload::class); $this->expectExceptionMessage('There was an error while creating the signature:' . PHP_EOL . '* error:'); $void = $this->algorithm()->sign('testing', InMemory::plainText($key)); } private function algorithm(): OpenSSL { return new KeyValidationSigner(); } } ================================================ FILE: tests/Signer/Rsa/RsaTestCase.php ================================================ algorithmId(), $this->algorithm()->algorithmId()); } #[PHPUnit\Test] final public function signatureAlgorithmMustBeCorrect(): void { self::assertSame($this->signatureAlgorithm(), $this->algorithm()->algorithm()); } #[PHPUnit\Test] public function signShouldReturnAValidOpensslSignature(): void { $payload = 'testing'; $signature = $this->algorithm()->sign($payload, self::$rsaKeys['private']); $publicKey = openssl_pkey_get_public(self::$rsaKeys['public']->contents()); assert($publicKey instanceof OpenSSLAsymmetricKey); self::assertSame( 1, openssl_verify($payload, $signature, $publicKey, $this->signatureAlgorithm()), ); } #[PHPUnit\Test] public function signShouldRaiseAnExceptionWhenKeyIsNotParseable(): void { $this->expectException(InvalidKeyProvided::class); $this->expectExceptionMessage('It was not possible to parse your key, reason:' . PHP_EOL . '* error:'); $this->algorithm()->sign('testing', InMemory::plainText('blablabla')); } #[PHPUnit\Test] public function allOpenSSLErrorsShouldBeOnTheErrorMessage(): void { // Injects a random OpenSSL error message openssl_pkey_get_private('blahblah'); $this->expectException(InvalidKeyProvided::class); $this->expectExceptionMessageMatches('/^.* reason:(' . PHP_EOL . '\* error:.*){2,}/'); $this->algorithm()->sign('testing', InMemory::plainText('blablabla')); } #[PHPUnit\Test] public function signShouldRaiseAnExceptionWhenKeyTypeIsNotRsa(): void { $this->expectException(InvalidKeyProvided::class); $this->expectExceptionMessage('The type of the provided key is not "RSA", "EC" provided'); $this->algorithm()->sign('testing', self::$ecdsaKeys['private']); } #[PHPUnit\Test] public function signShouldRaiseAnExceptionWhenKeyLengthIsBelowMinimum(): void { $this->expectException(InvalidKeyProvided::class); $this->expectExceptionMessage('Key provided is shorter than 2048 bits, only 512 bits provided'); $this->algorithm()->sign('testing', self::$rsaKeys['private_short']); } #[PHPUnit\Test] public function verifyShouldReturnTrueWhenSignatureIsValid(): void { $payload = 'testing'; $privateKey = openssl_pkey_get_private(self::$rsaKeys['private']->contents()); assert($privateKey instanceof OpenSSLAsymmetricKey); $signature = ''; openssl_sign($payload, $signature, $privateKey, $this->signatureAlgorithm()); self::assertTrue($this->algorithm()->verify($signature, $payload, self::$rsaKeys['public'])); } #[PHPUnit\Test] public function verifyShouldRaiseAnExceptionWhenKeyIsNotParseable(): void { $this->expectException(InvalidKeyProvided::class); $this->expectExceptionMessage('It was not possible to parse your key, reason:' . PHP_EOL . '* error:'); $this->algorithm()->verify('testing', 'testing', InMemory::plainText('blablabla')); } #[PHPUnit\Test] public function verifyShouldRaiseAnExceptionWhenKeyTypeIsNotRsa(): void { $this->expectException(InvalidKeyProvided::class); $this->expectExceptionMessage('It was not possible to parse your key'); $this->algorithm()->verify('testing', 'testing', self::$ecdsaKeys['private']); } } ================================================ FILE: tests/Signer/Rsa/Sha256Test.php ================================================ */ public static function base64Variants(): iterable { $binary = sodium_base642bin(self::B64, SODIUM_BASE64_VARIANT_ORIGINAL, ''); yield [self::B64, $binary, SODIUM_BASE64_VARIANT_ORIGINAL]; yield [rtrim(self::B64, '='), $binary, SODIUM_BASE64_VARIANT_ORIGINAL_NO_PADDING]; $urlBinary = sodium_base642bin(self::B64URL, SODIUM_BASE64_VARIANT_URLSAFE, ''); yield [self::B64URL, $urlBinary, SODIUM_BASE64_VARIANT_URLSAFE]; yield [rtrim(self::B64URL, '='), $urlBinary, SODIUM_BASE64_VARIANT_URLSAFE_NO_PADDING]; } #[PHPUnit\Test] #[PHPUnit\DataProvider('invalidBase64')] public function sodiumBase642BinRaisesExceptionOnInvalidBase64(string $content, int $variant): void { $this->expectException(CannotDecodeContent::class); SodiumBase64Polyfill::base642bin($content, $variant); } #[PHPUnit\Test] #[PHPUnit\DataProvider('invalidBase64')] public function fallbackBase642BinRaisesExceptionOnInvalidBase64(string $content, int $variant): void { $this->expectException(CannotDecodeContent::class); SodiumBase64Polyfill::base642binFallback($content, $variant); } /** @return iterable */ public static function invalidBase64(): iterable { yield 'UTF-8 content' => ['ááá', SODIUM_BASE64_VARIANT_ORIGINAL]; yield 'b64Url variant against original (padded)' => [ self::B64URL, SODIUM_BASE64_VARIANT_ORIGINAL, ]; yield 'b64Url variant against original (not padded)' => [ rtrim(self::B64URL, '='), SODIUM_BASE64_VARIANT_ORIGINAL_NO_PADDING, ]; } } ================================================ FILE: tests/TimeFractionPrecisionTest.php ================================================ format('U.u')); $token = $config->builder() ->issuedAt($issuedAt) ->getToken($config->signer(), $config->signingKey()); $parsedToken = $config->parser()->parse($token->toString()); self::assertInstanceOf(Plain::class, $parsedToken); self::assertSame($timeFraction, $parsedToken->claims()->get('iat')->format('U.u')); } /** @return iterable */ public static function datesWithPotentialRoundingIssues(): iterable { yield ['1613938511.017448']; yield ['1613938511.023691']; yield ['1613938511.018045']; yield ['1616074725.008455']; } #[PHPUnit\Test] #[PHPUnit\DataProvider('timeFractionConversions')] public function typeConversionDoesNotCauseParsingErrors(float|int|string $issuedAt, string $timeFraction): void { $encoder = new Encoding\JoseEncoder(); $headers = $encoder->base64UrlEncode($encoder->jsonEncode(['typ' => 'JWT', 'alg' => 'none'])); $claims = $encoder->base64UrlEncode($encoder->jsonEncode(['iat' => $issuedAt])); $config = Configuration::forSymmetricSigner( new KeyDumpSigner(), InMemory::plainText('private'), ); $parsedToken = $config->parser()->parse($headers . '.' . $claims . '.cHJpdmF0ZQ'); self::assertInstanceOf(Token\Plain::class, $parsedToken); self::assertSame($timeFraction, $parsedToken->claims()->get('iat')->format('U.u')); } /** @return iterable */ public static function timeFractionConversions(): iterable { yield [1616481863.528781890869140625, '1616481863.528782']; yield [1616497608.0510409, '1616497608.051041']; yield [1616536852.1000001, '1616536852.100000']; yield [1616457346.3878131, '1616457346.387813']; yield [1616457346.0, '1616457346.000000']; yield [1616457346, '1616457346.000000']; yield ['1616481863.528781890869140625', '1616481863.528782']; yield ['1616497608.0510409', '1616497608.051041']; yield ['1616536852.1000001', '1616536852.100000']; yield ['1616457346.3878131', '1616457346.387813']; yield ['1616457346.0', '1616457346.000000']; yield ['1616457346', '1616457346.000000']; } } ================================================ FILE: tests/Token/BuilderTest.php ================================================ encoder = $this->createMock(Encoder::class); $this->signer = $this->createMock(Signer::class); $this->signer->method('algorithmId')->willReturn('RS256'); } #[PHPUnit\Test] public function withClaimShouldRaiseExceptionWhenTryingToConfigureARegisteredClaim(): void { $this->encoder->expects($this->never())->method(self::anything()); $this->signer->expects($this->never())->method(self::anything()); $builder = Builder::new($this->encoder, new MicrosecondBasedDateConversion()); $this->expectException(RegisteredClaimGiven::class); $this->expectExceptionMessage( 'Builder#withClaim() is meant to be used for non-registered claims, ' . 'check the documentation on how to set claim "iss"', ); $builder->withClaim(RegisteredClaims::ISSUER, 'me'); } #[PHPUnit\Test] public function getTokenShouldReturnACompletelyConfigureToken(): void { $issuedAt = new DateTimeImmutable('@1487285080'); $notBefore = DateTimeImmutable::createFromFormat('U.u', '1487285080.000123'); $expiration = DateTimeImmutable::createFromFormat('U.u', '1487285080.123456'); self::assertInstanceOf(DateTimeImmutable::class, $notBefore); self::assertInstanceOf(DateTimeImmutable::class, $expiration); $this->encoder->expects(self::exactly(2)) ->method('jsonEncode') ->willReturnOnConsecutiveCalls('1', '2'); $this->encoder->expects(self::exactly(3)) ->method('base64UrlEncode') ->willReturnArgument(0); $this->signer->expects($this->once()) ->method('sign') ->with('1.2') ->willReturn('3'); $builder = Builder::new($this->encoder, new MicrosecondBasedDateConversion()); $token = $builder->identifiedBy('123456') ->issuedBy('https://issuer.com') ->issuedAt($issuedAt) ->canOnlyBeUsedAfter($notBefore) ->expiresAt($expiration) ->relatedTo('subject') ->permittedFor('test1') ->permittedFor('test2') ->permittedFor('test2') // should not be added since it's duplicated ->withClaim('test', 123) ->withHeader('userId', 2) ->getToken($this->signer, InMemory::plainText('123')); self::assertSame('JWT', $token->headers()->get('typ')); self::assertSame('RS256', $token->headers()->get('alg')); self::assertSame(2, $token->headers()->get('userId')); self::assertSame(123, $token->claims()->get('test')); self::assertSame($issuedAt, $token->claims()->get(RegisteredClaims::ISSUED_AT)); self::assertSame($notBefore, $token->claims()->get(RegisteredClaims::NOT_BEFORE)); self::assertSame($expiration, $token->claims()->get(RegisteredClaims::EXPIRATION_TIME)); self::assertSame('123456', $token->claims()->get(RegisteredClaims::ID)); self::assertSame('https://issuer.com', $token->claims()->get(RegisteredClaims::ISSUER)); self::assertSame('subject', $token->claims()->get(RegisteredClaims::SUBJECT)); self::assertSame(['test1', 'test2'], $token->claims()->get(RegisteredClaims::AUDIENCE)); self::assertSame('3', $token->signature()->toString()); } #[PHPUnit\Test] public function immutability(): void { $this->encoder->expects($this->never())->method(self::anything()); $this->signer->expects($this->never())->method(self::anything()); $map = new SplObjectStorage(); $builder = Builder::new($this->encoder, new MicrosecondBasedDateConversion()); $map[$builder] = true; $builder = $builder->identifiedBy('123456'); $map[$builder] = true; $builder = $builder->issuedBy('https://issuer.com'); $map[$builder] = true; $builder = $builder->issuedAt(new DateTimeImmutable()); $map[$builder] = true; $builder = $builder->canOnlyBeUsedAfter(new DateTimeImmutable()); $map[$builder] = true; $builder = $builder->expiresAt(new DateTimeImmutable()); $map[$builder] = true; $builder = $builder->relatedTo('subject'); $map[$builder] = true; $builder = $builder->permittedFor('test1'); $map[$builder] = true; $builder = $builder->withClaim('test', 123); $map[$builder] = true; $builder = $builder->withHeader('userId', 2); $map[$builder] = true; self::assertCount(10, $map); } } ================================================ FILE: tests/Token/DataSetTest.php ================================================ 1], 'one=1'); self::assertSame(1, $set->get('one')); } #[PHPUnit\Test] public function getShouldReturnTheFallbackValueWhenItWasGiven(): void { $set = new DataSet(['one' => 1], 'one=1'); self::assertSame(2, $set->get('two', 2)); } #[PHPUnit\Test] public function getShouldReturnNullWhenFallbackValueWasNotGiven(): void { $set = new DataSet(['one' => 1], 'one=1'); self::assertNull($set->get('two')); } #[PHPUnit\Test] public function hasShouldReturnTrueWhenItemWasConfigured(): void { $set = new DataSet(['one' => 1], 'one=1'); self::assertTrue($set->has('one')); } #[PHPUnit\Test] public function hasShouldReturnFalseWhenItemWasNotConfigured(): void { $set = new DataSet(['one' => 1], 'one=1'); self::assertFalse($set->has('two')); } #[PHPUnit\Test] public function allShouldReturnAllConfiguredItems(): void { $items = ['one' => 1, 'two' => 2]; $set = new DataSet($items, 'one=1'); self::assertSame($items, $set->all()); } #[PHPUnit\Test] public function toStringShouldReturnTheEncodedData(): void { $set = new DataSet(['one' => 1], 'one=1'); self::assertSame('one=1', $set->toString()); } } ================================================ FILE: tests/Token/ParserTest.php ================================================ decoder = $this->createMock(Decoder::class); } private function createParser(): Parser { return new Parser($this->decoder); } #[PHPUnit\Test] public function parseMustRaiseExceptionWhenTokenDoesNotHaveThreeParts(): void { $this->decoder->expects($this->never())->method(self::anything()); $parser = $this->createParser(); $this->expectException(InvalidTokenStructure::class); $this->expectExceptionMessage('The JWT string must have two dots'); $parser->parse('.'); } #[PHPUnit\Test] public function parseMustRaiseExceptionWhenTokenDoesNotHaveHeaders(): void { $this->decoder->expects($this->never())->method(self::anything()); $parser = $this->createParser(); $this->expectException(InvalidTokenStructure::class); $this->expectExceptionMessage('The JWT string is missing the Header part'); $parser->parse('.b.c'); } #[PHPUnit\Test] public function parseMustRaiseExceptionWhenTokenDoesNotHaveClaims(): void { $this->decoder->expects($this->never())->method(self::anything()); $parser = $this->createParser(); $this->expectException(InvalidTokenStructure::class); $this->expectExceptionMessage('The JWT string is missing the Claim part'); $parser->parse('a..c'); } #[PHPUnit\Test] public function parseMustRaiseExceptionWhenTokenDoesNotHaveSignature(): void { $this->decoder->expects($this->never())->method(self::anything()); $parser = $this->createParser(); $this->expectException(InvalidTokenStructure::class); $this->expectExceptionMessage('The JWT string is missing the Signature part'); $parser->parse('a.b.'); } #[PHPUnit\Test] public function parseMustRaiseExceptionWhenHeaderCannotBeDecoded(): void { $this->decoder ->expects($this->once()) ->method('base64UrlDecode') ->with('a') ->willReturn('b'); $this->decoder ->expects($this->once()) ->method('jsonDecode') ->with('b') ->willThrowException(new RuntimeException('Nope')); $parser = $this->createParser(); $this->expectException(RuntimeException::class); $this->expectExceptionMessage('Nope'); $parser->parse('a.b.c'); } #[PHPUnit\Test] public function parseMustRaiseExceptionWhenDealingWithNonArrayHeaders(): void { $this->decoder->expects($this->once()) ->method('jsonDecode') ->willReturn('A very invalid header'); $parser = $this->createParser(); $this->expectException(InvalidTokenStructure::class); $this->expectExceptionMessage('headers must be an array'); $parser->parse('a.a.a'); } #[PHPUnit\Test] public function parseMustRaiseExceptionWhenDealingWithHeadersThatHaveEmptyStringKeys(): void { $this->decoder->expects($this->once()) ->method('jsonDecode') ->willReturn(['' => 'foo']); $parser = $this->createParser(); $this->expectException(InvalidTokenStructure::class); $this->expectExceptionMessage('headers must be an array'); $parser->parse('a.a.a'); } #[PHPUnit\Test] public function parseMustRaiseExceptionWhenHeaderIsFromAnEncryptedToken(): void { $this->decoder->expects($this->once()) ->method('jsonDecode') ->willReturn(['enc' => 'AAA']); $parser = $this->createParser(); $this->expectException(UnsupportedHeaderFound::class); $this->expectExceptionMessage('Encryption is not supported yet'); $parser->parse('a.a.a'); } #[PHPUnit\Test] public function parseMustRaiseExceptionWhenDealingWithNonArrayClaims(): void { $this->decoder->expects($this->exactly(2)) ->method('jsonDecode') ->willReturnOnConsecutiveCalls(['typ' => 'JWT'], 'A very invalid claim set'); $parser = $this->createParser(); $this->expectException(InvalidTokenStructure::class); $this->expectExceptionMessage('claims must be an array'); $parser->parse('a.a.a'); } #[PHPUnit\Test] public function parseMustRaiseExceptionWhenDealingWithClaimsThatHaveEmptyStringKeys(): void { $this->decoder->expects($this->exactly(2)) ->method('jsonDecode') ->willReturnOnConsecutiveCalls(['typ' => 'JWT'], ['' => 'foo']); $parser = $this->createParser(); $this->expectException(InvalidTokenStructure::class); $this->expectExceptionMessage('claims must be an array'); $parser->parse('a.a.a'); } #[PHPUnit\Test] public function parseMustReturnAnUnsecuredTokenWhenSignatureIsNotInformed(): void { $this->decoder->expects($this->exactly(3)) ->method('base64UrlDecode') ->willReturnMap([ ['a', 'a_dec'], ['b', 'b_dec'], ['c', 'c_dec'], ]); $this->decoder->expects($this->exactly(2)) ->method('jsonDecode') ->willReturnMap([ ['a_dec', ['typ' => 'JWT', 'alg' => 'none']], ['b_dec', [RegisteredClaims::AUDIENCE => 'test']], ]); $parser = $this->createParser(); $token = $parser->parse('a.b.c'); self::assertInstanceOf(Plain::class, $token); $headers = new DataSet(['typ' => 'JWT', 'alg' => 'none'], 'a'); $claims = new DataSet([RegisteredClaims::AUDIENCE => ['test']], 'b'); $signature = new Signature('c_dec', 'c'); self::assertEquals($headers, $token->headers()); self::assertEquals($claims, $token->claims()); self::assertEquals($signature, $token->signature()); } #[PHPUnit\Test] public function parseMustConfigureTypeToJWTWhenItIsMissing(): void { $this->decoder->expects($this->exactly(3)) ->method('base64UrlDecode') ->willReturnMap([ ['a', 'a_dec'], ['b', 'b_dec'], ['c', 'c_dec'], ]); $this->decoder->expects($this->exactly(2)) ->method('jsonDecode') ->willReturnMap([ ['a_dec', ['alg' => 'none']], ['b_dec', [RegisteredClaims::AUDIENCE => 'test']], ]); $parser = $this->createParser(); $token = $parser->parse('a.b.c'); self::assertInstanceOf(Plain::class, $token); $headers = new DataSet(['typ' => 'JWT', 'alg' => 'none'], 'a'); $claims = new DataSet([RegisteredClaims::AUDIENCE => ['test']], 'b'); $signature = new Signature('c_dec', 'c'); self::assertEquals($headers, $token->headers()); self::assertEquals($claims, $token->claims()); self::assertEquals($signature, $token->signature()); } #[PHPUnit\Test] public function parseMustNotChangeTypeWhenItIsConfigured(): void { $this->decoder->expects($this->exactly(3)) ->method('base64UrlDecode') ->willReturnMap([ ['a', 'a_dec'], ['b', 'b_dec'], ['c', 'c_dec'], ]); $this->decoder->expects($this->exactly(2)) ->method('jsonDecode') ->willReturnMap([ ['a_dec', ['typ' => 'JWS', 'alg' => 'none']], ['b_dec', [RegisteredClaims::AUDIENCE => 'test']], ]); $parser = $this->createParser(); $token = $parser->parse('a.b.c'); self::assertInstanceOf(Plain::class, $token); $headers = new DataSet(['typ' => 'JWS', 'alg' => 'none'], 'a'); $claims = new DataSet([RegisteredClaims::AUDIENCE => ['test']], 'b'); $signature = new Signature('c_dec', 'c'); self::assertEquals($headers, $token->headers()); self::assertEquals($claims, $token->claims()); self::assertEquals($signature, $token->signature()); } #[PHPUnit\Test] public function parseShouldReplicateClaimValueOnHeaderWhenNeeded(): void { $this->decoder->expects($this->exactly(3)) ->method('base64UrlDecode') ->willReturnMap([ ['a', 'a_dec'], ['b', 'b_dec'], ['c', 'c_dec'], ]); $this->decoder->expects($this->exactly(2)) ->method('jsonDecode') ->willReturnMap([ ['a_dec', ['typ' => 'JWT', 'alg' => 'none', RegisteredClaims::AUDIENCE => 'test']], ['b_dec', [RegisteredClaims::AUDIENCE => 'test']], ]); $parser = $this->createParser(); $token = $parser->parse('a.b.c'); self::assertInstanceOf(Plain::class, $token); $headers = new DataSet(['typ' => 'JWT', 'alg' => 'none', RegisteredClaims::AUDIENCE => 'test'], 'a'); $claims = new DataSet([RegisteredClaims::AUDIENCE => ['test']], 'b'); $signature = new Signature('c_dec', 'c'); self::assertEquals($headers, $token->headers()); self::assertEquals($claims, $token->claims()); self::assertEquals($signature, $token->signature()); } #[PHPUnit\Test] public function parseMustReturnANonSignedTokenWhenSignatureAlgorithmIsMissing(): void { $this->decoder->expects($this->exactly(3)) ->method('base64UrlDecode') ->willReturnMap([ ['a', 'a_dec'], ['b', 'b_dec'], ['c', 'c_dec'], ]); $this->decoder->expects($this->exactly(2)) ->method('jsonDecode') ->willReturnMap([ ['a_dec', ['typ' => 'JWT']], ['b_dec', [RegisteredClaims::AUDIENCE => 'test']], ]); $parser = $this->createParser(); $token = $parser->parse('a.b.c'); self::assertInstanceOf(Plain::class, $token); $headers = new DataSet(['typ' => 'JWT'], 'a'); $claims = new DataSet([RegisteredClaims::AUDIENCE => ['test']], 'b'); $signature = new Signature('c_dec', 'c'); self::assertEquals($headers, $token->headers()); self::assertEquals($claims, $token->claims()); self::assertEquals($signature, $token->signature()); } #[PHPUnit\Test] public function parseMustReturnANonSignedTokenWhenSignatureAlgorithmIsNone(): void { $this->decoder->expects($this->exactly(3)) ->method('base64UrlDecode') ->willReturnMap([ ['a', 'a_dec'], ['b', 'b_dec'], ['c', 'c_dec'], ]); $this->decoder->expects($this->exactly(2)) ->method('jsonDecode') ->willReturnMap([ ['a_dec', ['typ' => 'JWT', 'alg' => 'none']], ['b_dec', [RegisteredClaims::AUDIENCE => 'test']], ]); $parser = $this->createParser(); $token = $parser->parse('a.b.c'); self::assertInstanceOf(Plain::class, $token); $headers = new DataSet(['typ' => 'JWT', 'alg' => 'none'], 'a'); $claims = new DataSet([RegisteredClaims::AUDIENCE => ['test']], 'b'); $signature = new Signature('c_dec', 'c'); self::assertEquals($headers, $token->headers()); self::assertEquals($claims, $token->claims()); self::assertEquals($signature, $token->signature()); } #[PHPUnit\Test] public function parseMustReturnASignedTokenWhenSignatureIsInformed(): void { $this->decoder->expects($this->exactly(3)) ->method('base64UrlDecode') ->willReturnMap([ ['a', 'a_dec'], ['b', 'b_dec'], ['c', 'c_dec'], ]); $this->decoder->expects($this->exactly(2)) ->method('jsonDecode') ->willReturnMap([ ['a_dec', ['typ' => 'JWT', 'alg' => 'HS256']], ['b_dec', [RegisteredClaims::AUDIENCE => 'test']], ]); $parser = $this->createParser(); $token = $parser->parse('a.b.c'); self::assertInstanceOf(Plain::class, $token); $headers = new DataSet(['typ' => 'JWT', 'alg' => 'HS256'], 'a'); $claims = new DataSet([RegisteredClaims::AUDIENCE => ['test']], 'b'); $signature = new Signature('c_dec', 'c'); self::assertEquals($headers, $token->headers()); self::assertEquals($claims, $token->claims()); self::assertEquals($signature, $token->signature()); } #[PHPUnit\Test] public function parseMustConvertDateClaimsToObjects(): void { $data = [ RegisteredClaims::ISSUED_AT => 1486930663, RegisteredClaims::EXPIRATION_TIME => 1486930757.023055, ]; $this->decoder->expects($this->exactly(3)) ->method('base64UrlDecode') ->willReturnMap([ ['a', 'a_dec'], ['b', 'b_dec'], ['c', 'c_dec'], ]); $this->decoder->expects($this->exactly(2)) ->method('jsonDecode') ->willReturnMap([ ['a_dec', ['typ' => 'JWT', 'alg' => 'HS256']], ['b_dec', $data], ]); $token = $this->createParser()->parse('a.b.c'); self::assertInstanceOf(Plain::class, $token); $claims = $token->claims(); self::assertEquals( DateTimeImmutable::createFromFormat('U', '1486930663'), $claims->get(RegisteredClaims::ISSUED_AT), ); self::assertEquals( DateTimeImmutable::createFromFormat('U.u', '1486930757.023055'), $claims->get(RegisteredClaims::EXPIRATION_TIME), ); } #[PHPUnit\Test] public function parseMustConvertStringDates(): void { $data = [RegisteredClaims::NOT_BEFORE => '1486930757.000000']; $this->decoder->expects($this->exactly(3)) ->method('base64UrlDecode') ->willReturnMap([ ['a', 'a_dec'], ['b', 'b_dec'], ['c', 'c_dec'], ]); $this->decoder->expects($this->exactly(2)) ->method('jsonDecode') ->willReturnMap([ ['a_dec', ['typ' => 'JWT', 'alg' => 'HS256']], ['b_dec', $data], ]); $token = $this->createParser()->parse('a.b.c'); self::assertInstanceOf(Plain::class, $token); $claims = $token->claims(); self::assertEquals( DateTimeImmutable::createFromFormat('U.u', '1486930757.000000'), $claims->get(RegisteredClaims::NOT_BEFORE), ); } #[PHPUnit\Test] public function parseShouldRaiseExceptionOnInvalidDate(): void { $data = [RegisteredClaims::ISSUED_AT => '14/10/2018 10:50:10.10 UTC']; $this->decoder->expects($this->exactly(2)) ->method('base64UrlDecode') ->willReturnMap([ ['a', 'a_dec'], ['b', 'b_dec'], ]); $this->decoder->expects($this->exactly(2)) ->method('jsonDecode') ->willReturnMap([ ['a_dec', ['typ' => 'JWT', 'alg' => 'HS256']], ['b_dec', $data], ]); $this->expectException(InvalidTokenStructure::class); $this->expectExceptionMessage('Value is not in the allowed date format: 14/10/2018 10:50:10.10 UTC'); $this->createParser()->parse('a.b.c'); } #[PHPUnit\Test] public function parseShouldRaiseExceptionOnTimestampBeyondDateTimeImmutableRange(): void { $data = [RegisteredClaims::ISSUED_AT => -10000000000 ** 5]; $this->decoder->expects($this->exactly(2)) ->method('base64UrlDecode') ->willReturnMap([ ['a', 'a_dec'], ['b', 'b_dec'], ]); $this->decoder->expects($this->exactly(2)) ->method('jsonDecode') ->willReturnMap([ ['a_dec', ['typ' => 'JWT', 'alg' => 'HS256']], ['b_dec', $data], ]); $this->expectException(InvalidTokenStructure::class); $this->createParser()->parse('a.b.c'); } } ================================================ FILE: tests/Token/PlainTest.php ================================================ headers = new DataSet(['alg' => 'none'], 'headers'); $this->claims = new DataSet([], 'claims'); $this->signature = new Signature('hash', 'signature'); } private function createToken( ?DataSet $headers = null, ?DataSet $claims = null, ?Signature $signature = null, ): Plain { return new Plain( $headers ?? $this->headers, $claims ?? $this->claims, $signature ?? $this->signature, ); } #[PHPUnit\Test] public function signedShouldCreateATokenWithSignature(): void { $token = $this->createToken(); self::assertSame($this->headers, $token->headers()); self::assertSame($this->claims, $token->claims()); self::assertSame($this->signature, $token->signature()); } #[PHPUnit\Test] public function payloadShouldReturnAStringWithTheEncodedHeadersAndClaims(): void { $token = $this->createToken(); self::assertSame('headers.claims', $token->payload()); } #[PHPUnit\Test] public function isPermittedForShouldReturnFalseWhenNoAudienceIsConfigured(): void { $token = $this->createToken(); self::assertFalse($token->isPermittedFor('testing')); } #[PHPUnit\Test] public function isPermittedForShouldReturnFalseWhenAudienceDoesNotMatchAsArray(): void { $token = $this->createToken( null, new DataSet([RegisteredClaims::AUDIENCE => ['test', 'test2']], ''), ); self::assertFalse($token->isPermittedFor('testing')); } #[PHPUnit\Test] public function isPermittedForShouldReturnFalseWhenAudienceTypeDoesNotMatch(): void { $token = $this->createToken( null, new DataSet([RegisteredClaims::AUDIENCE => [10]], ''), ); self::assertFalse($token->isPermittedFor('10')); } #[PHPUnit\Test] public function isPermittedForShouldReturnTrueWhenAudienceMatchesAsArray(): void { $token = $this->createToken( null, new DataSet([RegisteredClaims::AUDIENCE => ['testing', 'test']], ''), ); self::assertTrue($token->isPermittedFor('testing')); } #[PHPUnit\Test] public function isIdentifiedByShouldReturnFalseWhenNoIdWasConfigured(): void { $token = $this->createToken(); self::assertFalse($token->isIdentifiedBy('test')); } #[PHPUnit\Test] public function isIdentifiedByShouldReturnFalseWhenIdDoesNotMatch(): void { $token = $this->createToken( null, new DataSet([RegisteredClaims::ID => 'testing'], ''), ); self::assertFalse($token->isIdentifiedBy('test')); } #[PHPUnit\Test] public function isIdentifiedByShouldReturnTrueWhenIdMatches(): void { $token = $this->createToken( null, new DataSet([RegisteredClaims::ID => 'test'], ''), ); self::assertTrue($token->isIdentifiedBy('test')); } #[PHPUnit\Test] public function isRelatedToShouldReturnFalseWhenNoSubjectWasConfigured(): void { $token = $this->createToken(); self::assertFalse($token->isRelatedTo('test')); } #[PHPUnit\Test] public function isRelatedToShouldReturnFalseWhenSubjectDoesNotMatch(): void { $token = $this->createToken( null, new DataSet([RegisteredClaims::SUBJECT => 'testing'], ''), ); self::assertFalse($token->isRelatedTo('test')); } #[PHPUnit\Test] public function isRelatedToShouldReturnTrueWhenSubjectMatches(): void { $token = $this->createToken( null, new DataSet([RegisteredClaims::SUBJECT => 'test'], ''), ); self::assertTrue($token->isRelatedTo('test')); } #[PHPUnit\Test] public function hasBeenIssuedByShouldReturnFalseWhenIssuerIsNotConfigured(): void { $token = $this->createToken(); self::assertFalse($token->hasBeenIssuedBy('test')); } #[PHPUnit\Test] public function hasBeenIssuedByShouldReturnFalseWhenIssuerTypeDoesNotMatches(): void { $token = $this->createToken( null, new DataSet([RegisteredClaims::ISSUER => 10], ''), ); self::assertFalse($token->hasBeenIssuedBy('10')); } #[PHPUnit\Test] public function hasBeenIssuedByShouldReturnFalseWhenIssuerIsNotInTheGivenList(): void { $token = $this->createToken( null, new DataSet([RegisteredClaims::ISSUER => 'test'], ''), ); self::assertFalse($token->hasBeenIssuedBy('testing1', 'testing2')); } #[PHPUnit\Test] public function hasBeenIssuedByShouldReturnTrueWhenIssuerIsInTheGivenList(): void { $token = $this->createToken( null, new DataSet([RegisteredClaims::ISSUER => 'test'], ''), ); self::assertTrue($token->hasBeenIssuedBy('testing1', 'testing2', 'test')); } #[PHPUnit\Test] public function hasBeenIssuedBeforeShouldReturnTrueWhenIssueTimeIsNotConfigured(): void { $token = $this->createToken(); self::assertTrue($token->hasBeenIssuedBefore(new DateTimeImmutable())); } #[PHPUnit\Test] public function hasBeenIssuedBeforeShouldReturnTrueWhenIssueTimeIsBeforeThanNow(): void { $now = new DateTimeImmutable(); $token = $this->createToken( null, new DataSet([RegisteredClaims::ISSUED_AT => $now->modify('-100 seconds')], ''), ); self::assertTrue($token->hasBeenIssuedBefore($now)); } #[PHPUnit\Test] public function hasBeenIssuedBeforeShouldReturnTrueWhenIssueTimeIsEqualsToNow(): void { $now = new DateTimeImmutable(); $token = $this->createToken( null, new DataSet([RegisteredClaims::ISSUED_AT => $now], ''), ); self::assertTrue($token->hasBeenIssuedBefore($now)); } #[PHPUnit\Test] public function hasBeenIssuedBeforeShouldReturnFalseWhenIssueTimeIsGreaterThanNow(): void { $now = new DateTimeImmutable(); $token = $this->createToken( null, new DataSet([RegisteredClaims::ISSUED_AT => $now->modify('+100 seconds')], ''), ); self::assertFalse($token->hasBeenIssuedBefore($now)); } #[PHPUnit\Test] public function isMinimumTimeBeforeShouldReturnTrueWhenIssueTimeIsNotConfigured(): void { $token = $this->createToken(); self::assertTrue($token->isMinimumTimeBefore(new DateTimeImmutable())); } #[PHPUnit\Test] public function isMinimumTimeBeforeShouldReturnTrueWhenNotBeforeClaimIsBeforeThanNow(): void { $now = new DateTimeImmutable(); $token = $this->createToken( null, new DataSet([RegisteredClaims::NOT_BEFORE => $now->modify('-100 seconds')], ''), ); self::assertTrue($token->isMinimumTimeBefore($now)); } #[PHPUnit\Test] public function isMinimumTimeBeforeShouldReturnTrueWhenNotBeforeClaimIsEqualsToNow(): void { $now = new DateTimeImmutable(); $token = $this->createToken( null, new DataSet([RegisteredClaims::NOT_BEFORE => $now], ''), ); self::assertTrue($token->isMinimumTimeBefore($now)); } #[PHPUnit\Test] public function isMinimumTimeBeforeShouldReturnFalseWhenNotBeforeClaimIsGreaterThanNow(): void { $now = new DateTimeImmutable(); $token = $this->createToken( null, new DataSet([RegisteredClaims::NOT_BEFORE => $now->modify('100 seconds')], ''), ); self::assertFalse($token->isMinimumTimeBefore($now)); } #[PHPUnit\Test] public function isExpiredShouldReturnFalseWhenTokenDoesNotExpires(): void { $token = $this->createToken(); self::assertFalse($token->isExpired(new DateTimeImmutable())); } #[PHPUnit\Test] public function isExpiredShouldReturnFalseWhenTokenIsNotExpired(): void { $now = new DateTimeImmutable(); $token = $this->createToken( null, new DataSet([RegisteredClaims::EXPIRATION_TIME => $now->modify('+500 seconds')], ''), ); self::assertFalse($token->isExpired($now)); } #[PHPUnit\Test] public function isExpiredShouldReturnTrueWhenExpirationIsEqualsToNow(): void { $now = new DateTimeImmutable(); $token = $this->createToken( null, new DataSet([RegisteredClaims::EXPIRATION_TIME => $now], ''), ); self::assertTrue($token->isExpired($now)); } #[PHPUnit\Test] public function isExpiredShouldReturnTrueAfterTokenExpires(): void { $now = new DateTimeImmutable(); $token = $this->createToken( null, new DataSet([RegisteredClaims::EXPIRATION_TIME => $now], ''), ); self::assertTrue($token->isExpired($now->modify('+10 days'))); } #[PHPUnit\Test] public function toStringMustReturnEncodedDataWithEmptySignature(): void { $token = $this->createToken(null, null, new Signature('123', '456')); self::assertSame('headers.claims.456', $token->toString()); } #[PHPUnit\Test] public function toStringMustReturnEncodedData(): void { $token = $this->createToken(); self::assertSame('headers.claims.signature', $token->toString()); } } ================================================ FILE: tests/Token/SignatureTest.php ================================================ hash()); self::assertSame('encoded', $signature->toString()); } } ================================================ FILE: tests/UnsignedTokenTest.php ================================================ config = Configuration::forSymmetricSigner( new KeyDumpSigner(), InMemory::plainText('private'), ); } #[PHPUnit\Test] public function builderCanGenerateAToken(): Token { $user = ['name' => 'testing', 'email' => 'testing@abc.com']; $builder = $this->config->builder(); $expiration = new DateTimeImmutable('@' . (self::CURRENT_TIME + 3000)); $token = $builder->identifiedBy('1') ->permittedFor('https://client.abc.com') ->issuedBy('https://api.abc.com') ->expiresAt($expiration) ->withClaim('user', $user) ->getToken($this->config->signer(), $this->config->signingKey()); self::assertEquals(new Token\Signature('private', 'cHJpdmF0ZQ'), $token->signature()); self::assertEquals(['https://client.abc.com'], $token->claims()->get(Token\RegisteredClaims::AUDIENCE)); self::assertSame('https://api.abc.com', $token->claims()->get(Token\RegisteredClaims::ISSUER)); self::assertEquals($expiration, $token->claims()->get(Token\RegisteredClaims::EXPIRATION_TIME)); self::assertEquals($user, $token->claims()->get('user')); return $token; } #[PHPUnit\Test] #[PHPUnit\Depends('builderCanGenerateAToken')] public function parserCanReadAToken(Token $generated): void { $read = $this->config->parser()->parse($generated->toString()); assert($read instanceof Token\Plain); self::assertEquals($generated, $read); self::assertSame('testing', $read->claims()->get('user')['name']); } #[PHPUnit\Test] #[PHPUnit\Depends('builderCanGenerateAToken')] public function tokenValidationShouldPassWhenEverythingIsFine(Token $generated): void { $clock = new FrozenClock(new DateTimeImmutable('@' . self::CURRENT_TIME)); $constraints = [ new IdentifiedBy('1'), new PermittedFor('https://client.abc.com'), new IssuedBy('https://issuer.abc.com', 'https://api.abc.com'), new LooseValidAt($clock), ]; self::assertTrue($this->config->validator()->validate($generated, ...$constraints)); } #[PHPUnit\Test] #[PHPUnit\Depends('builderCanGenerateAToken')] public function tokenValidationShouldAllowCustomConstraint(Token $generated): void { self::assertTrue($this->config->validator()->validate($generated, $this->validUserConstraint())); } #[PHPUnit\Test] #[PHPUnit\Depends('builderCanGenerateAToken')] public function tokenAssertionShouldRaiseExceptionWhenOneOfTheConstraintsFails(Token $generated): void { $constraints = [ new IdentifiedBy('1'), new IssuedBy('https://issuer.abc.com'), ]; $this->expectException(RequiredConstraintsViolated::class); $this->expectExceptionMessage('The token violates some mandatory constraints'); $this->config->validator()->assert($generated, ...$constraints); } private function validUserConstraint(): Constraint { return new class () implements Constraint { public function assert(Token $token): void { if (! $token instanceof Token\Plain) { throw new ConstraintViolation(); } $claims = $token->claims(); if (! $claims->has('user')) { throw new ConstraintViolation(); } $name = $claims->get('user')['name'] ?? ''; $email = $claims->get('user')['email'] ?? ''; if ($name === '' || $email === '') { throw new ConstraintViolation(); } } }; } } ================================================ FILE: tests/UnsupportedParser.php ================================================ $claims * @param array $headers */ protected function buildToken( array $claims = [], array $headers = [], ?Signature $signature = null, ): Plain { return new Plain( new DataSet($headers, ''), new DataSet($claims, ''), $signature ?? new Signature('sig+hash', 'sig+encoded'), ); } protected function issueToken(Signer $signer, Signer\Key $key, ?Closure $customization = null): UnencryptedToken { return (new JwtFacade())->issue( $signer, $key, $customization ?? static fn (Builder $builder) => $builder, ); } } ================================================ FILE: tests/Validation/Constraint/HasClaimTest.php ================================================ expectException(CannotValidateARegisteredClaim::class); $this->expectExceptionMessage( 'The claim "' . $claim . '" is a registered claim, another constraint must be used to validate its value', ); new HasClaim($claim); } /** @return iterable */ public static function registeredClaims(): iterable { foreach (Token\RegisteredClaims::ALL as $claim) { yield $claim => [$claim]; } } #[PHPUnit\Test] public function assertShouldRaiseExceptionWhenClaimIsNotSet(): void { $constraint = new HasClaim('claimId'); $this->expectException(ConstraintViolation::class); $this->expectExceptionMessage('The token does not have the claim "claimId"'); $constraint->assert($this->buildToken()); } #[PHPUnit\Test] public function assertShouldRaiseExceptionWhenTokenIsNotAPlainToken(): void { $token = self::createStub(Token::class); $constraint = new HasClaim('claimId'); $this->expectException(ConstraintViolation::class); $this->expectExceptionMessage('You should pass a plain token'); $constraint->assert($token); } #[PHPUnit\Test] public function assertShouldNotRaiseExceptionWhenClaimMatches(): void { $token = $this->buildToken(['claimId' => 'claimValue']); $constraint = new HasClaim('claimId'); $constraint->assert($token); $this->addToAssertionCount(1); } } ================================================ FILE: tests/Validation/Constraint/HasClaimWithValueTest.php ================================================ expectException(CannotValidateARegisteredClaim::class); $this->expectExceptionMessage( 'The claim "' . $claim . '" is a registered claim, another constraint must be used to validate its value', ); new HasClaimWithValue($claim, 'testing'); } /** @return iterable */ public static function registeredClaims(): iterable { foreach (Token\RegisteredClaims::ALL as $claim) { yield $claim => [$claim]; } } #[PHPUnit\Test] public function assertShouldRaiseExceptionWhenClaimIsNotSet(): void { $constraint = new HasClaimWithValue('claimId', 'claimValue'); $this->expectException(ConstraintViolation::class); $this->expectExceptionMessage('The token does not have the claim "claimId"'); $constraint->assert($this->buildToken()); } #[PHPUnit\Test] public function assertShouldRaiseExceptionWhenClaimValueDoesNotMatch(): void { $constraint = new HasClaimWithValue('claimId', 'claimValue'); $this->expectException(ConstraintViolation::class); $this->expectExceptionMessage('The claim "claimId" does not have the expected value'); $constraint->assert($this->buildToken(['claimId' => 'Some wrong value'])); } #[PHPUnit\Test] public function assertShouldRaiseExceptionWhenTokenIsNotAPlainToken(): void { $constraint = new HasClaimWithValue('claimId', 'claimValue'); $this->expectException(ConstraintViolation::class); $this->expectExceptionMessage('You should pass a plain token'); $constraint->assert(self::createStub(Token::class)); } #[PHPUnit\Test] public function assertShouldNotRaiseExceptionWhenClaimMatches(): void { $token = $this->buildToken(['claimId' => 'claimValue']); $constraint = new HasClaimWithValue('claimId', 'claimValue'); $constraint->assert($token); $this->addToAssertionCount(1); } } ================================================ FILE: tests/Validation/Constraint/IdentifiedByTest.php ================================================ expectException(ConstraintViolation::class); $this->expectExceptionMessage('The token is not identified with the expected ID'); $constraint->assert($this->buildToken()); } #[PHPUnit\Test] public function assertShouldRaiseExceptionWhenIdDoesNotMatch(): void { $constraint = new IdentifiedBy('123456'); $this->expectException(ConstraintViolation::class); $this->expectExceptionMessage('The token is not identified with the expected ID'); $constraint->assert($this->buildToken([RegisteredClaims::ID => 15])); } #[PHPUnit\Test] public function assertShouldNotRaiseExceptionWhenIdMatches(): void { $token = $this->buildToken([RegisteredClaims::ID => '123456']); $constraint = new IdentifiedBy('123456'); $constraint->assert($token); $this->addToAssertionCount(1); } } ================================================ FILE: tests/Validation/Constraint/IssuedByTest.php ================================================ expectException(ConstraintViolation::class); $this->expectExceptionMessage('The token was not issued by the given issuers'); $constraint = new IssuedBy('test.com', 'test.net'); $constraint->assert($this->buildToken()); } #[PHPUnit\Test] public function assertShouldRaiseExceptionWhenIssuerValueDoesNotMatch(): void { $this->expectException(ConstraintViolation::class); $this->expectExceptionMessage('The token was not issued by the given issuers'); $constraint = new IssuedBy('test.com', 'test.net'); $constraint->assert($this->buildToken([RegisteredClaims::ISSUER => 'example.com'])); } #[PHPUnit\Test] public function assertShouldRaiseExceptionWhenIssuerTypeValueDoesNotMatch(): void { $this->expectException(ConstraintViolation::class); $this->expectExceptionMessage('The token was not issued by the given issuers'); $constraint = new IssuedBy('test.com', '123'); $constraint->assert($this->buildToken([RegisteredClaims::ISSUER => 123])); } #[PHPUnit\Test] public function assertShouldNotRaiseExceptionWhenIssuerMatches(): void { $token = $this->buildToken([RegisteredClaims::ISSUER => 'test.com']); $constraint = new IssuedBy('test.com', 'test.net'); $constraint->assert($token); $this->addToAssertionCount(1); } } ================================================ FILE: tests/Validation/Constraint/LooseValidAtTest.php ================================================ buildToken(); $constraint = $this->buildValidAtConstraint($this->clock); $constraint->assert($token); $this->addToAssertionCount(1); } } ================================================ FILE: tests/Validation/Constraint/PermittedForTest.php ================================================ expectException(ConstraintViolation::class); $this->expectExceptionMessage('The token is not allowed to be used by this audience'); $constraint->assert($this->buildToken()); } #[PHPUnit\Test] public function assertShouldRaiseExceptionWhenAudienceValueDoesNotMatch(): void { $constraint = new PermittedFor('test.com'); $this->expectException(ConstraintViolation::class); $this->expectExceptionMessage('The token is not allowed to be used by this audience'); $constraint->assert($this->buildToken([RegisteredClaims::AUDIENCE => ['aa.com']])); } #[PHPUnit\Test] public function assertShouldRaiseExceptionWhenAudienceTypeDoesNotMatch(): void { $constraint = new PermittedFor('123'); $this->expectException(ConstraintViolation::class); $this->expectExceptionMessage('The token is not allowed to be used by this audience'); $constraint->assert($this->buildToken([RegisteredClaims::AUDIENCE => [123]])); } #[PHPUnit\Test] public function assertShouldNotRaiseExceptionWhenAudienceMatches(): void { $token = $this->buildToken([RegisteredClaims::AUDIENCE => ['aa.com', 'test.com']]); $constraint = new PermittedFor('test.com'); $constraint->assert($token); $this->addToAssertionCount(1); } } ================================================ FILE: tests/Validation/Constraint/RelatedToTest.php ================================================ expectException(ConstraintViolation::class); $this->expectExceptionMessage('The token is not related to the expected subject'); $constraint->assert($this->buildToken()); } #[PHPUnit\Test] public function assertShouldRaiseExceptionWhenSubjectDoesNotMatch(): void { $constraint = new RelatedTo('user-auth'); $this->expectException(ConstraintViolation::class); $this->expectExceptionMessage('The token is not related to the expected subject'); $constraint->assert($this->buildToken([RegisteredClaims::SUBJECT => 'password-recovery'])); } #[PHPUnit\Test] public function assertShouldNotRaiseExceptionWhenSubjectMatches(): void { $token = $this->buildToken([RegisteredClaims::SUBJECT => 'user-auth']); $constraint = new RelatedTo('user-auth'); $constraint->assert($token); $this->addToAssertionCount(1); } } ================================================ FILE: tests/Validation/Constraint/SignedWithOneInSetTest.php ================================================ now(), $clock), new SignedWithUntilDate($signer, InMemory::plainText('c'), $clock->now()->modify('-2 minutes'), $clock), ); $this->expectException(ConstraintViolation::class); $this->expectExceptionMessage( 'It was not possible to verify the signature of the token, reasons:' . PHP_EOL . '- Token signature mismatch' . PHP_EOL . '- This constraint was only usable until 2023-11-19T22:18:00+00:00', ); $token = $this->issueToken($signer, InMemory::plainText('a')); $constraint->assert($token); } #[PHPUnit\Test] public function assertShouldNotRaiseExceptionsWhenSignatureIsVerifiedByAtLeastOneConstraint(): void { $clock = new FrozenClock(new DateTimeImmutable('2023-11-19 22:20:00')); $signer = new FakeSigner('123'); $constraint = new SignedWithOneInSet( new SignedWithUntilDate($signer, InMemory::plainText('b'), $clock->now(), $clock), new SignedWithUntilDate($signer, InMemory::plainText('c'), $clock->now()->modify('-2 minutes'), $clock), new SignedWithUntilDate($signer, InMemory::plainText('a'), $clock->now(), $clock), ); $token = $this->issueToken($signer, InMemory::plainText('a')); $constraint->assert($token); $this->addToAssertionCount(1); } } ================================================ FILE: tests/Validation/Constraint/SignedWithTest.php ================================================ signer = $this->createMock(Signer::class); $this->signer->method('algorithmId')->willReturn('RS256'); $this->key = Signer\Key\InMemory::plainText('123'); $this->signature = new Signature('1234', '5678'); } #[PHPUnit\Test] public function assertShouldRaiseExceptionWhenTokenIsNotAPlainToken(): void { $this->signer->expects($this->never())->method(self::anything()); $constraint = new SignedWith($this->signer, $this->key); $this->expectException(ConstraintViolation::class); $this->expectExceptionMessage('You should pass a plain token'); $constraint->assert(self::createStub(Token::class)); } #[PHPUnit\Test] public function assertShouldRaiseExceptionWhenSignerIsNotTheSame(): void { $token = $this->buildToken([], ['alg' => 'test'], $this->signature); $this->signer->expects($this->never())->method('verify'); $constraint = new SignedWith($this->signer, $this->key); $this->expectException(ConstraintViolation::class); $this->expectExceptionMessage('Token signer mismatch'); $constraint->assert($token); } #[PHPUnit\Test] public function assertShouldRaiseExceptionWhenSignatureIsInvalid(): void { $token = $this->buildToken([], ['alg' => 'RS256'], $this->signature); $this->signer->expects($this->once()) ->method('verify') ->with($this->signature->hash(), $token->payload(), $this->key) ->willReturn(false); $constraint = new SignedWith($this->signer, $this->key); $this->expectException(ConstraintViolation::class); $this->expectExceptionMessage('Token signature mismatch'); $constraint->assert($token); } #[PHPUnit\Test] public function assertShouldNotRaiseExceptionWhenSignatureIsValid(): void { $token = $this->buildToken([], ['alg' => 'RS256'], $this->signature); $this->signer->expects($this->once()) ->method('verify') ->with($this->signature->hash(), $token->payload(), $this->key) ->willReturn(true); $constraint = new SignedWith($this->signer, $this->key); $constraint->assert($token); $this->addToAssertionCount(1); } } ================================================ FILE: tests/Validation/Constraint/SignedWithUntilDateTest.php ================================================ now()->modify('-1 hour'), $clock, ); $this->expectException(ConstraintViolation::class); $this->expectExceptionMessage('This constraint was only usable until 2023-11-19T21:45:10+00:00'); $constraint->assert($this->issueToken(new FakeSigner('1'), InMemory::plainText('a'))); } #[PHPUnit\Test] public function assertShouldRaiseExceptionWhenTokenIsNotAPlainToken(): void { $clock = new FrozenClock(new DateTimeImmutable('2023-11-19 22:45:10')); $constraint = new SignedWithUntilDate(new FakeSigner('1'), InMemory::plainText('a'), $clock->now(), $clock); $this->expectException(ConstraintViolation::class); $this->expectExceptionMessage('You should pass a plain token'); $constraint->assert(self::createStub(Token::class)); } #[PHPUnit\Test] public function assertShouldRaiseExceptionWhenSignerIsNotTheSame(): void { $clock = new FrozenClock(new DateTimeImmutable('2023-11-19 22:45:10')); $key = InMemory::plainText('a'); $constraint = new SignedWithUntilDate(new FakeSigner('1'), $key, $clock->now(), $clock); $this->expectException(ConstraintViolation::class); $this->expectExceptionMessage('Token signer mismatch'); $constraint->assert($this->issueToken(new FakeSigner('2'), $key)); } #[PHPUnit\Test] public function assertShouldRaiseExceptionWhenSignatureIsInvalid(): void { $clock = new FrozenClock(new DateTimeImmutable('2023-11-19 22:45:10')); $signer = new FakeSigner('1'); $constraint = new SignedWithUntilDate($signer, InMemory::plainText('a'), $clock->now(), $clock); $this->expectException(ConstraintViolation::class); $this->expectExceptionMessage('Token signature mismatch'); $constraint->assert($this->issueToken($signer, InMemory::plainText('b'))); } #[PHPUnit\Test] public function assertShouldNotRaiseExceptionWhenSignatureIsValid(): void { $clock = new FrozenClock(new DateTimeImmutable('2023-11-19 22:45:10')); $signer = new FakeSigner('1'); $key = InMemory::plainText('a'); $constraint = new SignedWithUntilDate($signer, $key, $clock->now(), $clock); $constraint->assert($this->issueToken($signer, $key)); $this->addToAssertionCount(1); } #[PHPUnit\Test] public function clockShouldBeOptional(): void { $signer = new FakeSigner('1'); $key = InMemory::plainText('a'); $constraint = new SignedWithUntilDate($signer, $key, new DateTimeImmutable('+10 seconds')); $constraint->assert($this->issueToken($signer, $key)); $this->addToAssertionCount(1); } } ================================================ FILE: tests/Validation/Constraint/StrictValidAtTest.php ================================================ buildValidAtConstraint($this->clock); $this->expectException(ConstraintViolation::class); $this->expectExceptionMessage('You should pass a plain token'); $constraint->assert(self::createStub(Token::class)); } #[PHPUnit\Test] public function assertShouldRaiseExceptionWhenIatClaimIsMissing(): void { $constraint = $this->buildValidAtConstraint($this->clock); $this->expectException(ConstraintViolation::class); $this->expectExceptionMessage('"Issued At" claim missing'); $constraint->assert($this->buildToken()); } #[PHPUnit\Test] public function assertShouldRaiseExceptionWhenNbfClaimIsMissing(): void { $now = $this->clock->now(); $claims = [ RegisteredClaims::ISSUED_AT => $now->modify('-5 seconds'), ]; $constraint = $this->buildValidAtConstraint($this->clock); $this->expectException(ConstraintViolation::class); $this->expectExceptionMessage('"Not Before" claim missing'); $constraint->assert($this->buildToken($claims)); } #[PHPUnit\Test] public function assertShouldRaiseExceptionWhenExpClaimIsMissing(): void { $now = $this->clock->now(); $claims = [ RegisteredClaims::ISSUED_AT => $now->modify('-5 seconds'), RegisteredClaims::NOT_BEFORE => $now->modify('-5 seconds'), ]; $constraint = $this->buildValidAtConstraint($this->clock); $this->expectException(ConstraintViolation::class); $this->expectExceptionMessage('"Expiration Time" claim missing'); $constraint->assert($this->buildToken($claims)); } } ================================================ FILE: tests/Validation/Constraint/ValidAtTestCase.php ================================================ clock = new FrozenClock(new DateTimeImmutable()); } abstract protected function buildValidAtConstraint(Clock $clock, ?DateInterval $leeway = null): Constraint; #[PHPUnit\Test] final public function constructShouldRaiseExceptionOnNegativeLeeway(): void { $leeway = new DateInterval('PT30S'); $leeway->invert = 1; $this->expectException(LeewayCannotBeNegative::class); $this->expectExceptionMessage('Leeway cannot be negative'); $this->buildValidAtConstraint($this->clock, $leeway); } #[PHPUnit\Test] final public function assertShouldRaiseExceptionWhenTokenIsExpired(): void { $now = $this->clock->now(); $claims = [ RegisteredClaims::ISSUED_AT => $now->modify('-20 seconds'), RegisteredClaims::NOT_BEFORE => $now->modify('-10 seconds'), RegisteredClaims::EXPIRATION_TIME => $now->modify('-10 seconds'), ]; $constraint = $this->buildValidAtConstraint($this->clock); $this->expectException(ConstraintViolation::class); $this->expectExceptionMessage('The token is expired'); $constraint->assert($this->buildToken($claims)); } #[PHPUnit\Test] final public function assertShouldRaiseExceptionWhenMinimumTimeIsNotMet(): void { $now = $this->clock->now(); $claims = [ RegisteredClaims::ISSUED_AT => $now->modify('-20 seconds'), RegisteredClaims::NOT_BEFORE => $now->modify('+40 seconds'), RegisteredClaims::EXPIRATION_TIME => $now->modify('+60 seconds'), ]; $constraint = $this->buildValidAtConstraint($this->clock); $this->expectException(ConstraintViolation::class); $this->expectExceptionMessage('The token cannot be used yet'); $constraint->assert($this->buildToken($claims)); } #[PHPUnit\Test] final public function assertShouldRaiseExceptionWhenTokenWasIssuedInTheFuture(): void { $now = $this->clock->now(); $claims = [ RegisteredClaims::ISSUED_AT => $now->modify('+20 seconds'), RegisteredClaims::NOT_BEFORE => $now->modify('+40 seconds'), RegisteredClaims::EXPIRATION_TIME => $now->modify('+60 seconds'), ]; $constraint = $this->buildValidAtConstraint($this->clock); $this->expectException(ConstraintViolation::class); $this->expectExceptionMessage('The token was issued in the future'); $constraint->assert($this->buildToken($claims)); } #[PHPUnit\Test] final public function assertShouldNotRaiseExceptionWhenLeewayIsUsed(): void { $now = $this->clock->now(); $claims = [ RegisteredClaims::ISSUED_AT => $now->modify('+5 seconds'), RegisteredClaims::NOT_BEFORE => $now->modify('+5 seconds'), RegisteredClaims::EXPIRATION_TIME => $now->modify('-5 seconds'), ]; $constraint = $this->buildValidAtConstraint($this->clock, new DateInterval('PT6S')); $constraint->assert($this->buildToken($claims)); $this->addToAssertionCount(1); } #[PHPUnit\Test] final public function assertShouldNotRaiseExceptionWhenTokenIsUsedInTheRightMoment(): void { $constraint = $this->buildValidAtConstraint($this->clock); $now = $this->clock->now(); $token = $this->buildToken( [ RegisteredClaims::ISSUED_AT => $now->modify('-40 seconds'), RegisteredClaims::NOT_BEFORE => $now->modify('-20 seconds'), RegisteredClaims::EXPIRATION_TIME => $now->modify('+60 seconds'), ], ); $constraint->assert($token); $this->addToAssertionCount(1); $token = $this->buildToken( [ RegisteredClaims::ISSUED_AT => $now, RegisteredClaims::NOT_BEFORE => $now, RegisteredClaims::EXPIRATION_TIME => $now->modify('+60 seconds'), ], ); $constraint->assert($token); $this->addToAssertionCount(1); } } ================================================ FILE: tests/Validation/ConstraintViolationTest.php ================================================ getMessage()); self::assertSame(IdentifiedBy::class, $violation->constraint); } } ================================================ FILE: tests/Validation/RequiredConstraintsViolatedTest.php ================================================ getMessage(), ); self::assertSame([$violation], $exception->violations()); } } ================================================ FILE: tests/Validation/ValidatorTest.php ================================================ token = self::createStub(Token::class); } #[PHPUnit\Test] public function assertShouldRaiseExceptionWhenNoConstraintIsGiven(): void { $validator = new Validator(); $this->expectException(NoConstraintsGiven::class); $validator->assert($this->token, ...[]); } #[PHPUnit\Test] public function assertShouldRaiseExceptionWhenAtLeastOneConstraintFails(): void { $failedConstraint = $this->createMock(Constraint::class); $successfulConstraint = $this->createMock(Constraint::class); $failedConstraint->expects($this->once()) ->method('assert') ->willThrowException(new ConstraintViolation()); $successfulConstraint->expects($this->once()) ->method('assert'); $validator = new Validator(); $this->expectException(RequiredConstraintsViolated::class); $this->expectExceptionMessage('The token violates some mandatory constraints'); $validator->assert( $this->token, $failedConstraint, $successfulConstraint, ); } #[PHPUnit\Test] public function assertShouldNotRaiseExceptionWhenNoConstraintFails(): void { $constraint = $this->createMock(Constraint::class); $constraint->expects($this->once())->method('assert'); $validator = new Validator(); $validator->assert($this->token, $constraint); $this->addToAssertionCount(1); } #[PHPUnit\Test] public function validateShouldRaiseExceptionWhenNoConstraintIsGiven(): void { $validator = new Validator(); $this->expectException(NoConstraintsGiven::class); $validator->validate($this->token); } #[PHPUnit\Test] public function validateShouldReturnFalseWhenAtLeastOneConstraintFails(): void { $failedConstraint = $this->createMock(Constraint::class); $successfulConstraint = $this->createMock(Constraint::class); $failedConstraint->expects($this->once()) ->method('assert') ->willThrowException(new ConstraintViolation()); $successfulConstraint->expects($this->never()) ->method('assert'); $validator = new Validator(); self::assertFalse( $validator->validate( $this->token, $failedConstraint, $successfulConstraint, ), ); } #[PHPUnit\Test] public function validateShouldReturnTrueWhenNoConstraintFails(): void { $constraint = $this->createMock(Constraint::class); $constraint->expects($this->once())->method('assert'); $validator = new Validator(); self::assertTrue($validator->validate($this->token, $constraint)); } } ================================================ FILE: tests/_keys/ecdsa/private.key ================================================ -----BEGIN EC PRIVATE KEY----- MHcCAQEEIBGpMoZJ64MMSzuo5JbmXpf9V4qSWdLIl/8RmJLcfn/qoAoGCCqGSM49 AwEHoUQDQgAE7it/EKmcv9bfpcV1fBreLMRXxWpnd0wxa2iFruiI2tsEdGFTLTsy U+GeRqC7zN0aTnTQajarUylKJ3UWr/r1kg== -----END EC PRIVATE KEY----- ================================================ FILE: tests/_keys/ecdsa/private2.key ================================================ -----BEGIN EC PARAMETERS----- BggqhkjOPQMBBw== -----END EC PARAMETERS----- -----BEGIN EC PRIVATE KEY----- MHcCAQEEIM6G7WZ6SqoPwrHwGXhOJkYD+ErT8dfRvrNifgBQvSb7oAoGCCqGSM49 AwEHoUQDQgAE09Hkp/u0tIGdzlQ99R/sXCOr9DTZAfLex4D4Po0C1L3qUqHrzZ0m B3bAhe+pwEDQ/jqVqdzxhA9i4PqT7F4Aew== -----END EC PRIVATE KEY----- ================================================ FILE: tests/_keys/ecdsa/private_ec384.key ================================================ -----BEGIN EC PARAMETERS----- BgUrgQQAIg== -----END EC PARAMETERS----- -----BEGIN EC PRIVATE KEY----- MIGkAgEBBDC9EkM5BmOhEWZIrTnm2zkiEIDKdylYrRMYMyZiyTsSxihwrCoyH1hX F2UipTTWtaGgBwYFK4EEACKhZANiAAR86pZ9ZK8EWkFsoBj8idwMJqXtOMJ4GB+6 GEq1/uypgrnkHbtgWLTRJgWnliurJpHV9SekXwoV0F5nxEgX3pQiYS6T2OtVAVs7 jxuwNTcdDm+a2KsZOb62ns0YXG+CL7o= -----END EC PRIVATE KEY----- ================================================ FILE: tests/_keys/ecdsa/private_ec512.key ================================================ -----BEGIN EC PRIVATE KEY----- MIHcAgEBBEIAD3yO2xWzu9sSrWXsUcsjppnOPGisza3C4cAL5eAV65xAm4Q6o0LP D7xIBDhxxqDXHsV9iGwbO0yyxzlLAyc3dIygBwYFK4EEACOhgYkDgYYABABOq2Sn fRlHi0Ofxg6JTFN+KLb36FWxSrsSzIstZW6zLIYPS3kAYJF3d+jGWRrcxO0kTTMe cg+bMYATIp8/BYKqXQGayqC3zb7Bxa+YnamkXkQGAU6g94TrGVA42+aYhBjklLRb F09R1HrIvzhy3unxzowqjKPozFq10VENyzO1TRx7mg== -----END EC PRIVATE KEY----- ================================================ FILE: tests/_keys/ecdsa/public1.key ================================================ -----BEGIN PUBLIC KEY----- MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE7it/EKmcv9bfpcV1fBreLMRXxWpn d0wxa2iFruiI2tsEdGFTLTsyU+GeRqC7zN0aTnTQajarUylKJ3UWr/r1kg== -----END PUBLIC KEY----- ================================================ FILE: tests/_keys/ecdsa/public2.key ================================================ -----BEGIN PUBLIC KEY----- MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEdgxRxlhzhHGj+v6S2ikp+33LoGp5 QWbEWv8BORsr2Ayg6C7deDDRM/s/f0R++4zZqXro1gDTVF5VDv7nE+EfEw== -----END PUBLIC KEY----- ================================================ FILE: tests/_keys/ecdsa/public2_ec512.key ================================================ -----BEGIN PUBLIC KEY----- MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBpHZ4gSVGEeYFWsHYNDMSO49wPtsP 4/yAqywK7D+OQ5P+1yhM3tAUm8wnI/+msJcrVpVf9eGdk8NQGtg9hTro7mEBzrBu 3aNJqbkN7yXAb95rc19r787XvkxJ3YjJ+BRMZtYKn/1N/YdtkEpJVgt6WdVbsupB veMsYYahRoZgEZgFW78= -----END PUBLIC KEY----- ================================================ FILE: tests/_keys/ecdsa/public3.key ================================================ -----BEGIN PUBLIC KEY----- MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE09Hkp/u0tIGdzlQ99R/sXCOr9DTZ AfLex4D4Po0C1L3qUqHrzZ0mB3bAhe+pwEDQ/jqVqdzxhA9i4PqT7F4Aew== -----END PUBLIC KEY----- ================================================ FILE: tests/_keys/ecdsa/public_ec384.key ================================================ -----BEGIN PUBLIC KEY----- MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEfOqWfWSvBFpBbKAY/IncDCal7TjCeBgf uhhKtf7sqYK55B27YFi00SYFp5YrqyaR1fUnpF8KFdBeZ8RIF96UImEuk9jrVQFb O48bsDU3HQ5vmtirGTm+tp7NGFxvgi+6 -----END PUBLIC KEY----- ================================================ FILE: tests/_keys/ecdsa/public_ec512.key ================================================ -----BEGIN PUBLIC KEY----- MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQATqtkp30ZR4tDn8YOiUxTfii29+hV sUq7EsyLLWVusyyGD0t5AGCRd3foxlka3MTtJE0zHnIPmzGAEyKfPwWCql0Bmsqg t82+wcWvmJ2ppF5EBgFOoPeE6xlQONvmmIQY5JS0WxdPUdR6yL84ct7p8c6MKoyj 6MxatdFRDcsztU0ce5o= -----END PUBLIC KEY----- ================================================ FILE: tests/_keys/rsa/encrypted-private.key ================================================ -----BEGIN RSA PRIVATE KEY----- Proc-Type: 4,ENCRYPTED DEK-Info: AES-128-CBC,0D71668CE71033CB9150ED82FC87F4A1 uLzPNDdlHnZ77tAGMHyPYERDMBcdV4SsQJYcSjiHhR2o0dLGTdgOpQrXTHPX4GJF LlEWLhAAV9wx2mM/2kHDWB4uZwThtT9/v+RFoW1WbVO/d3lhI9fg4/73/DWAH/7/ afMRc7ZOVoAmVCESotPx4khCHoE97RdY/JtkLTzc3+peqmL53AbYXrg9rTN1B+ZV U3w4ciQS8Uki87zDYIBjYtaOCyMUTvug25CvdssvUMBoc/Jc0xps6/vAyXrnzlGT pZD0Tst8idswfDi613BhAaxJspeY0AErWA59qJ3eGzbiQq5RDWcbJe/Tz5r/6+NN DkvNQ7DaEZ6LpeWX0MUq6/QWfrM8yE95XhjyC1d3LYn32lXHUygbgTFWIgLDoOE6 nBhu34SWtbLAnqYGewaJFxhlYVS9rb/uvYQg70r5X9Sx6alCQPiPyIv39IItezn2 HF2GRfE91MPZUeDhdqdvvOlSZVM5KnYc1fhamGAwM48gdDDXe8Czu/JEGoANNvC3 l/Z1p5RtGF4hrel9WpeX9zQq3pvtfVcVIiWuRUwCOSQytXlieRK37sMuYeggvmjV VvaCods3mS/panWg9T/D/deIXjhzNJLvyiJg8+3sY5H4yNe0XpbaAc/ySwt9Rcxy FzFQ+5pghLSZgR1uV3AhdcnzXBU2GkYhdGKt2tUsH0UeVQ2BXxTlBFsCOh2dWqcj y3suIG65bukDAAWidQ4q3S6ZIMpXBhhCj7nwB5jQ7wSlU3U9So0ndr7zxdUILiMm chHi3q5apVZnMGcwv2B33rt4nD7HgGEmRKkCelrSrBATY1ut+T4rCDzKDqDs3jpv hYIWrlNPTkJyQz3eWly6Db+FJEfdYGadYJusc7/nOxCh/QmUu8Sh3NhKT6TH0bS7 1AAqd8H+2hJ9I32Dhd2qwAF7PkNe2LGi+P8tbAtepKGim5w65wnsPePMnrfxumsG PeDnMrqeCKy+fME7a/MS5kmEBpmD4BMhVC6/OhFVz8gBty1f8yIEZggHNQN2QK7m NIrG+PwqW2w8HoxOlAi2Ix4LTPifrdfsH02U7aM1pgo1rZzD4AOzqvzCaK43H2VB BHLeTBGoLEUxXA9C+iGbeQlKXkMC00QKkjK5+nvkvnvePFfsrTQIpuyGufD/MoPb 6fpwsyHZDxhxMN1PJk1b1lPq2Ui4hXpVNOYd4Q6OQz7bwxTMRX9XQromUlKMMgAT edX8v2NdM7Ssy1IwHuGVbDEpZdjoeaWZ1iNRV17i/EaJAqwYDQLfsuHBlzZL1ov1 xkKVJdL8Y3q80oRAzTQDVdzL/rI44LLAfv609YByCnw29feYJY2W6gV0O7ZSw413 XUkc5CaEbR1LuG8NtnOOPJV4Tb/hNsIDtvVm7Hl5npBKBe4iVgQ2LNuC2eT69d/z uvzgjISlumPiO5ivuYe0QtLPuJSc+/Bl8bPL8gcNQEtqkzj7IftHPPZNs+bJC2uY bPjq5KoDNAMF6VHuKHwu48MBYpnXDIg3ZenmJwGRULRBhK6324hDS6NJ7ULTBU2M TZCHmg89ySLBfCAspVeo63o/R7bs9a7BP9x2h5uwCBogSvkEwhhPKnboVN45bp9c -----END RSA PRIVATE KEY----- ================================================ FILE: tests/_keys/rsa/encrypted-public.key ================================================ -----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwLpbUP8a9yflt5LKUUS3 NPuRM7yEouPWg0VKeY5AURu4i8bqQ20K5jwfRJ+w05FvlywG4EuxpnpTFTVS2/do q3xufzTf/C3KIDOAHEifkdx4140btKxxm4mD9Eu2CQ32adZyScha50KUFlfnAAic Hb8wYxjFyWo3PAbGYmCQCn2z97Ab0Ar6NR1e+V9f8EL9Orr2f04puKJfQTZdWVDF UJR4w7QZ/CPY0LEsiFLW3QQCNraka1mtrLJwPqreBtDEkj8IoISNkrguu/97RQZz miJgBQkVjr6OfqG5WIFr0MzbRZc1/aK9g8ft88nhhQm0E3GqkCxBKTwgA03HtK07 qQIDAQAB -----END PUBLIC KEY----- ================================================ FILE: tests/_keys/rsa/private.key ================================================ -----BEGIN PRIVATE KEY----- MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDTvwE87MtgREYL TL4aHhQo3ZzogmxxvMUsKnPzyxRs1YrXOSOpwN0npsXarBKKVIUMNLfFODp/vnQn 2Zp06N8XG59WAOKwvC4MfxLDQkA+JXggzHlkbVoTN+dUkdYIFqSKuAPGwiWToRK2 SxEhij3rE2FON8jQZvDxZkiP9a4vxJO3OTPQwKredXFiObsXD/c3RtLFhKctjCyH OIrP0bQEsee/m7JNtG4ry6BPusN6wb+vJo5ieBYPa3c19akNq6q/nYWhplhkkJSu aOrL5xXEFzI5TvcvnXR568GVcxK8YLfFkdxpsXGt5rAbeh0h/U5kILEAqv8P9PGT ZpicKbrnAgMBAAECggEAd3yTQEQHR91/ASVfKPHMQns77eCbPVtekFusbugsMHYY EPdHbqVMpvFvOMRc+f5Tzd15ziq6qBdbCJm8lThLm4iU0z1QrpaiDZ8vgUvDYM5Y CXoZDli+uZWUTp60/n94fmb0ipZIChScsI2PrzOJWTvobvD/uso8MJydWc8zafQm uqYzygOfjFZvU4lSfgzpefhpquy0JUy5TiKRmGUnwLb3TtcsVavjsn4QmNwLYgOF 2OE+R12ex3pAKTiRE6FcnE1xFIo1GKhBa2Otgw3MDO6Gg+kn8Q4alKz6C6RRlgaH R7sYzEfJhsk/GGFTYOzXKQz2lSaStKt9wKCor04RcQKBgQDzPOu5jCTfayUo7xY2 jHtiogHyKLLObt9l3qbwgXnaD6rnxYNvCrA0OMvT+iZXsFZKJkYzJr8ZOxOpPROk 10WdOaefiwUyL5dypueSwlIDwVm+hI4Bs82MajHtzOozh+73wA+aw5rPs84Uix9w VbbwaVR6qP/BV09yJYS5kQ7fmwKBgQDe2xjywX2d2MC+qzRr+LfU+1+gq0jjhBCX WHqRN6IECB0xTnXUf9WL/VCoI1/55BhdbbEja+4btYgcXSPmlXBIRKQ4VtFfVmYB kPXeD8oZ7LyuNdCsbKNe+x1IHXDe6Wfs3L9ulCfXxeIE84wy3fd66mQahyXV9iD9 CkuifMqUpQKBgQCiydHlY1LGJ/o9tA2Ewm5Na6mrvOs2V2Ox1NqbObwoYbX62eiF 53xX5u8bVl5U75JAm+79it/4bd5RtKux9dUETbLOhwcaOFm+hM+VG/IxyzRZ2nMD 1qcpY2U5BpxzknUvYF3RMTop6edxPk7zKpp9ubCtSu+oINvtxAhY/SkcIwKBgGP1 upcImyO2GZ5shLL5eNubdSVILwV+M0LveOqyHYXZbd6z5r5OKKcGFKuWUnJwEU22 6gGNY9wh7M9sJ7JBzX9c6pwqtPcidda2AtJ8GpbOTUOG9/afNBhiYpv6OKqD3w2r ZmJfKg/qvpqh83zNezgy8nvDqwDxyZI2j/5uIx/RAoGBAMWRmxtv6H2cKhibI/aI MTJM4QRjyPNxQqvAQsv+oHUbid06VK3JE+9iQyithjcfNOwnCaoO7I7qAj9QEfJS MZQc/W/4DHJebo2kd11yoXPVTXXOuEwLSKCejBXABBY0MPNuPUmiXeU0O3Tyi37J TUKzrgcd7NvlA41Y4xKcOqEA -----END PRIVATE KEY----- ================================================ FILE: tests/_keys/rsa/private_512.key ================================================ -----BEGIN PRIVATE KEY----- MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAxZT4cHZXf5QfGX1m oiSKKSC6AeFO8tGIn9C+4x/bEaQAq6f+V5/0+lFG6uboGC7eItPNWfOMmLnrI162 cOnB1QIDAQABAkBn4OKdfhqSoLWZGS0UolFhPiuNQX/YegFyxLUXhHAQ3VQdUAHs 4jFT2tviDI1uREdCooKyIQIYOVILrikkc8QBAiEA67ch0VSQvH6A2YO8mKiAUz40 aB3S4bUKdgj9FoLO/OECIQDWlb/G65w4nfXBplhChGm7SKTS+4zfe6SuqVQYsF8P dQIgYlCtC0mxYN2G0rLOzAGkHJRaeX7PAZNofJj9LxF6UiECIEMPX3SJ8zNaYhAX rSN0gBpwVFo/FMJOwKN49XgVvk91AiEAk/DSNPKv1djAz5nzd4t9I4tOqrbPwWr1 QOxSsGdKfBg= -----END PRIVATE KEY----- ================================================ FILE: tests/_keys/rsa/public.key ================================================ -----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA078BPOzLYERGC0y+Gh4U KN2c6IJscbzFLCpz88sUbNWK1zkjqcDdJ6bF2qwSilSFDDS3xTg6f750J9madOjf FxufVgDisLwuDH8Sw0JAPiV4IMx5ZG1aEzfnVJHWCBakirgDxsIlk6EStksRIYo9 6xNhTjfI0Gbw8WZIj/WuL8STtzkz0MCq3nVxYjm7Fw/3N0bSxYSnLYwshziKz9G0 BLHnv5uyTbRuK8ugT7rDesG/ryaOYngWD2t3NfWpDauqv52FoaZYZJCUrmjqy+cV xBcyOU73L510eevBlXMSvGC3xZHcabFxreawG3odIf1OZCCxAKr/D/Txk2aYnCm6 5wIDAQAB -----END PUBLIC KEY----- ================================================ FILE: tests/_keys/rsa/public_512.key ================================================ -----BEGIN PUBLIC KEY----- MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAMWU+HB2V3+UHxl9ZqIkiikgugHhTvLR iJ/QvuMf2xGkAKun/lef9PpRRurm6Bgu3iLTzVnzjJi56yNetnDpwdUCAwEAAQ== -----END PUBLIC KEY-----