From 1fae8c46348ed6da8a56deaff282034cb57673e0 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Mon, 26 Oct 2020 10:34:38 -0700 Subject: [PATCH 1/9] docs: add JWK usage to README (#307) --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index 9c8b5455..ba139079 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,19 @@ echo "Decode:\n" . print_r($decoded_array, true) . "\n"; ?> ``` +Using JWKs +---------- + +```php +// Set of keys. The "keys" key is required. For example, the JSON response to +// this endpoint: https://www.gstatic.com/iap/verify/public_key-jwk +$jwks = ['keys' => []]; + +// JWK::parseKeySet($jwks) returns an associative array of **kid** to private +// key. Pass this as the second parameter to JWT::decode. +JWT::decode($payload, JWK::parseKeySet($jwks), $supportedAlgorithm); +``` + Changelog --------- From f42c9110abe98dd6cfe9053c49bc86acc70b2d23 Mon Sep 17 00:00:00 2001 From: Grant Anderson Date: Thu, 11 Feb 2021 17:02:00 -0700 Subject: [PATCH 2/9] fix: add missing use statement in JWK (#303) --- src/JWK.php | 1 + tests/JWKTest.php | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/src/JWK.php b/src/JWK.php index 1d273917..7632f4a4 100644 --- a/src/JWK.php +++ b/src/JWK.php @@ -3,6 +3,7 @@ namespace Firebase\JWT; use DomainException; +use InvalidArgumentException; use UnexpectedValueException; /** diff --git a/tests/JWKTest.php b/tests/JWKTest.php index 3d317d55..b8b67540 100644 --- a/tests/JWKTest.php +++ b/tests/JWKTest.php @@ -43,6 +43,20 @@ public function testParseJwkKeySet() self::$keys = $keys; } + public function testParseJwkKey_empty() + { + $this->setExpectedException('InvalidArgumentException', 'JWK must not be empty'); + + JWK::parseKeySet(array('keys' => array(array()))); + } + + public function testParseJwkKeySet_empty() + { + $this->setExpectedException('InvalidArgumentException', 'JWK Set did not contain any keys'); + + JWK::parseKeySet(array('keys' => array())); + } + /** * @depends testParseJwkKeySet */ From 7b4f4d2641d5b370f73ed0e6bcf340beddcc0ca3 Mon Sep 17 00:00:00 2001 From: Benoit Borrel <234378+bborrel@users.noreply.github.com> Date: Fri, 5 Mar 2021 14:39:07 -0500 Subject: [PATCH 3/9] chore: add phpdoc @throws in JWT::decode (#320) --- src/JWT.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/JWT.php b/src/JWT.php index 4860028b..76a0551c 100644 --- a/src/JWT.php +++ b/src/JWT.php @@ -62,6 +62,7 @@ class JWT * * @return object The JWT's payload as a PHP object * + * @throws InvalidArgumentException Provided JWT was empty * @throws UnexpectedValueException Provided JWT was invalid * @throws SignatureInvalidException Provided JWT was invalid because the signature verification failed * @throws BeforeValidException Provided JWT is trying to be used before it's eligible as defined by 'nbf' From 474047dbd8442a730ab03810f2835839b107cd29 Mon Sep 17 00:00:00 2001 From: Ashutosh K Tripathi Date: Sat, 6 Mar 2021 03:07:26 +0530 Subject: [PATCH 4/9] chore: remove leading backslashes in imports (#301) Co-authored-by: Brent Shaffer --- src/JWT.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/JWT.php b/src/JWT.php index 76a0551c..b167abd7 100644 --- a/src/JWT.php +++ b/src/JWT.php @@ -2,10 +2,10 @@ namespace Firebase\JWT; -use \DomainException; -use \InvalidArgumentException; -use \UnexpectedValueException; -use \DateTime; +use DomainException; +use InvalidArgumentException; +use UnexpectedValueException; +use DateTime; /** * JSON Web Token implementation, based on this spec: From 1b99208a49381e054d0ab8771cb9c208014f4df8 Mon Sep 17 00:00:00 2001 From: Alan Si Date: Sun, 1 Nov 2020 10:39:26 +0800 Subject: [PATCH 5/9] Code for microsoft jwt --- .github/workflows/coveralls.yml | 36 ++ .github/workflows/php.yml | 33 ++ .gitignore | 7 + .php_cs | 53 +++ .unibeautifyrc.yml | 5 + LICENSE => LICENCE | 2 + README.md | 318 ++++++++---------- composer.json | 79 +++-- phpunit.xml.dist | 24 +- src/Adfs/AdfsAccessTokenJWT.php | 13 + src/Adfs/AdfsConfiguration.php | 38 +++ src/Adfs/AdfsIdTokenJWT.php | 13 + src/AzureAd/AzureAdAccessTokenJWT.php | 13 + src/AzureAd/AzureAdConfiguration.php | 66 ++++ src/AzureAd/AzureAdIdTokenJWT.php | 13 + src/Base/MicrosoftAccessTokenJWT.php | 16 + src/Base/MicrosoftConfiguration.php | 173 ++++++++++ src/Base/MicrosoftIdTokenJWT.php | 21 ++ src/Base/MicrosoftJWT.php | 87 +++++ src/BeforeValidException.php | 5 +- src/ExpiredException.php | 5 +- src/JWK.php | 56 +-- src/JWT.php | 208 +++++++----- src/SignatureInvalidException.php | 5 +- tests/Adfs/AdfsAccessTokenJWTTest.php | 146 ++++++++ tests/Adfs/AdfsConfigurationTest.php | 126 +++++++ tests/Adfs/AdfsIdTokenJWTTest.php | 147 ++++++++ tests/AzureAd/AzureAdAccessTokenJWTTest.php | 148 ++++++++ tests/AzureAd/AzureAdConfigurationTest.php | 139 ++++++++ tests/AzureAd/AzureAdIdTokenJWTTest.php | 149 ++++++++ tests/JWKExtrasTest.php | 89 +++++ tests/JWKTest.php | 34 +- tests/JWTExtrasTest.php | 122 +++++++ tests/JWTTest.php | 187 +++++----- .../adfs/configuration/configuration.json | 75 +++++ .../metadata/adfs/configuration/jwks_uri.json | 14 + .../adfs/configuration/jwks_uri_private.json | 18 + tests/metadata/adfs/configuration/private.pem | 27 ++ .../azure_ad/configuration/configuration.json | 52 +++ .../azure_ad/configuration/jwks_uri.json | 14 + .../configuration/jwks_uri_private.json | 18 + .../azure_ad/configuration/private.pem | 27 ++ tests/metadata/jwk/rsa-public-no-e.json | 7 + tests/metadata/jwk/rsa-public-no-n.json | 7 + tests/metadata/jwk/rsa-public-private.json | 14 + tests/metadata/jwk/rsa-public.json | 8 + 46 files changed, 2423 insertions(+), 434 deletions(-) create mode 100644 .github/workflows/coveralls.yml create mode 100644 .github/workflows/php.yml create mode 100644 .php_cs create mode 100644 .unibeautifyrc.yml rename LICENSE => LICENCE (98%) create mode 100644 src/Adfs/AdfsAccessTokenJWT.php create mode 100644 src/Adfs/AdfsConfiguration.php create mode 100644 src/Adfs/AdfsIdTokenJWT.php create mode 100644 src/AzureAd/AzureAdAccessTokenJWT.php create mode 100644 src/AzureAd/AzureAdConfiguration.php create mode 100644 src/AzureAd/AzureAdIdTokenJWT.php create mode 100644 src/Base/MicrosoftAccessTokenJWT.php create mode 100644 src/Base/MicrosoftConfiguration.php create mode 100644 src/Base/MicrosoftIdTokenJWT.php create mode 100644 src/Base/MicrosoftJWT.php create mode 100644 tests/Adfs/AdfsAccessTokenJWTTest.php create mode 100644 tests/Adfs/AdfsConfigurationTest.php create mode 100644 tests/Adfs/AdfsIdTokenJWTTest.php create mode 100644 tests/AzureAd/AzureAdAccessTokenJWTTest.php create mode 100644 tests/AzureAd/AzureAdConfigurationTest.php create mode 100644 tests/AzureAd/AzureAdIdTokenJWTTest.php create mode 100644 tests/JWKExtrasTest.php create mode 100644 tests/JWTExtrasTest.php create mode 100644 tests/metadata/adfs/configuration/configuration.json create mode 100644 tests/metadata/adfs/configuration/jwks_uri.json create mode 100644 tests/metadata/adfs/configuration/jwks_uri_private.json create mode 100644 tests/metadata/adfs/configuration/private.pem create mode 100644 tests/metadata/azure_ad/configuration/configuration.json create mode 100644 tests/metadata/azure_ad/configuration/jwks_uri.json create mode 100644 tests/metadata/azure_ad/configuration/jwks_uri_private.json create mode 100644 tests/metadata/azure_ad/configuration/private.pem create mode 100644 tests/metadata/jwk/rsa-public-no-e.json create mode 100644 tests/metadata/jwk/rsa-public-no-n.json create mode 100644 tests/metadata/jwk/rsa-public-private.json create mode 100644 tests/metadata/jwk/rsa-public.json diff --git a/.github/workflows/coveralls.yml b/.github/workflows/coveralls.yml new file mode 100644 index 00000000..dde873c5 --- /dev/null +++ b/.github/workflows/coveralls.yml @@ -0,0 +1,36 @@ +name: Submit Coveralls + +on: + push: + branches: [master, integration] + pull_request: + branches: [master, integration] + +jobs: + build: + name: Build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Validate composer.json and composer.lock + run: composer validate + + - name: Cache Composer packages + id: composer-cache + uses: actions/cache@v2 + with: + path: vendor + key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} + restore-keys: | + ${{ runner.os }}-php- + + - name: Install dependencies + if: steps.composer-cache.outputs.cache-hit != 'true' + run: composer install --prefer-dist --no-progress --no-suggest + + - name: Run test and submit to coveralls + env: + COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + composer run coveralls diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml new file mode 100644 index 00000000..dd862c60 --- /dev/null +++ b/.github/workflows/php.yml @@ -0,0 +1,33 @@ +name: PHP Test + +on: + push: + branches: [master, integration] + pull_request: + branches: [master, integration] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: Validate composer.json and composer.lock + run: composer validate + + - name: Cache Composer packages + id: composer-cache + uses: actions/cache@v2 + with: + path: vendor + key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} + restore-keys: | + ${{ runner.os }}-php- + + - name: Install dependencies + if: steps.composer-cache.outputs.cache-hit != 'true' + run: composer install --prefer-dist --no-progress --no-suggest + + - name: Run test suite + run: composer run test diff --git a/.gitignore b/.gitignore index 080f19aa..233cd741 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,10 @@ phpunit.phar phpunit.phar.asc composer.phar composer.lock + +coverage +coverage/* + +build + +.coveralls.yml diff --git a/.php_cs b/.php_cs new file mode 100644 index 00000000..2a46ea2b --- /dev/null +++ b/.php_cs @@ -0,0 +1,53 @@ +exclude('node_modules') + ->exclude('vendor') + ->in(__DIR__); + +return PhpCsFixer\Config::create() + ->setRules([ + '@PSR2' => true, + 'array_syntax' => ['syntax' => 'short'], + 'array_indentation' => true, + 'cast_spaces' => true, + 'combine_consecutive_unsets' => true, + 'concat_space' => ['spacing' => 'one'], + 'linebreak_after_opening_tag' => true, + 'no_blank_lines_after_class_opening' => true, + 'no_blank_lines_after_phpdoc' => true, + 'no_extra_consecutive_blank_lines' => true, + 'no_trailing_comma_in_singleline_array' => true, + 'no_whitespace_in_blank_line' => true, + 'no_spaces_around_offset' => true, + 'no_unused_imports' => true, + 'no_useless_else' => true, + 'no_useless_return' => true, + 'no_whitespace_before_comma_in_array' => true, + 'normalize_index_brace' => true, + 'phpdoc_indent' => true, + 'phpdoc_to_comment' => true, + 'phpdoc_trim' => true, + 'single_quote' => true, + 'trailing_comma_in_multiline_array' => true, + 'trim_array_spaces' => true, + 'method_argument_space' => ['ensure_fully_multiline' => false], + 'no_break_comment' => false, + 'blank_line_before_statement' => true, + ]) + ->setFinder($finder); \ No newline at end of file diff --git a/.unibeautifyrc.yml b/.unibeautifyrc.yml new file mode 100644 index 00000000..8f5cff68 --- /dev/null +++ b/.unibeautifyrc.yml @@ -0,0 +1,5 @@ +PHP: + beautifiers: + - PHP-CS-Fixer + PHP-CS-Fixer: + prefer_beautifier_config: "./php_cs" diff --git a/LICENSE b/LICENCE similarity index 98% rename from LICENSE rename to LICENCE index cb0c49b3..8f074417 100644 --- a/LICENSE +++ b/LICENCE @@ -1,3 +1,5 @@ +BSD 3-Clause License + Copyright (c) 2011, Neuman Vong All rights reserved. diff --git a/README.md b/README.md index ba139079..0ecec9e2 100644 --- a/README.md +++ b/README.md @@ -1,213 +1,175 @@ -[![Build Status](https://travis-ci.org/firebase/php-jwt.png?branch=master)](https://travis-ci.org/firebase/php-jwt) -[![Latest Stable Version](https://poser.pugx.org/firebase/php-jwt/v/stable)](https://packagist.org/packages/firebase/php-jwt) -[![Total Downloads](https://poser.pugx.org/firebase/php-jwt/downloads)](https://packagist.org/packages/firebase/php-jwt) -[![License](https://poser.pugx.org/firebase/php-jwt/license)](https://packagist.org/packages/firebase/php-jwt) +[![Packagist](https://img.shields.io/packagist/v/alancting/php-microsoft-jwt?style=for-the-badge)](https://packagist.org/packages/alancting/php-microsoft-jwt) +[![GitHub](https://img.shields.io/github/v/release/alancting/php-microsoft-jwt?label=GitHub&style=for-the-badge)](https://github.com/alancting/php-microsoft-jwt) +[![Test](https://img.shields.io/github/workflow/status/alancting/php-microsoft-jwt/PHP%20Test?label=TEST&style=for-the-badge)](https://github.com/alancting/php-microsoft-jwt) +[![Coverage Status](https://img.shields.io/coveralls/github/alancting/php-microsoft-jwt/master?style=for-the-badge)](https://coveralls.io/github/alancting/php-microsoft-jwt?branch=master) +[![GitHub license](https://img.shields.io/github/license/alancting/php-microsoft-jwt?color=green&style=for-the-badge)](https://github.com/alancting/php-microsoft-jwt/blob/master/LICENCE) +[![firebase/php-jwt Version](https://img.shields.io/static/v1?label=firebase%2Fphp-jwt&message=5.2.0&color=blue&style=for-the-badge)](https://github.com/firebase/php-jwt/tree/v5.2.0) -PHP-JWT -======= -A simple library to encode and decode JSON Web Tokens (JWT) in PHP, conforming to [RFC 7519](https://tools.ietf.org/html/rfc7519). +# php-microsoft-jwt -Installation ------------- +A simple library to validate and decode Microsoft Azure Active Directory ([Azure AD](https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-app-types)), Microsoft Active Directory Federation Services (ADFS) JSON Web Tokens (JWT) in PHP, conforming to [RFC 7519](https://tools.ietf.org/html/rfc7519). -Use composer to manage your dependencies and download PHP-JWT: +**Forked From [firebase/php-jwt](https://github.com/firebase/php-jwt)** + +## Installation + +Use composer to manage your dependencies and download php-microsoft-jwt: ```bash -composer require firebase/php-jwt +composer require alancting/php-microsoft-jwt ``` -Example -------- +## Example + +### ADFS + ```php "http://example.org", - "aud" => "http://example.com", - "iat" => 1356999524, - "nbf" => 1357000000 -); +use Alancting\Microsoft\JWT\Adfs\AdfsConfiguration; +use Alancting\Microsoft\JWT\Adfs\AdfsAccessTokenJWT; +use Alancting\Microsoft\JWT\Adfs\AdfsIdTokenJWT; + +... /** - * IMPORTANT: - * You must specify supported algorithms for your application. See - * https://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-40 - * for a list of spec-compliant algorithms. + * AdfsConfiguration class will go to https://{you_asfs_hostname}/adfs/.well-known/openid-configuration to parse the configuration for your application + * */ -$jwt = JWT::encode($payload, $key); -$decoded = JWT::decode($jwt, $key, array('HS256')); +$config_options = [ + 'client_id' => '{client_id}', + 'hostname' => '{you_asfs_hostname}', +]; -print_r($decoded); +/** + * You can also specific the local configuration by + */ +// $config_options = [ +// 'client_id' => '{client_id}', +// 'config_uri' => 'local_path_to_configuration_json', +// ]; -/* - NOTE: This will now be an object instead of an associative array. To get - an associative array, you will need to cast it as such: -*/ +$config = new AdfsConfiguration($config_options); -$decoded_array = (array) $decoded; +$id_token = 'adfs.id.token.jwt'; +$access_token = 'adfs.access.token.jwt'; /** - * You can add a leeway to account for when there is a clock skew times between - * the signing and verifying servers. It is recommended that this leeway should - * not be bigger than a few minutes. - * - * Source: http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html#nbfDef + * If id token is invalid, exception will be thrown. + */ +$id_token_jwt = new AdfsIdTokenJWT($config, $id_token); +echo "\n"; +// Getting payload from id token +print_r($id_token_jwt->getPayload()); +echo "\n"; +// Getting value from payload by attribute of id token +print_r($id_token_jwt->get('attribute_name')); +echo "\n"; + +/** + * If id token is invalid, exception will be thrown. + * To validate and decode access token jwt, you need to pass $audience (scope name of your app) */ -JWT::$leeway = 60; // $leeway in seconds -$decoded = JWT::decode($jwt, $key, array('HS256')); +$access_token_jwt = new AdfsAccessTokenJWT($config, $access_token, $audience); +echo "\n"; +// Getting payload from access token +print_r($access_token_jwt->getPayload()); +echo "\n"; +// Getting value from payload by attribute of access token +print_r($access_token_jwt->get('attribute_name')); +echo "\n"; -?> +/** + * You might want to 'cache' the tokens for expire validation + * To check whether the access token and id token are expired, simply call + */ +echo ($id_token_jwt->isExpired()) ? 'Id token is expired' : 'Id token is valid'; +echo ($id_token->isExpired()) ? 'Access token is expired' : 'Access token is valid'; ``` -Example with RS256 (openssl) ----------------------------- + +### Azure Ad + ```php "example.org", - "aud" => "example.com", - "iat" => 1356999524, - "nbf" => 1357000000 -); - -$jwt = JWT::encode($payload, $privateKey, 'RS256'); -echo "Encode:\n" . print_r($jwt, true) . "\n"; - -$decoded = JWT::decode($jwt, $publicKey, array('RS256')); - -/* - NOTE: This will now be an object instead of an associative array. To get - an associative array, you will need to cast it as such: -*/ - -$decoded_array = (array) $decoded; -echo "Decode:\n" . print_r($decoded_array, true) . "\n"; -?> -``` -Using JWKs ----------- +use Alancting\Microsoft\JWT\AzureAd\AzureAdConfiguration; +use Alancting\Microsoft\JWT\AzureAd\AzureAdAccessTokenJWT; +use Alancting\Microsoft\JWT\AzureAd\AzureAdIdTokenJWT; -```php -// Set of keys. The "keys" key is required. For example, the JSON response to -// this endpoint: https://www.gstatic.com/iap/verify/public_key-jwk -$jwks = ['keys' => []]; +... -// JWK::parseKeySet($jwks) returns an associative array of **kid** to private -// key. Pass this as the second parameter to JWT::decode. -JWT::decode($payload, JWK::parseKeySet($jwks), $supportedAlgorithm); +/** + * AzureAdConfiguration class will go to https://login.microsoftonline.com/{tenant}/v2.0/.well-known/openid-configuration to parse the configuration for your application + */ +$config_options = [ + 'tenant' => '{tenant_id} | common | organizations | consumers', + 'tenant_id' => '{tenant_id}', + 'client_id' => '{client_id}' +]; + +/** + * You can also specific the local configuration by + */ +// $config_options = [ +// 'tenant' => '{tenant_id} | common | organizations | consumers', +// 'tenant_id' => '{tenant_id}', +// 'client_id' => '{client_id}' +// 'config_uri' => 'local_path_to_configuration_json', +// ]; + +$config = new AzureAdConfiguration($config_options); + +$id_token = 'azure_ad.id.token.jwt'; +$access_token = 'azure_ad.access.token.jwt'; + +/** + * If id token is invalid, exception will be thrown. + */ +$id_token_jwt = new AzureAdIdTokenJWT($config, $id_token); +echo "\n"; +/** + * You could also pass $audience if needed + */ +// $id_token_jwt = new AzureAdIdTokenJWT($config, $id_token, $audience); +// echo "\n"; + +// Getting payload from id token +print_r($id_token_jwt->getPayload()); +echo "\n"; +// Getting value from payload by attribute of id token +print_r($id_token_jwt->get('attribute_name')); +echo "\n"; + +/** + * If id token is invalid, exception will be thrown. + * To validate and decode access token jwt, you need to pass $audience (scope name of your app) + */ +$access_token_jwt = new AzureAdAccessTokenJWT($config, $access_token, $audience); +echo "\n"; +// Getting payload from access token +print_r($access_token_jwt->getPayload()); +echo "\n"; +// Getting value from payload by attribute of access token +print_r($access_token_jwt->get('attribute_name')); +echo "\n"; + +/** + * You might want to 'cache' the tokens for expire validation + * To check whether the access token and id token are expired, simply call + */ +echo ($id_token_jwt->isExpired()) ? 'Id token is expired' : 'Id token is valid'; +echo ($id_token->isExpired()) ? 'Access token is expired' : 'Access token is valid'; ``` -Changelog ---------- - -#### 5.0.0 / 2017-06-26 -- Support RS384 and RS512. - See [#117](https://github.com/firebase/php-jwt/pull/117). Thanks [@joostfaassen](https://github.com/joostfaassen)! -- Add an example for RS256 openssl. - See [#125](https://github.com/firebase/php-jwt/pull/125). Thanks [@akeeman](https://github.com/akeeman)! -- Detect invalid Base64 encoding in signature. - See [#162](https://github.com/firebase/php-jwt/pull/162). Thanks [@psignoret](https://github.com/psignoret)! -- Update `JWT::verify` to handle OpenSSL errors. - See [#159](https://github.com/firebase/php-jwt/pull/159). Thanks [@bshaffer](https://github.com/bshaffer)! -- Add `array` type hinting to `decode` method - See [#101](https://github.com/firebase/php-jwt/pull/101). Thanks [@hywak](https://github.com/hywak)! -- Add all JSON error types. - See [#110](https://github.com/firebase/php-jwt/pull/110). Thanks [@gbalduzzi](https://github.com/gbalduzzi)! -- Bugfix 'kid' not in given key list. - See [#129](https://github.com/firebase/php-jwt/pull/129). Thanks [@stampycode](https://github.com/stampycode)! -- Miscellaneous cleanup, documentation and test fixes. - See [#107](https://github.com/firebase/php-jwt/pull/107), [#115](https://github.com/firebase/php-jwt/pull/115), - [#160](https://github.com/firebase/php-jwt/pull/160), [#161](https://github.com/firebase/php-jwt/pull/161), and - [#165](https://github.com/firebase/php-jwt/pull/165). Thanks [@akeeman](https://github.com/akeeman), - [@chinedufn](https://github.com/chinedufn), and [@bshaffer](https://github.com/bshaffer)! - -#### 4.0.0 / 2016-07-17 -- Add support for late static binding. See [#88](https://github.com/firebase/php-jwt/pull/88) for details. Thanks to [@chappy84](https://github.com/chappy84)! -- Use static `$timestamp` instead of `time()` to improve unit testing. See [#93](https://github.com/firebase/php-jwt/pull/93) for details. Thanks to [@josephmcdermott](https://github.com/josephmcdermott)! -- Fixes to exceptions classes. See [#81](https://github.com/firebase/php-jwt/pull/81) for details. Thanks to [@Maks3w](https://github.com/Maks3w)! -- Fixes to PHPDoc. See [#76](https://github.com/firebase/php-jwt/pull/76) for details. Thanks to [@akeeman](https://github.com/akeeman)! - -#### 3.0.0 / 2015-07-22 -- Minimum PHP version updated from `5.2.0` to `5.3.0`. -- Add `\Firebase\JWT` namespace. See -[#59](https://github.com/firebase/php-jwt/pull/59) for details. Thanks to -[@Dashron](https://github.com/Dashron)! -- Require a non-empty key to decode and verify a JWT. See -[#60](https://github.com/firebase/php-jwt/pull/60) for details. Thanks to -[@sjones608](https://github.com/sjones608)! -- Cleaner documentation blocks in the code. See -[#62](https://github.com/firebase/php-jwt/pull/62) for details. Thanks to -[@johanderuijter](https://github.com/johanderuijter)! - -#### 2.2.0 / 2015-06-22 -- Add support for adding custom, optional JWT headers to `JWT::encode()`. See -[#53](https://github.com/firebase/php-jwt/pull/53/files) for details. Thanks to -[@mcocaro](https://github.com/mcocaro)! - -#### 2.1.0 / 2015-05-20 -- Add support for adding a leeway to `JWT:decode()` that accounts for clock skew -between signing and verifying entities. Thanks to [@lcabral](https://github.com/lcabral)! -- Add support for passing an object implementing the `ArrayAccess` interface for -`$keys` argument in `JWT::decode()`. Thanks to [@aztech-dev](https://github.com/aztech-dev)! - -#### 2.0.0 / 2015-04-01 -- **Note**: It is strongly recommended that you update to > v2.0.0 to address - known security vulnerabilities in prior versions when both symmetric and - asymmetric keys are used together. -- Update signature for `JWT::decode(...)` to require an array of supported - algorithms to use when verifying token signatures. - - -Tests ------ +## Tests + Run the tests using phpunit: ```bash -$ pear install PHPUnit -$ phpunit --configuration phpunit.xml.dist -PHPUnit 3.7.10 by Sebastian Bergmann. -..... -Time: 0 seconds, Memory: 2.50Mb -OK (5 tests, 5 assertions) +$ composer install +$ composer run test ``` -New Lines in private keys ------ - -If your private key contains `\n` characters, be sure to wrap it in double quotes `""` -and not single quotes `''` in order to properly interpret the escaped characters. +## License -License -------- [3-Clause BSD](http://opensource.org/licenses/BSD-3-Clause). diff --git a/composer.json b/composer.json index 25d1cfa9..649a300a 100644 --- a/composer.json +++ b/composer.json @@ -1,33 +1,52 @@ { - "name": "firebase/php-jwt", - "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", - "homepage": "https://github.com/firebase/php-jwt", - "keywords": [ - "php", - "jwt" - ], - "authors": [ - { - "name": "Neuman Vong", - "email": "neuman+pear@twilio.com", - "role": "Developer" - }, - { - "name": "Anant Narayanan", - "email": "anant@php.net", - "role": "Developer" - } - ], - "license": "BSD-3-Clause", - "require": { - "php": ">=5.3.0" - }, - "autoload": { - "psr-4": { - "Firebase\\JWT\\": "src" - } - }, - "require-dev": { - "phpunit/phpunit": ">=4.8 <=9" + "name": "alancting/php-microsoft-jwt", + "description": "A simple library to validate and decode Microsoft Azure Active Directory (Azure AD), Microsoft Active Directory Federation Services (ADFS) JSON Web Tokens (JWT) in PHP, conforming to RFC 7519", + "homepage": "https://github.com/alancting/php-microsoft-jwt", + "keywords": [ + "php", + "jwt", + "openid", + "adfs", + "azure", + "ad", + "microsoft" + ], + "version": "1.0.0", + "authors": [ + { + "name": "Neuman Vong", + "email": "neuman+pear@twilio.com", + "role": "Developer" + }, { + "name": "Anant Narayanan", + "email": "anant@php.net", + "role": "Developer" + }, { + "name": "alancting", + "homepage": "https://github.com/alancting" } + ], + "license": "BSD-3-Clause", + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": ">=4.8 <=9", + "php-coveralls/php-coveralls": "^2.3" + }, + "autoload": { + "psr-4": { + "Alancting\\Microsoft\\JWT\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Alancting\\Microsoft\\Tests\\": "tests/" + } + }, + "scripts": { + "test": ["./vendor/bin/phpunit --colors=always"], + "coverage": ["./vendor/bin/phpunit --colors=always --coverage-text --coverage-html ./coverage --coverage-clover=build/logs/clover.xml"], + "coveralls": ["composer run coverage && ./vendor/bin/php-coveralls"] + } } diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 9f85f5ba..461937e6 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,19 +1,21 @@ - + ./tests + ./tests/JWKTest.php + ./tests/JWTTest.php + + + + ./src + + + + + + diff --git a/src/Adfs/AdfsAccessTokenJWT.php b/src/Adfs/AdfsAccessTokenJWT.php new file mode 100644 index 00000000..31777aff --- /dev/null +++ b/src/Adfs/AdfsAccessTokenJWT.php @@ -0,0 +1,13 @@ +hostname = $options['hostname']; + } + + $options['config_uri'] = isset($options['config_uri']) ? $options['config_uri'] : $this->getRemoteConfigUri(); + + parent::__construct($options); + } + + protected function getDefaultSigningAlgValues() + { + return ['RS256']; + } + + private function getRemoteConfigUri() + { + return $this->replaceStr($this->base_url, 'hostname', $this->hostname); + } +} \ No newline at end of file diff --git a/src/Adfs/AdfsIdTokenJWT.php b/src/Adfs/AdfsIdTokenJWT.php new file mode 100644 index 00000000..5ff364f7 --- /dev/null +++ b/src/Adfs/AdfsIdTokenJWT.php @@ -0,0 +1,13 @@ +getConfiguration()->getClientId(); + } +} \ No newline at end of file diff --git a/src/AzureAd/AzureAdAccessTokenJWT.php b/src/AzureAd/AzureAdAccessTokenJWT.php new file mode 100644 index 00000000..494a2a45 --- /dev/null +++ b/src/AzureAd/AzureAdAccessTokenJWT.php @@ -0,0 +1,13 @@ +tenant = $options['tenant']; + $this->tenant_id = $options['tenant_id']; + + $options['config_uri'] = isset($options['config_uri']) ? $options['config_uri'] : $this->getRemoteConfigUri(); + + parent::__construct($options); + } + + protected function getDefaultSigningAlgValues() + { + return ['RS256']; + } + + public function getTenant() + { + return $this->tenant; + } + + public function getTenantId() + { + return $this->tenant_id; + } + + public function getIssuer() + { + return $this->replaceTenantId(parent::getIssuer()); + } + + private function getRemoteConfigUri() + { + return $this->replaceStr($this->basr_url, 'tenant', $this->tenant); + } + + private function replaceTenantId($str) + { + return $this->replaceStr($str, 'tenantid', $this->tenant_id); + } +} \ No newline at end of file diff --git a/src/AzureAd/AzureAdIdTokenJWT.php b/src/AzureAd/AzureAdIdTokenJWT.php new file mode 100644 index 00000000..e9b1f9a5 --- /dev/null +++ b/src/AzureAd/AzureAdIdTokenJWT.php @@ -0,0 +1,13 @@ +getConfiguration()->getClientId(); + } +} \ No newline at end of file diff --git a/src/Base/MicrosoftAccessTokenJWT.php b/src/Base/MicrosoftAccessTokenJWT.php new file mode 100644 index 00000000..d414fe42 --- /dev/null +++ b/src/Base/MicrosoftAccessTokenJWT.php @@ -0,0 +1,16 @@ +getConfiguration()->getAccessTokenIssuer(); + } + + protected function getAllowedAlgs() + { + return $this->getConfiguration()->getTokenEndpointAuthSigingAlgValuesSupported(); + } +} \ No newline at end of file diff --git a/src/Base/MicrosoftConfiguration.php b/src/Base/MicrosoftConfiguration.php new file mode 100644 index 00000000..fdc58678 --- /dev/null +++ b/src/Base/MicrosoftConfiguration.php @@ -0,0 +1,173 @@ +config_uri = $options['config_uri']; + $this->client_id = $options['client_id']; + $this->options = $options; + $this->_load(); + } + + public function getClientId() + { + return $this->client_id; + } + + public function getConfigUri() + { + return $this->config_uri; + } + + public function getJWKs() + { + return $this->jwks; + } + + public function getIdTokenSigingAlgValuesSupported() + { + return $this->id_token_signing_alg_values_supported; + } + + public function getTokenEndpointAuthSigingAlgValuesSupported() + { + return $this->token_endpoint_auth_signing_alg_values_supported; + } + + public function getIssuer() + { + return $this->issuer; + } + + public function getAccessTokenIssuer() + { + return $this->access_token_issuer; + } + + public function getAuthorizationEndpoint() + { + return $this->authorization_endpoint; + } + + public function getTokenEndpoint() + { + return $this->token_endpoint; + } + + public function getUserInfoEndpoint() + { + return $this->userinfo_endpoint; + } + + public function getDeviceAuthEndpoint() + { + return $this->device_authorization_endpoint; + } + + public function getEndSessionEndpoint() + { + return $this->end_session_endpoint; + } + + public function getLoadStatus() + { + $result = [ + 'status' => $this->loaded, + ]; + if (!$this->loaded) { + $result['error'] = $this->load_error; + } + + return $result; + } + + protected function replaceStr($str, $key, $value) + { + return str_replace('{' . $key . '}', $value, $str); + } + + private function _load() + { + try { + $this->loaded = false; + + $json = $this->getFromUrlOrFile($this->config_uri); + $data = json_decode($json, true); + + $this->authorization_endpoint = $data['authorization_endpoint']; + $this->token_endpoint = $data['token_endpoint']; + $this->userinfo_endpoint = $data['userinfo_endpoint']; + $this->device_authorization_endpoint = $data['device_authorization_endpoint']; + $this->end_session_endpoint = $data['end_session_endpoint']; + $this->jwks_uri = $data['jwks_uri']; + $this->issuer = $data['issuer']; + $this->access_token_issuer = (isset($data['access_token_issuer'])) ? $data['access_token_issuer'] : $this->issuer; + $this->id_token_signing_alg_values_supported = isset($data['id_token_signing_alg_values_supported']) ? $data['id_token_signing_alg_values_supported'] : $this->getDefaultSigningAlgValues(); + $this->token_endpoint_auth_signing_alg_values_supported = isset($data['token_endpoint_auth_signing_alg_values_supported']) ? $data['token_endpoint_auth_signing_alg_values_supported'] : $this->getDefaultSigningAlgValues(); + + $jwks_json = $this->getFromUrlOrFile($this->jwks_uri); + $jwks_data = json_decode($jwks_json, true); + + $this->jwks = JWK::parseKeySet($jwks_data); + + $this->loaded = true; + } catch (\Exception $e) { + $this->load_error = $e->getMessage(); + } + } + + private function getFromUrlOrFile($value) + { + $targetUri = $value; + if (filter_var($value, FILTER_VALIDATE_URL) === false) { + $targetUri = realpath($value) === false ? __DIR__ . $value : $value; + } + + $result = @file_get_contents($targetUri); + if ($result === false) { + throw new \Exception('Configuration not found'); + } + + return $result; + } +} \ No newline at end of file diff --git a/src/Base/MicrosoftIdTokenJWT.php b/src/Base/MicrosoftIdTokenJWT.php new file mode 100644 index 00000000..ec9352b5 --- /dev/null +++ b/src/Base/MicrosoftIdTokenJWT.php @@ -0,0 +1,21 @@ +getConfiguration()->getIssuer(); + } + + protected function getAllowedAlgs() + { + return $this->getConfiguration()->getIdTokenSigingAlgValuesSupported(); + } + + protected function getDefaultAudience() + { + return $this->getConfiguration()->getClientId(); + } +} \ No newline at end of file diff --git a/src/Base/MicrosoftJWT.php b/src/Base/MicrosoftJWT.php new file mode 100644 index 00000000..aabf32a9 --- /dev/null +++ b/src/Base/MicrosoftJWT.php @@ -0,0 +1,87 @@ +configuration = $configuration; + $this->audience = (!$audience) ? $this->getDefaultAudience() : $audience; + $this->decode($token, $allowed_algs); + } + + public function isExpired() + { + return JWT::isExpired($this->payload); + } + + public function getPayload() + { + return $this->payload; + } + + public function getJWT() + { + return $this->jwt; + } + + public function get($key) + { + return isset($this->getPayload()->{$key}) ? $this->getPayload()->{$key} : false; + } + + protected function decode($jwt, $allowed_algs) + { + $this->jwt = $jwt; + + $payload = JWT::decode( + $jwt, + $this->getConfiguration()->getJWKs(), + array_merge($this->getAllowedAlgs(), $allowed_algs) + ); + + $this->_validateIssuer($payload); + $this->_validateAudience($payload); + + $this->payload = $payload; + } + + protected function getConfiguration() + { + return $this->configuration; + } + + private function _validateIssuer($payload) + { + if (!isset($payload->iss)) { + throw new UnexpectedValueException('Missing issuer'); + } + if ($payload->iss !== $this->getIssuer()) { + throw new UnexpectedValueException('Invalid issuer: ' . $payload->iss); + } + } + + private function _validateAudience($payload) + { + if (!isset($payload->aud)) { + throw new UnexpectedValueException('Missing audience'); + } + if ($payload->aud !== $this->audience) { + throw new UnexpectedValueException('Invalid audience: ' . $payload->aud); + } + } +} \ No newline at end of file diff --git a/src/BeforeValidException.php b/src/BeforeValidException.php index fdf82bd9..6c262cd3 100644 --- a/src/BeforeValidException.php +++ b/src/BeforeValidException.php @@ -1,6 +1,7 @@ $v) { $kid = isset($v['kid']) ? $v['kid'] : $k; + $x5t = isset($v['x5t']) ? $v['x5t'] : $k; + if ($key = self::parseKey($v)) { $keys[$kid] = $key; + + if ($kid !== $x5t) { + $keys[$x5t] = $key; + } } } - + if (0 === \count($keys)) { throw new UnexpectedValueException('No supported algorithms found in JWK Set'); } - return $keys; } @@ -81,25 +88,26 @@ private static function parseKey(array $jwk) } switch ($jwk['kty']) { - case 'RSA': - if (\array_key_exists('d', $jwk)) { - throw new UnexpectedValueException('RSA private keys are not supported'); - } - if (!isset($jwk['n']) || !isset($jwk['e'])) { - throw new UnexpectedValueException('RSA keys must contain values for both "n" and "e"'); - } + case 'RSA': + if (\array_key_exists('d', $jwk)) { + throw new UnexpectedValueException('RSA private keys are not supported'); + } + if (!isset($jwk['n']) || !isset($jwk['e'])) { + throw new UnexpectedValueException('RSA keys must contain values for both "n" and "e"'); + } - $pem = self::createPemFromModulusAndExponent($jwk['n'], $jwk['e']); - $publicKey = \openssl_pkey_get_public($pem); - if (false === $publicKey) { - throw new DomainException( - 'OpenSSL error: ' . \openssl_error_string() - ); - } - return $publicKey; - default: - // Currently only RSA is supported - break; + $pem = self::createPemFromModulusAndExponent($jwk['n'], $jwk['e']); + $publicKey = \openssl_pkey_get_public($pem); + + if (false === $publicKey) { + throw new DomainException( + 'OpenSSL error: ' . \openssl_error_string() + ); + } + return $publicKey; + default: + // Currently only RSA is supported + break; } } @@ -146,7 +154,7 @@ private static function createPemFromModulusAndExponent($n, $e) $rsaPublicKey = "-----BEGIN PUBLIC KEY-----\r\n" . \chunk_split(\base64_encode($rsaPublicKey), 64) . '-----END PUBLIC KEY-----'; - + return $rsaPublicKey; } @@ -156,7 +164,7 @@ private static function createPemFromModulusAndExponent($n, $e) * DER supports lengths up to (2**8)**127, however, we'll only support lengths up to (2**8)**4. See * {@link http://itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf#p=13 X.690 paragraph 8.1.3} for more information. * - * @param int $length + * @param int $length * @return string */ private static function encodeLength($length) @@ -169,4 +177,4 @@ private static function encodeLength($length) return \pack('Ca*', 0x80 | \strlen($temp), $temp); } -} +} \ No newline at end of file diff --git a/src/JWT.php b/src/JWT.php index b167abd7..f8665133 100644 --- a/src/JWT.php +++ b/src/JWT.php @@ -1,6 +1,6 @@ kid)) { - if (!isset($key[$header->kid])) { - throw new UnexpectedValueException('"kid" invalid, unable to lookup correct key'); + // Added support for ADFS 2016 (Missing kid) + if (isset($header->kid) || isset($header->x5t)) { + if (isset($header->kid)) { + if (!isset($key[$header->kid])) { + throw new UnexpectedValueException('"kid" invalid, unable to lookup correct key'); + } + $key = $key[$header->kid]; + } + + if (isset($header->x5t) && (is_array($key) || $key instanceof \ArrayAccess)) { + if (!isset($key[$header->x5t])) { + throw new UnexpectedValueException('"x5t" invalid, unable to lookup correct key'); + } + $key = $key[$header->x5t]; } - $key = $key[$header->kid]; } else { - throw new UnexpectedValueException('"kid" empty, unable to lookup correct key'); + throw new UnexpectedValueException('"kid" && "x5t" empty, unable to lookup correct key'); } } - + // Check the signature if (!static::verify("$headb64.$bodyb64", $sig, $key, $header->alg)) { throw new SignatureInvalidException('Signature verification failed'); @@ -139,25 +153,35 @@ public static function decode($jwt, $key, array $allowed_algs = array()) 'Cannot handle token prior to ' . \date(DateTime::ISO8601, $payload->iat) ); } - + // Check if this token has expired. - if (isset($payload->exp) && ($timestamp - static::$leeway) >= $payload->exp) { + if (static::isExpired($payload, $timestamp)) { throw new ExpiredException('Expired token'); } + // Check if aud is from the app + return $payload; } /** * Converts and signs a PHP object or array into a JWT string. * - * @param object|array $payload PHP object or array - * @param string $key The secret key. - * If the algorithm used is asymmetric, this is the private key - * @param string $alg The signing algorithm. - * Supported algorithms are 'ES256', 'HS256', 'HS384', 'HS512', 'RS256', 'RS384', and 'RS512' - * @param mixed $keyId - * @param array $head An array with header elements to attach + * @param object|array $payload PHP object or array + * @param string $key The secret key. + * If the + * algorithm used + * is asymmetric, + * this is the + * private key + * @param string $alg The signing algorithm. + * Supported algorithms + * are 'ES256', 'HS256', + * 'HS384', 'HS512', + * 'RS256', 'RS384', and + * 'RS512' + * @param mixed $keyId + * @param array $head An array with header elements to attach * * @return string A signed JWT * @@ -187,10 +211,14 @@ public static function encode($payload, $key, $alg = 'HS256', $keyId = null, $he /** * Sign a string with a given key and algorithm. * - * @param string $msg The message to sign - * @param string|resource $key The secret key - * @param string $alg The signing algorithm. - * Supported algorithms are 'ES256', 'HS256', 'HS384', 'HS512', 'RS256', 'RS384', and 'RS512' + * @param string $msg The message to sign + * @param string|resource $key The secret key + * @param string $alg The signing algorithm. + * Supported algorithms + * are 'ES256', 'HS256', + * 'HS384', 'HS512', + * 'RS256', 'RS384', and + * 'RS512' * * @return string An encrypted message * @@ -203,19 +231,19 @@ public static function sign($msg, $key, $alg = 'HS256') } list($function, $algorithm) = static::$supported_algs[$alg]; switch ($function) { - case 'hash_hmac': - return \hash_hmac($algorithm, $msg, $key, true); - case 'openssl': - $signature = ''; - $success = \openssl_sign($msg, $signature, $key, $algorithm); - if (!$success) { - throw new DomainException("OpenSSL unable to sign data"); - } else { - if ($alg === 'ES256') { - $signature = self::signatureFromDER($signature, 256); - } - return $signature; + case 'hash_hmac': + return \hash_hmac($algorithm, $msg, $key, true); + case 'openssl': + $signature = ''; + $success = \openssl_sign($msg, $signature, $key, $algorithm); + if (!$success) { + throw new DomainException("OpenSSL unable to sign data"); + } else { + if ($alg === 'ES256') { + $signature = self::signatureFromDER($signature, 256); } + return $signature; + } } } @@ -223,10 +251,10 @@ public static function sign($msg, $key, $alg = 'HS256') * Verify a signature with the message, key and method. Not all methods * are symmetric, so we must have a separate verify and sign method. * - * @param string $msg The original message (header and body) - * @param string $signature The original signature - * @param string|resource $key For HS*, a string key works. for RS*, must be a resource of an openssl public key - * @param string $alg The algorithm + * @param string $msg The original message (header and body) + * @param string $signature The original signature + * @param string|resource $key For HS*, a string key works. for RS*, must be a resource of an openssl public key + * @param string $alg The algorithm * * @return bool * @@ -240,32 +268,32 @@ private static function verify($msg, $signature, $key, $alg) list($function, $algorithm) = static::$supported_algs[$alg]; switch ($function) { - case 'openssl': - $success = \openssl_verify($msg, $signature, $key, $algorithm); - if ($success === 1) { - return true; - } elseif ($success === 0) { - return false; - } - // returns 1 on success, 0 on failure, -1 on error. - throw new DomainException( - 'OpenSSL error: ' . \openssl_error_string() - ); - case 'hash_hmac': - default: - $hash = \hash_hmac($algorithm, $msg, $key, true); - if (\function_exists('hash_equals')) { - return \hash_equals($signature, $hash); - } - $len = \min(static::safeStrlen($signature), static::safeStrlen($hash)); + case 'openssl': + $success = \openssl_verify($msg, $signature, $key, $algorithm); + if ($success === 1) { + return true; + } elseif ($success === 0) { + return false; + } + // returns 1 on success, 0 on failure, -1 on error. + throw new DomainException( + 'OpenSSL error: ' . \openssl_error_string() + ); + case 'hash_hmac': + default: + $hash = \hash_hmac($algorithm, $msg, $key, true); + if (\function_exists('hash_equals')) { + return \hash_equals($signature, $hash); + } + $len = \min(static::safeStrlen($signature), static::safeStrlen($hash)); - $status = 0; - for ($i = 0; $i < $len; $i++) { - $status |= (\ord($signature[$i]) ^ \ord($hash[$i])); - } - $status |= (static::safeStrlen($signature) ^ static::safeStrlen($hash)); + $status = 0; + for ($i = 0; $i < $len; $i++) { + $status |= (\ord($signature[$i]) ^ \ord($hash[$i])); + } + $status |= (static::safeStrlen($signature) ^ static::safeStrlen($hash)); - return ($status === 0); + return ($status === 0); } } @@ -281,13 +309,15 @@ private static function verify($msg, $signature, $key, $alg) public static function jsonDecode($input) { if (\version_compare(PHP_VERSION, '5.4.0', '>=') && !(\defined('JSON_C_VERSION') && PHP_INT_SIZE > 4)) { - /** In PHP >=5.4.0, json_decode() accepts an options parameter, that allows you + /** + * In PHP >=5.4.0, json_decode() accepts an options parameter, that allows you * to specify that large ints (like Steam Transaction IDs) should be treated as * strings, rather than the PHP default behaviour of converting them to floats. */ $obj = \json_decode($input, false, 512, JSON_BIGINT_AS_STRING); } else { - /** Not all servers will support that, however, so for older versions we must + /** + * Not all servers will support that, however, so for older versions we must * manually detect large ints in the JSON string and quote them (thus converting *them to strings) before decoding, hence the preg_replace() call. */ @@ -353,6 +383,26 @@ public static function urlsafeB64Encode($input) return \str_replace('=', '', \strtr(\base64_encode($input), '+/', '-_')); } + /** + * Return whether the JWT is expired + * + * @param object|array $payload PHP object or array + * @param int $timestamp + * + * @return bool + */ + public static function isExpired($payload, $timestamp = null) + { + if (is_null($timestamp)) { + $timestamp = \is_null(static::$timestamp) ? \time() : static::$timestamp; + } + + if (isset($payload->exp) && ($timestamp - static::$leeway) >= $payload->exp) { + return true; + } + return false; + } + /** * Helper method to create a JSON error. * @@ -394,8 +444,8 @@ private static function safeStrlen($str) /** * Convert an ECDSA signature to an ASN.1 DER sequence * - * @param string $sig The ECDSA signature to convert - * @return string The encoded DER object + * @param string $sig The ECDSA signature to convert + * @return string The encoded DER object */ private static function signatureToDER($sig) { @@ -425,9 +475,9 @@ private static function signatureToDER($sig) /** * Encodes a value into a DER object. * - * @param int $type DER tag - * @param string $value the value to encode - * @return string the encoded object + * @param int $type DER tag + * @param string $value the value to encode + * @return string the encoded object */ private static function encodeDER($type, $value) { @@ -448,9 +498,9 @@ private static function encodeDER($type, $value) /** * Encodes signature from a DER object. * - * @param string $der binary signature in DER format - * @param int $keySize the number of bits in the key - * @return string the signature + * @param string $der binary signature in DER format + * @param int $keySize the number of bits in the key + * @return string the signature */ private static function signatureFromDER($der, $keySize) { @@ -474,9 +524,9 @@ private static function signatureFromDER($der, $keySize) /** * Reads binary DER-encoded data and decodes into a single object * - * @param string $der the binary data in DER format - * @param int $offset the offset of the data stream containing the object - * to decode + * @param string $der the binary data in DER format + * @param int $offset the offset of the data stream containing the object + * to decode * @return array [$offset, $data] the new offset and the decoded object */ private static function readDER($der, $offset = 0) @@ -510,4 +560,4 @@ private static function readDER($der, $offset = 0) return array($pos, $data); } -} +} \ No newline at end of file diff --git a/src/SignatureInvalidException.php b/src/SignatureInvalidException.php index 87cb34df..154d29fd 100644 --- a/src/SignatureInvalidException.php +++ b/src/SignatureInvalidException.php @@ -1,6 +1,7 @@ adfs_config = new AdfsConfiguration( + [ + 'client_id' => 'client-id', + 'config_uri' => __DIR__ . '/../metadata/adfs/configuration/configuration.json', + ] + ); + + $jwkSet = json_decode( + file_get_contents(__DIR__ . '/../metadata/adfs/configuration/jwks_uri.json'), + true + ); + $this->jwks = JWK::parseKeySet($jwkSet); + + $this->private_key = file_get_contents(__DIR__ . '/../metadata/adfs/configuration/private.pem'); + } + + public function testValidAccessToken() + { + $payload = [ + 'iss' => 'http://your_domain/adfs/services/trust', + 'aud' => 'urn:microsoft:userinfo', + 'exp' => time() + 10000, + ]; + $access_token = JWT::encode($payload, $this->private_key, 'RS256', '2lEZNsDIjsBPH94_b7-1z1IvnybfzOIz0hsBamzxCWc'); + $access_token_jwt = new AdfsAccessTokenJWT($this->adfs_config, $access_token); + + $this->assertFalse($access_token_jwt->isExpired()); + $this->assertEquals((object) $payload, $access_token_jwt->getPayload()); + $this->assertEquals($access_token, $access_token_jwt->getJWT()); + } + + public function testValidAccessTokenOtherAud() + { + $payload = [ + 'iss' => 'http://your_domain/adfs/services/trust', + 'aud' => 'client-id', + 'exp' => time() + 10000, + ]; + $access_token = JWT::encode($payload, $this->private_key, 'RS256', '2lEZNsDIjsBPH94_b7-1z1IvnybfzOIz0hsBamzxCWc'); + $access_token_jwt = new AdfsAccessTokenJWT($this->adfs_config, $access_token, 'client-id'); + + $this->assertFalse($access_token_jwt->isExpired()); + $this->assertEquals((object) $payload, $access_token_jwt->getPayload()); + $this->assertEquals($access_token, $access_token_jwt->getJWT()); + } + + public function testInValidAccessTokenMissingIssuer() + { + $this->setExpectedException( + 'UnexpectedValueException', + 'Missing issuer' + ); + + $payload = [ + 'aud' => 'client-id', + 'exp' => time() + 10000, + ]; + $access_token = JWT::encode($payload, $this->private_key, 'RS256', '2lEZNsDIjsBPH94_b7-1z1IvnybfzOIz0hsBamzxCWc'); + $access_token_jwt = new AdfsAccessTokenJWT($this->adfs_config, $access_token, 'client-id'); + } + + public function testInvalidAccessTokenInvalidIssuer() + { + $this->setExpectedException( + 'UnexpectedValueException', + 'Invalid issuer' + ); + $payload = [ + 'iss' => 'http://wrong_domain/adfs/services/trust', + 'aud' => 'client-id', + 'exp' => time() + 10000, + ]; + $access_token = JWT::encode($payload, $this->private_key, 'RS256', '2lEZNsDIjsBPH94_b7-1z1IvnybfzOIz0hsBamzxCWc'); + $access_token_jwt = new AdfsAccessTokenJWT($this->adfs_config, $access_token, 'client-id'); + } + + public function testValidAccessTokenMissingAudience() + { + $this->setExpectedException( + 'UnexpectedValueException', + 'Missing audience' + ); + + $payload = [ + 'iss' => 'http://your_domain/adfs/services/trust', + 'exp' => time() + 10000, + ]; + $access_token = JWT::encode($payload, $this->private_key, 'RS256', '2lEZNsDIjsBPH94_b7-1z1IvnybfzOIz0hsBamzxCWc'); + $access_token_jwt = new AdfsAccessTokenJWT($this->adfs_config, $access_token, 'client-id'); + + $this->assertFalse($access_token_jwt->isExpired()); + $this->assertEquals((object) $payload, $access_token_jwt->getPayload()); + $this->assertEquals($access_token, $access_token_jwt->getJWT()); + } + + public function testValidAccessTokenInvalidAudience() + { + $this->setExpectedException( + 'UnexpectedValueException', + 'Invalid audience' + ); + + $payload = [ + 'iss' => 'http://your_domain/adfs/services/trust', + 'aud' => 'wrong-client-id', + 'exp' => time() + 10000, + ]; + $access_token = JWT::encode($payload, $this->private_key, 'RS256', '2lEZNsDIjsBPH94_b7-1z1IvnybfzOIz0hsBamzxCWc'); + $access_token_jwt = new AdfsAccessTokenJWT($this->adfs_config, $access_token, 'client-id'); + + $this->assertFalse($access_token_jwt->isExpired()); + $this->assertEquals((object) $payload, $access_token_jwt->getPayload()); + $this->assertEquals($access_token, $access_token_jwt->getJWT()); + } + + public function setExpectedException($exceptionName, $message = '', $code = null) + { + if (method_exists($this, 'expectException')) { + $this->expectException($exceptionName); + if (!empty($message)) { + $this->expectExceptionMessage($message); + } + } else { + parent::setExpectedException($exceptionName, $message, $code); + } + } +} \ No newline at end of file diff --git a/tests/Adfs/AdfsConfigurationTest.php b/tests/Adfs/AdfsConfigurationTest.php new file mode 100644 index 00000000..a138641a --- /dev/null +++ b/tests/Adfs/AdfsConfigurationTest.php @@ -0,0 +1,126 @@ +setExpectedException( + 'UnexpectedValueException', + 'Missing hostname' + ); + + $config = new AdfsConfiguration([]); + } + + public function testMissingConfigUriOptions() + { + $this->setExpectedException( + 'UnexpectedValueException', + 'Missing config_uri' + ); + + $config = new AdfsConfiguration( + [ + 'hostname' => 'some_hostname.com', + ] + ); + } + + public function testMissingCliendIdOptions() + { + $this->setExpectedException( + 'UnexpectedValueException', + 'Missing client_id' + ); + + $config = new AdfsConfiguration( + [ + 'hostname' => 'some_hostname.com', + 'config_uri' => __DIR__ . '/../metadata/adfs/configuration/configuration.json', + ] + ); + } + + public function testIfHostnameGivenOptions() + { + $config = new AdfsConfiguration( + [ + 'hostname' => 'some_hostname.com', + 'client_id' => 'client-id', + ] + ); + + $this->assertEquals($config->getConfigUri(), 'https://some_hostname.com/adfs/.well-known/openid-configuration'); + } + + public function testIfConfigUrisGivenOptions() + { + $config = new AdfsConfiguration( + [ + 'client_id' => 'client-id', + 'config_uri' => __DIR__ . '/../metadata/adfs/configuration/configuration.json', + ] + ); + + $this->assertEquals($config->getConfigUri(), __DIR__ . '/../metadata/adfs/configuration/configuration.json'); + } + + public function testInvalodConfigUri() + { + $config = new AdfsConfiguration( + [ + 'client_id' => 'client-id', + 'config_uri' => 'http://127.0.0.1/not_exists', + ] + ); + + $this->assertEquals($config->getLoadStatus(), [ + 'status' => false, + 'error' => 'Configuration not found', + ]); + } + + public function testConstructor() + { + $config = new AdfsConfiguration( + [ + 'client_id' => 'client-id', + 'config_uri' => __DIR__ . '/../metadata/adfs/configuration/configuration.json', + ] + ); + + $this->assertEquals($config->getLoadStatus(), [ + 'status' => true, + ]); + + $this->assertEquals($config->getClientId(), 'client-id'); + + $this->assertArrayHasKey('2lEZNsDIjsBPH94_b7-1z1IvnybfzOIz0hsBamzxCWc', $config->getJWKs()); + + $this->assertEquals($config->getIdTokenSigingAlgValuesSupported(), ['RS256']); + $this->assertEquals($config->getTokenEndpointAuthSigingAlgValuesSupported(), ['RS256']); + + $this->assertEquals($config->getIssuer(), 'https://your_domain/adfs'); + $this->assertEquals($config->getAccessTokenIssuer(), 'http://your_domain/adfs/services/trust'); + + $this->assertEquals($config->getAuthorizationEndpoint(), 'https://your_domain/adfs/oauth2/authorize/'); + $this->assertEquals($config->getTokenEndpoint(), 'https://your_domain/adfs/oauth2/token/'); + $this->assertEquals($config->getUserInfoEndpoint(), 'https://your_domain/adfs/userinfo'); + $this->assertEquals($config->getDeviceAuthEndpoint(), 'https://your_domain/adfs/oauth2/devicecode'); + $this->assertEquals($config->getEndSessionEndpoint(), 'https://your_domain/adfs/oauth2/logout'); + } + + private function setExpectedException($exceptionName, $message = '', $code = null) + { + if (method_exists($this, 'expectException')) { + $this->expectException($exceptionName); + } else { + parent::setExpectedException($exceptionName, $message, $code); + } + } +} \ No newline at end of file diff --git a/tests/Adfs/AdfsIdTokenJWTTest.php b/tests/Adfs/AdfsIdTokenJWTTest.php new file mode 100644 index 00000000..2ea83f22 --- /dev/null +++ b/tests/Adfs/AdfsIdTokenJWTTest.php @@ -0,0 +1,147 @@ +adfs_config = new AdfsConfiguration( + [ + 'client_id' => 'client-id', + 'config_uri' => __DIR__ . '/../metadata/adfs/configuration/configuration.json', + ] + ); + + $jwkSet = json_decode( + file_get_contents(__DIR__ . '/../metadata/adfs/configuration/jwks_uri.json'), + true + ); + $this->jwks = JWK::parseKeySet($jwkSet); + + $this->private_key = file_get_contents(__DIR__ . '/../metadata/adfs/configuration/private.pem'); + } + + public function testValidIdToken() + { + $payload = [ + 'iss' => 'https://your_domain/adfs', + 'aud' => 'client-id', + 'exp' => time()+10000, + 'unique_name' => 'tester123', + ]; + $id_token = JWT::encode($payload, $this->private_key, 'RS256', '2lEZNsDIjsBPH94_b7-1z1IvnybfzOIz0hsBamzxCWc'); + $id_token_jwt = new AdfsIdTokenJWT($this->adfs_config, $id_token); + + $this->assertFalse($id_token_jwt->isExpired()); + $this->assertEquals((object) $payload, $id_token_jwt->getPayload()); + $this->assertEquals($id_token, $id_token_jwt->getJWT()); + $this->assertEquals('tester123', $id_token_jwt->get('unique_name')); + } + + public function testValidIdTokenOtherAuth() + { + $payload = [ + 'iss' => 'https://your_domain/adfs', + 'aud' => 'other-client-id', + 'exp' => time()+10000, + 'unique_name' => 'tester123', + ]; + $id_token = JWT::encode($payload, $this->private_key, 'RS256', '2lEZNsDIjsBPH94_b7-1z1IvnybfzOIz0hsBamzxCWc'); + $id_token_jwt = new AdfsIdTokenJWT($this->adfs_config, $id_token, 'other-client-id'); + + $this->assertFalse($id_token_jwt->isExpired()); + $this->assertEquals((object) $payload, $id_token_jwt->getPayload()); + $this->assertEquals($id_token, $id_token_jwt->getJWT()); + $this->assertEquals('tester123', $id_token_jwt->get('unique_name')); + } + + public function testInvalidIdTokenMissingIssuer() + { + $this->setExpectedException( + 'UnexpectedValueException', + 'Missing issuer' + ); + + $payload = [ + 'aud' => 'client-id', + 'exp' => time()+10000, + 'unique_name' => 'tester123', + ]; + $id_token = JWT::encode($payload, $this->private_key, 'RS256', '2lEZNsDIjsBPH94_b7-1z1IvnybfzOIz0hsBamzxCWc'); + $id_token_jwt = new AdfsIdTokenJWT($this->adfs_config, $id_token); + } + + public function testInvalidIdTokenInvalidIssuer() + { + $this->setExpectedException( + 'UnexpectedValueException', + 'Invalid issuer' + ); + + $payload = [ + 'iss' => 'https://wrong_domain/adfs', + 'aud' => 'client-id', + 'exp' => time()+10000, + 'unique_name' => 'tester123', + ]; + $id_token = JWT::encode($payload, $this->private_key, 'RS256', '2lEZNsDIjsBPH94_b7-1z1IvnybfzOIz0hsBamzxCWc'); + $id_token_jwt = new AdfsIdTokenJWT($this->adfs_config, $id_token); + } + + public function testInvalidIdTokenMissingAudience() + { + $this->setExpectedException( + 'UnexpectedValueException', + 'Missing audience' + ); + + $payload = [ + 'iss' => 'https://your_domain/adfs', + 'exp' => time()+10000, + 'unique_name' => 'tester123', + ]; + $id_token = JWT::encode($payload, $this->private_key, 'RS256', '2lEZNsDIjsBPH94_b7-1z1IvnybfzOIz0hsBamzxCWc'); + $id_token_jwt = new AdfsIdTokenJWT($this->adfs_config, $id_token); + } + + public function testInvalidIdTokenInvalidAudience() + { + $this->setExpectedException( + 'UnexpectedValueException', + 'Invalid audience' + ); + + $payload = [ + 'iss' => 'https://your_domain/adfs', + 'aud' => 'wrong-client-id', + 'exp' => time()+10000, + 'unique_name' => 'tester123', + ]; + $id_token = JWT::encode($payload, $this->private_key, 'RS256', '2lEZNsDIjsBPH94_b7-1z1IvnybfzOIz0hsBamzxCWc'); + $id_token_jwt = new AdfsIdTokenJWT($this->adfs_config, $id_token); + } + + public function setExpectedException($exceptionName, $message = '', $code = null) + { + if (method_exists($this, 'expectException')) { + $this->expectException($exceptionName); + if (!empty($message)) { + $this->expectExceptionMessage($message); + } + } else { + parent::setExpectedException($exceptionName, $message, $code); + } + } +} \ No newline at end of file diff --git a/tests/AzureAd/AzureAdAccessTokenJWTTest.php b/tests/AzureAd/AzureAdAccessTokenJWTTest.php new file mode 100644 index 00000000..a1fd22a6 --- /dev/null +++ b/tests/AzureAd/AzureAdAccessTokenJWTTest.php @@ -0,0 +1,148 @@ +azure_ad_config = new AzureAdConfiguration( + [ + 'tenant' => 'iv9puejd-qmJ1-AL2i-j3TP-wrb7qjjvxttz', + 'tenant_id' => 'iv9puejd-qmJ1-AL2i-j3TP-wrb7qjjvxttz', + 'client_id' => 'client-id', + 'config_uri' => __DIR__ . '/../metadata/azure_ad/configuration/configuration.json', + ] + ); + + $jwkSet = json_decode( + file_get_contents(__DIR__ . '/../metadata/azure_ad/configuration/jwks_uri.json'), + true + ); + $this->jwks = JWK::parseKeySet($jwkSet); + + $this->private_key = file_get_contents(__DIR__ . '/../metadata/azure_ad/configuration/private.pem'); + } + + public function testValidAccessToken() + { + $payload = [ + 'iss' => 'https://login.microsoftonline.com/iv9puejd-qmJ1-AL2i-j3TP-wrb7qjjvxttz/v2.0', + 'aud' => '00000003-0000-0000-c000-000000000000', + 'exp' => time() + 10000, + ]; + $access_token = JWT::encode($payload, $this->private_key, 'RS256', '2lEZNsDIjsBPH94_b7-1z1IvnybfzOIz0hsBamzxCWc'); + $access_token_jwt = new AzureAdAccessTokenJWT($this->azure_ad_config, $access_token); + + $this->assertFalse($access_token_jwt->isExpired()); + $this->assertEquals((object) $payload, $access_token_jwt->getPayload()); + $this->assertEquals($access_token, $access_token_jwt->getJWT()); + } + + public function testValidAccessTokenOtherAud() + { + $payload = [ + 'iss' => 'https://login.microsoftonline.com/iv9puejd-qmJ1-AL2i-j3TP-wrb7qjjvxttz/v2.0', + 'aud' => 'client-id', + 'exp' => time() + 10000, + ]; + $access_token = JWT::encode($payload, $this->private_key, 'RS256', '2lEZNsDIjsBPH94_b7-1z1IvnybfzOIz0hsBamzxCWc'); + $access_token_jwt = new AzureAdAccessTokenJWT($this->azure_ad_config, $access_token, 'client-id'); + + $this->assertFalse($access_token_jwt->isExpired()); + $this->assertEquals((object) $payload, $access_token_jwt->getPayload()); + $this->assertEquals($access_token, $access_token_jwt->getJWT()); + } + + public function testInValidAccessTokenMissingIssuer() + { + $this->setExpectedException( + 'UnexpectedValueException', + 'Missing issuer' + ); + + $payload = [ + 'aud' => 'client-id', + 'exp' => time() + 10000, + ]; + $access_token = JWT::encode($payload, $this->private_key, 'RS256', '2lEZNsDIjsBPH94_b7-1z1IvnybfzOIz0hsBamzxCWc'); + $access_token_jwt = new AzureAdAccessTokenJWT($this->azure_ad_config, $access_token, 'client-id'); + } + + public function testInvalidAccessTokenInvalidIssuer() + { + $this->setExpectedException( + 'UnexpectedValueException', + 'Invalid issuer' + ); + $payload = [ + 'iss' => 'https://login.microsoftonline.com/wrong_id/v2.0', + 'aud' => 'client-id', + 'exp' => time() + 10000, + ]; + $access_token = JWT::encode($payload, $this->private_key, 'RS256', '2lEZNsDIjsBPH94_b7-1z1IvnybfzOIz0hsBamzxCWc'); + $access_token_jwt = new AzureAdAccessTokenJWT($this->azure_ad_config, $access_token, 'client-id'); + } + + public function testValidAccessTokenMissingAudience() + { + $this->setExpectedException( + 'UnexpectedValueException', + 'Missing audience' + ); + + $payload = [ + 'iss' => 'https://login.microsoftonline.com/iv9puejd-qmJ1-AL2i-j3TP-wrb7qjjvxttz/v2.0', + 'exp' => time() + 10000, + ]; + $access_token = JWT::encode($payload, $this->private_key, 'RS256', '2lEZNsDIjsBPH94_b7-1z1IvnybfzOIz0hsBamzxCWc'); + $access_token_jwt = new AzureAdAccessTokenJWT($this->azure_ad_config, $access_token, 'client-id'); + + $this->assertFalse($access_token_jwt->isExpired()); + $this->assertEquals((object) $payload, $access_token_jwt->getPayload()); + $this->assertEquals($access_token, $access_token_jwt->getJWT()); + } + + public function testValidAccessTokenInvalidAudience() + { + $this->setExpectedException( + 'UnexpectedValueException', + 'Invalid audience' + ); + + $payload = [ + 'iss' => 'https://login.microsoftonline.com/iv9puejd-qmJ1-AL2i-j3TP-wrb7qjjvxttz/v2.0', + 'aud' => 'wrong-client-id', + 'exp' => time() + 10000, + ]; + $access_token = JWT::encode($payload, $this->private_key, 'RS256', '2lEZNsDIjsBPH94_b7-1z1IvnybfzOIz0hsBamzxCWc'); + $access_token_jwt = new AzureAdAccessTokenJWT($this->azure_ad_config, $access_token, 'client-id'); + + $this->assertFalse($access_token_jwt->isExpired()); + $this->assertEquals((object) $payload, $access_token_jwt->getPayload()); + $this->assertEquals($access_token, $access_token_jwt->getJWT()); + } + + public function setExpectedException($exceptionName, $message = '', $code = null) + { + if (method_exists($this, 'expectException')) { + $this->expectException($exceptionName); + if (!empty($message)) { + $this->expectExceptionMessage($message); + } + } else { + parent::setExpectedException($exceptionName, $message, $code); + } + } +} \ No newline at end of file diff --git a/tests/AzureAd/AzureAdConfigurationTest.php b/tests/AzureAd/AzureAdConfigurationTest.php new file mode 100644 index 00000000..9c683a04 --- /dev/null +++ b/tests/AzureAd/AzureAdConfigurationTest.php @@ -0,0 +1,139 @@ +setExpectedException( + 'UnexpectedValueException', + 'Missing tenant' + ); + + $config = new AzureAdConfiguration([]); + } + + public function testMissingTenantIdOptions() + { + $this->setExpectedException( + 'UnexpectedValueException', + 'Missing tenant_id' + ); + + $config = new AzureAdConfiguration( + [ + 'tenant' => 'iv9puejd-qmJ1-AL2i-j3TP-wrb7qjjvxttz', + ] + ); + } + + public function testMissingCliendIdOptions() + { + $this->setExpectedException( + 'UnexpectedValueException', + 'Missing client_id' + ); + + $config = new AzureAdConfiguration( + [ + 'tenant' => 'iv9puejd-qmJ1-AL2i-j3TP-wrb7qjjvxttz', + 'tenant_id' => 'iv9puejd-qmJ1-AL2i-j3TP-wrb7qjjvxttz', + ] + ); + } + + public function testIfConfigUrisGivenOptions() + { + $config = new AzureAdConfiguration( + [ + 'tenant' => 'iv9puejd-qmJ1-AL2i-j3TP-wrb7qjjvxttz', + 'tenant_id' => 'iv9puejd-qmJ1-AL2i-j3TP-wrb7qjjvxttz', + 'client_id' => 'client-id', + 'config_uri' => __DIR__ . '/../metadata/azure_ad/configuration/configuration.json', + ] + ); + + $this->assertEquals($config->getConfigUri(), __DIR__ . '/../metadata/azure_ad/configuration/configuration.json'); + } + + public function testIfConfigUrisNotGivenOptions() + { + $config = new AzureAdConfiguration( + [ + 'tenant' => 'iv9puejd-qmJ1-AL2i-j3TP-wrb7qjjvxttz', + 'tenant_id' => 'iv9puejd-qmJ1-AL2i-j3TP-wrb7qjjvxttz', + 'client_id' => 'client-id', + ] + ); + + $this->assertEquals($config->getConfigUri(), 'https://login.microsoftonline.com/iv9puejd-qmJ1-AL2i-j3TP-wrb7qjjvxttz/v2.0/.well-known/openid-configuration'); + } + + public function testInvalodConfigUri() + { + $config = new AzureAdConfiguration( + [ + 'tenant' => 'iv9puejd-qmJ1-AL2i-j3TP-wrb7qjjvxttz', + 'tenant_id' => 'iv9puejd-qmJ1-AL2i-j3TP-wrb7qjjvxttz', + 'client_id' => 'client-id', + 'config_uri' => 'http://127.0.0.1/not_exists', + ] + ); + + $this->assertEquals($config->getLoadStatus(), [ + 'status' => false, + 'error' => 'Configuration not found', + ]); + } + + public function testConstructor() + { + $config = new AzureAdConfiguration( + [ + 'tenant' => 'iv9puejd-qmJ1-AL2i-j3TP-wrb7qjjvxttz', + 'tenant_id' => 'iv9puejd-qmJ1-AL2i-j3TP-wrb7qjjvxttz', + 'client_id' => 'client-id', + 'config_uri' => __DIR__ . '/../metadata/azure_ad/configuration/configuration.json', + ] + ); + + $this->assertEquals($config->getLoadStatus(), [ + 'status' => true, + ]); + + $this->assertEquals($config->getTenant(), 'iv9puejd-qmJ1-AL2i-j3TP-wrb7qjjvxttz'); + $this->assertEquals($config->getTenantId(), 'iv9puejd-qmJ1-AL2i-j3TP-wrb7qjjvxttz'); + + $this->assertEquals($config->getClientId(), 'client-id'); + + $this->assertArrayHasKey('2lEZNsDIjsBPH94_b7-1z1IvnybfzOIz0hsBamzxCWc', $config->getJWKs()); + + $this->assertEquals($config->getIdTokenSigingAlgValuesSupported(), ['RS256']); + $this->assertEquals($config->getTokenEndpointAuthSigingAlgValuesSupported(), ['RS256']); + + $this->assertEquals($config->getIssuer(), 'https://login.microsoftonline.com/iv9puejd-qmJ1-AL2i-j3TP-wrb7qjjvxttz/v2.0'); + $this->assertEquals($config->getAccessTokenIssuer(), 'https://login.microsoftonline.com/iv9puejd-qmJ1-AL2i-j3TP-wrb7qjjvxttz/v2.0'); + + $this->assertEquals($config->getAuthorizationEndpoint(), 'https://login.microsoftonline.com/iv9puejd-qmJ1-AL2i-j3TP-wrb7qjjvxttz/oauth2/v2.0/authorize'); + $this->assertEquals($config->getTokenEndpoint(), 'https://login.microsoftonline.com/iv9puejd-qmJ1-AL2i-j3TP-wrb7qjjvxttz/oauth2/v2.0/token'); + $this->assertEquals($config->getUserInfoEndpoint(), 'https://graph.microsoft.com/oidc/userinfo'); + $this->assertEquals($config->getDeviceAuthEndpoint(), 'https://login.microsoftonline.com/iv9puejd-qmJ1-AL2i-j3TP-wrb7qjjvxttz/oauth2/v2.0/devicecode'); + $this->assertEquals($config->getEndSessionEndpoint(), 'https://login.microsoftonline.com/iv9puejd-qmJ1-AL2i-j3TP-wrb7qjjvxttz/oauth2/v2.0/logout'); + } + + private function setExpectedException($exceptionName, $message = '', $code = null) + { + if (method_exists($this, 'expectException')) { + $this->expectException($exceptionName); + if (!empty($message)) { + $this->expectExceptionMessage($message); + } + } else { + parent::setExpectedException($exceptionName, $message, $code); + } + } +} \ No newline at end of file diff --git a/tests/AzureAd/AzureAdIdTokenJWTTest.php b/tests/AzureAd/AzureAdIdTokenJWTTest.php new file mode 100644 index 00000000..7c8a6fd6 --- /dev/null +++ b/tests/AzureAd/AzureAdIdTokenJWTTest.php @@ -0,0 +1,149 @@ +azure_ad_config = new AzureAdConfiguration( + [ + 'tenant' => 'iv9puejd-qmJ1-AL2i-j3TP-wrb7qjjvxttz', + 'tenant_id' => 'iv9puejd-qmJ1-AL2i-j3TP-wrb7qjjvxttz', + 'client_id' => 'client-id', + 'config_uri' => __DIR__ . '/../metadata/azure_ad/configuration/configuration.json', + ] + ); + + $jwkSet = json_decode( + file_get_contents(__DIR__ . '/../metadata/azure_ad/configuration/jwks_uri.json'), + true + ); + $this->jwks = JWK::parseKeySet($jwkSet); + + $this->private_key = file_get_contents(__DIR__ . '/../metadata/azure_ad/configuration/private.pem'); + } + + public function testValidIdToken() + { + $payload = [ + 'iss' => 'https://login.microsoftonline.com/iv9puejd-qmJ1-AL2i-j3TP-wrb7qjjvxttz/v2.0', + 'aud' => 'client-id', + 'exp' => time() + 10000, + 'unique_name' => 'tester123', + ]; + $id_token = JWT::encode($payload, $this->private_key, 'RS256', '2lEZNsDIjsBPH94_b7-1z1IvnybfzOIz0hsBamzxCWc'); + $id_token_jwt = new AzureAdIdTokenJWT($this->azure_ad_config, $id_token); + + $this->assertFalse($id_token_jwt->isExpired()); + $this->assertEquals((object) $payload, $id_token_jwt->getPayload()); + $this->assertEquals($id_token, $id_token_jwt->getJWT()); + $this->assertEquals('tester123', $id_token_jwt->get('unique_name')); + } + + public function testValidIdTokenOtherAuth() + { + $payload = [ + 'iss' => 'https://login.microsoftonline.com/iv9puejd-qmJ1-AL2i-j3TP-wrb7qjjvxttz/v2.0', + 'aud' => 'other-client-id', + 'exp' => time() + 10000, + 'unique_name' => 'tester123', + ]; + $id_token = JWT::encode($payload, $this->private_key, 'RS256', '2lEZNsDIjsBPH94_b7-1z1IvnybfzOIz0hsBamzxCWc'); + $id_token_jwt = new AzureAdIdTokenJWT($this->azure_ad_config, $id_token, 'other-client-id'); + + $this->assertFalse($id_token_jwt->isExpired()); + $this->assertEquals((object) $payload, $id_token_jwt->getPayload()); + $this->assertEquals($id_token, $id_token_jwt->getJWT()); + $this->assertEquals('tester123', $id_token_jwt->get('unique_name')); + } + + public function testInvalidIdTokenMissingIssuer() + { + $this->setExpectedException( + 'UnexpectedValueException', + 'Missing issuer' + ); + + $payload = [ + 'aud' => 'client-id', + 'exp' => time() + 10000, + 'unique_name' => 'tester123', + ]; + $id_token = JWT::encode($payload, $this->private_key, 'RS256', '2lEZNsDIjsBPH94_b7-1z1IvnybfzOIz0hsBamzxCWc'); + $id_token_jwt = new AzureAdIdTokenJWT($this->azure_ad_config, $id_token); + } + + public function testInvalidIdTokenInvalidIssuer() + { + $this->setExpectedException( + 'UnexpectedValueException', + 'Invalid issuer' + ); + + $payload = [ + 'iss' => 'https://login.microsoftonline.com/wrong_id/v2.0', + 'aud' => 'client-id', + 'exp' => time() + 10000, + 'unique_name' => 'tester123', + ]; + $id_token = JWT::encode($payload, $this->private_key, 'RS256', '2lEZNsDIjsBPH94_b7-1z1IvnybfzOIz0hsBamzxCWc'); + $id_token_jwt = new AzureAdIdTokenJWT($this->azure_ad_config, $id_token); + } + + public function testInvalidIdTokenMissingAudience() + { + $this->setExpectedException( + 'UnexpectedValueException', + 'Missing audience' + ); + + $payload = [ + 'iss' => 'https://login.microsoftonline.com/iv9puejd-qmJ1-AL2i-j3TP-wrb7qjjvxttz/v2.0', + 'exp' => time() + 10000, + 'unique_name' => 'tester123', + ]; + $id_token = JWT::encode($payload, $this->private_key, 'RS256', '2lEZNsDIjsBPH94_b7-1z1IvnybfzOIz0hsBamzxCWc'); + $id_token_jwt = new AzureAdIdTokenJWT($this->azure_ad_config, $id_token); + } + + public function testInvalidIdTokenInvalidAudience() + { + $this->setExpectedException( + 'UnexpectedValueException', + 'Invalid audience' + ); + + $payload = [ + 'iss' => 'https://login.microsoftonline.com/iv9puejd-qmJ1-AL2i-j3TP-wrb7qjjvxttz/v2.0', + 'aud' => 'wrong-client-id', + 'exp' => time() + 10000, + 'unique_name' => 'tester123', + ]; + $id_token = JWT::encode($payload, $this->private_key, 'RS256', '2lEZNsDIjsBPH94_b7-1z1IvnybfzOIz0hsBamzxCWc'); + $id_token_jwt = new AzureAdIdTokenJWT($this->azure_ad_config, $id_token); + } + + public function setExpectedException($exceptionName, $message = '', $code = null) + { + if (method_exists($this, 'expectException')) { + $this->expectException($exceptionName); + if (!empty($message)) { + $this->expectExceptionMessage($message); + } + } else { + parent::setExpectedException($exceptionName, $message, $code); + } + } +} \ No newline at end of file diff --git a/tests/JWKExtrasTest.php b/tests/JWKExtrasTest.php new file mode 100644 index 00000000..32d5cd51 --- /dev/null +++ b/tests/JWKExtrasTest.php @@ -0,0 +1,89 @@ +setExpectedException( + 'UnexpectedValueException', + '"keys" member must exist in the JWK Set' + ); + + $keys = JWK::parseKeySet([]); + } + + public function testParseKeySetEmptyKeys() + { + $this->setExpectedException( + 'InvalidArgumentException', + 'JWK Set did not contain any keys' + ); + + $keys = JWK::parseKeySet(['keys' => []]); + } + + public function testParseKeyMissingKeys() + { + $this->setExpectedException( + 'InvalidArgumentException', + 'JWK must not be empty' + ); + + $keys = $this->invokeMethod(new JWK, 'parseKey', ['jwk' => []]); + } + + public function testParseKeyPrivateKey() + { + $this->setExpectedException( + 'UnexpectedValueException', + 'RSA private keys are not supported' + ); + + $jwk = json_decode( + file_get_contents(__DIR__ . '/metadata/jwk/rsa-public-private.json'), + true + ); + + $keys = $this->invokeMethod(new JWK, 'parseKey', ['jwk' => $jwk]); + } + + public function testParseKeyKeyMissingNorE() + { + $this->setExpectedException( + 'UnexpectedValueException', + 'RSA keys must contain values for both "n" and "e"' + ); + + $jwk = json_decode( + file_get_contents(__DIR__ . '/metadata/jwk/rsa-public-no-n.json'), + true + ); + + $keys = $this->invokeMethod(new JWK, 'parseKey', ['jwk' => $jwk]); + + $this->setExpectedException( + 'UnexpectedValueException', + 'RSA keys must contain values for both "n" and "e"' + ); + + $jwk = json_decode( + file_get_contents(__DIR__ . '/metadata/jwk/rsa-public-no-e.json'), + true + ); + + $keys = $this->invokeMethod(new JWK, 'parseKey', ['jwk' => $jwk]); + } + + private function invokeMethod($object, $methodName, array $parameters = []) + { + $reflection = new \ReflectionClass(get_class($object)); + $method = $reflection->getMethod($methodName); + $method->setAccessible(true); + + return $method->invokeArgs($object, $parameters); + } +} \ No newline at end of file diff --git a/tests/JWKTest.php b/tests/JWKTest.php index b8b67540..100b3f0f 100644 --- a/tests/JWKTest.php +++ b/tests/JWKTest.php @@ -1,8 +1,12 @@ 'foo'); - $keys = JWK::parseKeySet(array('keys' => array($badJwk))); + $badJwk = ['kid' => 'foo']; + $keys = JWK::parseKeySet(['keys' => [$badJwk]]); } public function testInvalidAlgorithm() @@ -27,8 +31,8 @@ public function testInvalidAlgorithm() 'No supported algorithms found in JWK Set' ); - $badJwk = array('kty' => 'BADALG'); - $keys = JWK::parseKeySet(array('keys' => array($badJwk))); + $badJwk = ['kty' => 'BADALG']; + $keys = JWK::parseKeySet(['keys' => [$badJwk]]); } public function testParseJwkKeySet() @@ -63,12 +67,12 @@ public function testParseJwkKeySet_empty() public function testDecodeByJwkKeySetTokenExpired() { $privKey1 = file_get_contents(__DIR__ . '/rsa1-private.pem'); - $payload = array('exp' => strtotime('-1 hour')); + $payload = ['exp' => strtotime('-1 hour')]; $msg = JWT::encode($payload, $privKey1, 'RS256', 'jwk1'); - $this->setExpectedException('Firebase\JWT\ExpiredException'); + $this->setExpectedException('Alancting\Microsoft\JWT\ExpiredException'); - JWT::decode($msg, self::$keys, array('RS256')); + JWT::decode($msg, self::$keys, ['RS256']); } /** @@ -77,12 +81,12 @@ public function testDecodeByJwkKeySetTokenExpired() public function testDecodeByJwkKeySet() { $privKey1 = file_get_contents(__DIR__ . '/rsa1-private.pem'); - $payload = array('sub' => 'foo', 'exp' => strtotime('+10 seconds')); + $payload = ['sub' => 'foo', 'exp' => strtotime('+10 seconds')]; $msg = JWT::encode($payload, $privKey1, 'RS256', 'jwk1'); - $result = JWT::decode($msg, self::$keys, array('RS256')); + $result = JWT::decode($msg, self::$keys, ['RS256']); - $this->assertEquals("foo", $result->sub); + $this->assertEquals('foo', $result->sub); } /** @@ -91,12 +95,12 @@ public function testDecodeByJwkKeySet() public function testDecodeByMultiJwkKeySet() { $privKey2 = file_get_contents(__DIR__ . '/rsa2-private.pem'); - $payload = array('sub' => 'bar', 'exp' => strtotime('+10 seconds')); + $payload = ['sub' => 'bar', 'exp' => strtotime('+10 seconds')]; $msg = JWT::encode($payload, $privKey2, 'RS256', 'jwk2'); - $result = JWT::decode($msg, self::$keys, array('RS256')); + $result = JWT::decode($msg, self::$keys, ['RS256']); - $this->assertEquals("bar", $result->sub); + $this->assertEquals('bar', $result->sub); } /* @@ -113,4 +117,4 @@ public function setExpectedException($exceptionName, $message = '', $code = null parent::setExpectedException($exceptionName, $message, $code); } } -} +} \ No newline at end of file diff --git a/tests/JWTExtrasTest.php b/tests/JWTExtrasTest.php new file mode 100644 index 00000000..3a6ba371 --- /dev/null +++ b/tests/JWTExtrasTest.php @@ -0,0 +1,122 @@ +setExpectedException( + 'UnexpectedValueException', + 'Empty algorithm' + ); + JWT::decode($jwt, 'my_key', ['HS256']); + } + + public function testDecodeInvalidHeaderUnsupportedAlg() + { + $jwt = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzk5OSJ9.eyJtZXNzYWdlIjoiYWJjIiwibmJmIjoxNjAxODgxMjg0fQ.V43ebYjSX8b7PyTWv5x7Q12g550ZP3shut19XrCCsaQ'; + $this->setExpectedException( + 'UnexpectedValueException', + 'Algorithm not supported' + ); + JWT::decode($jwt, 'my_key', ['HS256']); + } + + public function testDecodeInvalidHeaderNoKidAndX5tJWK() + { + $jwt = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJtZXNzYWdlIjoiYWJjIiwibmJmIjoxNjAxODgxNjc4fQ.5m_UELHsCus6gJyOEXOXkDcuG0qjlUO3dxpR9GT1QNQRR'; + $this->setExpectedException( + 'UnexpectedValueException', + '"kid" && "x5t" empty, unable to lookup correct key' + ); + JWT::decode($jwt, ['my_key'], ['HS256']); + } + + public function testDecodeInvalidHeaderNoKidJWK() + { + $jwt = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6ImtpZGtleSJ9.eyJtZXNzYWdlIjoiYWJjIiwibmJmIjoxNjAxODgxNjQ5fQ.pr3XRYQGfRBMO0YJb5365XOHsUBKpTueaJNH1I2L8EURR'; + $this->setExpectedException( + 'UnexpectedValueException', + '"kid" invalid, unable to lookup correct key' + ); + JWT::decode($jwt, ['my_key'], ['HS256']); + } + + public function testDecodeInvalidHeaderNoX5tJWK() + { + $jwt = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsIng1dCI6Ing1dGtleSJ9.eyJtZXNzYWdlIjoiYWJjIiwibmJmIjoxNjAxODgxNjI4fQ.OuhrKy2YVRhDzscPnBMOPuZDxytACA5wH_YdO32FnGERR'; + $this->setExpectedException( + 'UnexpectedValueException', + '"x5t" invalid, unable to lookup correct key' + ); + JWT::decode($jwt, ['my_key'], ['HS256']); + } + + public function testSignInvalidAlg() + { + $msg = 'testmsg'; + $this->setExpectedException( + 'DomainException', + 'Algorithm not supported' + ); + JWT::sign($msg, 'my_key', 'HS999'); + } + + public function testVerifyInvalidAlg() + { + $msg = 'testmsg'; + $sign = 'sign'; + + $this->setExpectedException( + 'DomainException', + 'Algorithm not supported' + ); + $keys = $this->invokeMethod( + new JWT, + 'verify', + [ + 'msg' => $msg, + 'signature' => $sign, + 'key' => 'my_key', + 'alg' => 'HS999', + ] + ); + } + + public function testIsExpiredEmptyTimestamp() + { + $future_payload = new \stdClass(); + $future_payload->exp = time() + 1000000; + + $expired_payload = new \stdClass(); + $expired_payload->exp = time() - 1000000; + + $this->assertFalse(JWT::isExpired($future_payload)); + $this->assertTrue(JWT::isExpired($expired_payload)); + } + + public function setExpectedException($exceptionName, $message = '', $code = null) + { + if (method_exists($this, 'expectException')) { + $this->expectException($exceptionName); + if (!empty($message)) { + $this->expectExceptionMessage($message); + } + } else { + parent::setExpectedException($exceptionName, $message, $code); + } + } + + private function invokeMethod($object, $methodName, array $parameters = []) + { + $reflection = new \ReflectionClass(get_class($object)); + $method = $reflection->getMethod($methodName); + $method->setAccessible(true); + + return $method->invokeArgs($object, $parameters); + } +} \ No newline at end of file diff --git a/tests/JWTTest.php b/tests/JWTTest.php index fc9c3756..44309cea 100644 --- a/tests/JWTTest.php +++ b/tests/JWTTest.php @@ -1,9 +1,12 @@ assertEquals(JWT::decode($msg, 'my_key', array('HS256')), 'abc'); + $this->assertEquals(JWT::decode($msg, 'my_key', ['HS256']), 'abc'); } public function testDecodeFromPython() { $msg = 'eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.Iio6aHR0cDovL2FwcGxpY2F0aW9uL2NsaWNreT9ibGFoPTEuMjMmZi5vbz00NTYgQUMwMDAgMTIzIg.E_U8X2YpMT5K1cEiT_3-IvBYfrdIFIeVYeOqre_Z5Cg'; $this->assertEquals( - JWT::decode($msg, 'my_key', array('HS256')), + JWT::decode($msg, 'my_key', ['HS256']), '*:http://application/clicky?blah=1.23&f.oo=456 AC000 123' ); } @@ -36,7 +39,7 @@ public function testDecodeFromPython() public function testUrlSafeCharacters() { $encoded = JWT::encode('f?', 'a'); - $this->assertEquals('f?', JWT::decode($encoded, 'a', array('HS256'))); + $this->assertEquals('f?', JWT::decode($encoded, 'a', ['HS256'])); } public function testMalformedUtf8StringsFail() @@ -53,52 +56,52 @@ public function testMalformedJsonThrowsException() public function testExpiredToken() { - $this->setExpectedException('Firebase\JWT\ExpiredException'); - $payload = array( - "message" => "abc", - "exp" => time() - 20); // time in the past + $this->setExpectedException('Alancting\Microsoft\JWT\ExpiredException'); + $payload = [ + 'message' => 'abc', + 'exp' => time() - 20, ]; // time in the past $encoded = JWT::encode($payload, 'my_key'); - JWT::decode($encoded, 'my_key', array('HS256')); + JWT::decode($encoded, 'my_key', ['HS256']); } public function testBeforeValidTokenWithNbf() { - $this->setExpectedException('Firebase\JWT\BeforeValidException'); - $payload = array( - "message" => "abc", - "nbf" => time() + 20); // time in the future + $this->setExpectedException('Alancting\Microsoft\JWT\BeforeValidException'); + $payload = [ + 'message' => 'abc', + 'nbf' => time() + 20, ]; // time in the future $encoded = JWT::encode($payload, 'my_key'); - JWT::decode($encoded, 'my_key', array('HS256')); + JWT::decode($encoded, 'my_key', ['HS256']); } public function testBeforeValidTokenWithIat() { - $this->setExpectedException('Firebase\JWT\BeforeValidException'); - $payload = array( - "message" => "abc", - "iat" => time() + 20); // time in the future + $this->setExpectedException('Alancting\Microsoft\JWT\BeforeValidException'); + $payload = [ + 'message' => 'abc', + 'iat' => time() + 20, ]; // time in the future $encoded = JWT::encode($payload, 'my_key'); - JWT::decode($encoded, 'my_key', array('HS256')); + JWT::decode($encoded, 'my_key', ['HS256']); } public function testValidToken() { - $payload = array( - "message" => "abc", - "exp" => time() + JWT::$leeway + 20); // time in the future + $payload = [ + 'message' => 'abc', + 'exp' => time() + JWT::$leeway + 20, ]; // time in the future $encoded = JWT::encode($payload, 'my_key'); - $decoded = JWT::decode($encoded, 'my_key', array('HS256')); + $decoded = JWT::decode($encoded, 'my_key', ['HS256']); $this->assertEquals($decoded->message, 'abc'); } public function testValidTokenWithLeeway() { JWT::$leeway = 60; - $payload = array( - "message" => "abc", - "exp" => time() - 20); // time in the past + $payload = [ + 'message' => 'abc', + 'exp' => time() - 20, ]; // time in the past $encoded = JWT::encode($payload, 'my_key'); - $decoded = JWT::decode($encoded, 'my_key', array('HS256')); + $decoded = JWT::decode($encoded, 'my_key', ['HS256']); $this->assertEquals($decoded->message, 'abc'); JWT::$leeway = 0; } @@ -106,46 +109,46 @@ public function testValidTokenWithLeeway() public function testExpiredTokenWithLeeway() { JWT::$leeway = 60; - $payload = array( - "message" => "abc", - "exp" => time() - 70); // time far in the past - $this->setExpectedException('Firebase\JWT\ExpiredException'); + $payload = [ + 'message' => 'abc', + 'exp' => time() - 70, ]; // time far in the past + $this->setExpectedException('Alancting\Microsoft\JWT\ExpiredException'); $encoded = JWT::encode($payload, 'my_key'); - $decoded = JWT::decode($encoded, 'my_key', array('HS256')); + $decoded = JWT::decode($encoded, 'my_key', ['HS256']); $this->assertEquals($decoded->message, 'abc'); JWT::$leeway = 0; } public function testValidTokenWithList() { - $payload = array( - "message" => "abc", - "exp" => time() + 20); // time in the future + $payload = [ + 'message' => 'abc', + 'exp' => time() + 20, ]; // time in the future $encoded = JWT::encode($payload, 'my_key'); - $decoded = JWT::decode($encoded, 'my_key', array('HS256', 'HS512')); + $decoded = JWT::decode($encoded, 'my_key', ['HS256', 'HS512']); $this->assertEquals($decoded->message, 'abc'); } public function testValidTokenWithNbf() { - $payload = array( - "message" => "abc", - "iat" => time(), - "exp" => time() + 20, // time in the future - "nbf" => time() - 20); + $payload = [ + 'message' => 'abc', + 'iat' => time(), + 'exp' => time() + 20, // time in the future + 'nbf' => time() - 20, ]; $encoded = JWT::encode($payload, 'my_key'); - $decoded = JWT::decode($encoded, 'my_key', array('HS256')); + $decoded = JWT::decode($encoded, 'my_key', ['HS256']); $this->assertEquals($decoded->message, 'abc'); } public function testValidTokenWithNbfLeeway() { JWT::$leeway = 60; - $payload = array( - "message" => "abc", - "nbf" => time() + 20); // not before in near (leeway) future + $payload = [ + 'message' => 'abc', + 'nbf' => time() + 20, ]; // not before in near (leeway) future $encoded = JWT::encode($payload, 'my_key'); - $decoded = JWT::decode($encoded, 'my_key', array('HS256')); + $decoded = JWT::decode($encoded, 'my_key', ['HS256']); $this->assertEquals($decoded->message, 'abc'); JWT::$leeway = 0; } @@ -153,23 +156,23 @@ public function testValidTokenWithNbfLeeway() public function testInvalidTokenWithNbfLeeway() { JWT::$leeway = 60; - $payload = array( - "message" => "abc", - "nbf" => time() + 65); // not before too far in future + $payload = [ + 'message' => 'abc', + 'nbf' => time() + 65, ]; // not before too far in future $encoded = JWT::encode($payload, 'my_key'); - $this->setExpectedException('Firebase\JWT\BeforeValidException'); - JWT::decode($encoded, 'my_key', array('HS256')); + $this->setExpectedException('Alancting\Microsoft\JWT\BeforeValidException'); + JWT::decode($encoded, 'my_key', ['HS256']); JWT::$leeway = 0; } public function testValidTokenWithIatLeeway() { JWT::$leeway = 60; - $payload = array( - "message" => "abc", - "iat" => time() + 20); // issued in near (leeway) future + $payload = [ + 'message' => 'abc', + 'iat' => time() + 20, ]; // issued in near (leeway) future $encoded = JWT::encode($payload, 'my_key'); - $decoded = JWT::decode($encoded, 'my_key', array('HS256')); + $decoded = JWT::decode($encoded, 'my_key', ['HS256']); $this->assertEquals($decoded->message, 'abc'); JWT::$leeway = 0; } @@ -177,70 +180,72 @@ public function testValidTokenWithIatLeeway() public function testInvalidTokenWithIatLeeway() { JWT::$leeway = 60; - $payload = array( - "message" => "abc", - "iat" => time() + 65); // issued too far in future + $payload = [ + 'message' => 'abc', + 'iat' => time() + 65, ]; // issued too far in future $encoded = JWT::encode($payload, 'my_key'); - $this->setExpectedException('Firebase\JWT\BeforeValidException'); - JWT::decode($encoded, 'my_key', array('HS256')); + $this->setExpectedException('Alancting\Microsoft\JWT\BeforeValidException'); + JWT::decode($encoded, 'my_key', ['HS256']); JWT::$leeway = 0; } public function testInvalidToken() { - $payload = array( - "message" => "abc", - "exp" => time() + 20); // time in the future + $payload = [ + 'message' => 'abc', + 'exp' => time() + 20, ]; // time in the future $encoded = JWT::encode($payload, 'my_key'); - $this->setExpectedException('Firebase\JWT\SignatureInvalidException'); - JWT::decode($encoded, 'my_key2', array('HS256')); + $this->setExpectedException('Alancting\Microsoft\JWT\SignatureInvalidException'); + JWT::decode($encoded, 'my_key2', ['HS256']); } public function testNullKeyFails() { - $payload = array( - "message" => "abc", - "exp" => time() + JWT::$leeway + 20); // time in the future + $payload = [ + 'message' => 'abc', + 'exp' => time() + JWT::$leeway + 20, ]; // time in the future $encoded = JWT::encode($payload, 'my_key'); $this->setExpectedException('InvalidArgumentException'); - JWT::decode($encoded, null, array('HS256')); + JWT::decode($encoded, null, ['HS256']); } public function testEmptyKeyFails() { - $payload = array( - "message" => "abc", - "exp" => time() + JWT::$leeway + 20); // time in the future + $payload = [ + 'message' => 'abc', + 'exp' => time() + JWT::$leeway + 20, ]; // time in the future $encoded = JWT::encode($payload, 'my_key'); $this->setExpectedException('InvalidArgumentException'); - JWT::decode($encoded, '', array('HS256')); + JWT::decode($encoded, '', ['HS256']); } public function testRSEncodeDecode() { - $privKey = openssl_pkey_new(array('digest_alg' => 'sha256', - 'private_key_bits' => 1024, - 'private_key_type' => OPENSSL_KEYTYPE_RSA)); + $privKey = openssl_pkey_new( + ['digest_alg' => 'sha256', + 'private_key_bits' => 1024, + 'private_key_type' => OPENSSL_KEYTYPE_RSA, ] + ); $msg = JWT::encode('abc', $privKey, 'RS256'); $pubKey = openssl_pkey_get_details($privKey); $pubKey = $pubKey['key']; - $decoded = JWT::decode($msg, $pubKey, array('RS256')); + $decoded = JWT::decode($msg, $pubKey, ['RS256']); $this->assertEquals($decoded, 'abc'); } public function testKIDChooser() { - $keys = array('1' => 'my_key', '2' => 'my_key2'); + $keys = ['1' => 'my_key', '2' => 'my_key2']; $msg = JWT::encode('abc', $keys['1'], 'HS256', '1'); - $decoded = JWT::decode($msg, $keys, array('HS256')); + $decoded = JWT::decode($msg, $keys, ['HS256']); $this->assertEquals($decoded, 'abc'); } public function testArrayAccessKIDChooser() { - $keys = new ArrayObject(array('1' => 'my_key', '2' => 'my_key2')); + $keys = new ArrayObject(['1' => 'my_key', '2' => 'my_key2']); $msg = JWT::encode('abc', $keys['1'], 'HS256', '1'); - $decoded = JWT::decode($msg, $keys, array('HS256')); + $decoded = JWT::decode($msg, $keys, ['HS256']); $this->assertEquals($decoded, 'abc'); } @@ -248,14 +253,14 @@ public function testNoneAlgorithm() { $msg = JWT::encode('abc', 'my_key'); $this->setExpectedException('UnexpectedValueException'); - JWT::decode($msg, 'my_key', array('none')); + JWT::decode($msg, 'my_key', ['none']); } public function testIncorrectAlgorithm() { $msg = JWT::encode('abc', 'my_key'); $this->setExpectedException('UnexpectedValueException'); - JWT::decode($msg, 'my_key', array('RS256')); + JWT::decode($msg, 'my_key', ['RS256']); } public function testMissingAlgorithm() @@ -267,21 +272,21 @@ public function testMissingAlgorithm() public function testAdditionalHeaders() { - $msg = JWT::encode('abc', 'my_key', 'HS256', null, array('cty' => 'test-eit;v=1')); - $this->assertEquals(JWT::decode($msg, 'my_key', array('HS256')), 'abc'); + $msg = JWT::encode('abc', 'my_key', 'HS256', null, ['cty' => 'test-eit;v=1']); + $this->assertEquals(JWT::decode($msg, 'my_key', ['HS256']), 'abc'); } public function testInvalidSegmentCount() { $this->setExpectedException('UnexpectedValueException'); - JWT::decode('brokenheader.brokenbody', 'my_key', array('HS256')); + JWT::decode('brokenheader.brokenbody', 'my_key', ['HS256']); } public function testInvalidSignatureEncoding() { - $msg = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwibmFtZSI6ImZvbyJ9.Q4Kee9E8o0Xfo4ADXvYA8t7dN_X_bU9K5w6tXuiSjlUxx"; + $msg = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwibmFtZSI6ImZvbyJ9.Q4Kee9E8o0Xfo4ADXvYA8t7dN_X_bU9K5w6tXuiSjlUxx'; $this->setExpectedException('UnexpectedValueException'); - JWT::decode($msg, 'secret', array('HS256')); + JWT::decode($msg, 'secret', ['HS256']); } /** @@ -290,13 +295,13 @@ public function testInvalidSignatureEncoding() public function testEncodeAndDecodeEcdsaToken() { $privateKey = file_get_contents(__DIR__ . '/ecdsa-private.pem'); - $payload = array('foo' => 'bar'); + $payload = ['foo' => 'bar']; $encoded = JWT::encode($payload, $privateKey, 'ES256'); // Verify decoding succeeds $publicKey = file_get_contents(__DIR__ . '/ecdsa-public.pem'); - $decoded = JWT::decode($encoded, $publicKey, array('ES256')); + $decoded = JWT::decode($encoded, $publicKey, ['ES256']); $this->assertEquals('bar', $decoded->foo); } -} +} \ No newline at end of file diff --git a/tests/metadata/adfs/configuration/configuration.json b/tests/metadata/adfs/configuration/configuration.json new file mode 100644 index 00000000..9ffdf942 --- /dev/null +++ b/tests/metadata/adfs/configuration/configuration.json @@ -0,0 +1,75 @@ +{ + "issuer": "https://your_domain/adfs", + "authorization_endpoint": "https://your_domain/adfs/oauth2/authorize/", + "token_endpoint": "https://your_domain/adfs/oauth2/token/", + "jwks_uri": "/../../tests/metadata/adfs/configuration/jwks_uri.json", + "token_endpoint_auth_methods_supported": [ + "client_secret_post", "client_secret_basic", "private_key_jwt", "windows_client_authentication" + ], + "response_types_supported": [ + "code", + "id_token", + "code id_token", + "id_token token", + "code token", + "code id_token token" + ], + "response_modes_supported": [ + "query", "fragment", "form_post" + ], + "grant_types_supported": [ + "authorization_code", + "refresh_token", + "client_credentials", + "urn:ietf:params:oauth:grant-type:jwt-bearer", + "implicit", + "password", + "srv_challenge", + "urn:ietf:params:oauth:grant-type:device_code", + "device_code" + ], + "subject_types_supported": ["pairwise"], + "scopes_supported": [ + "user_impersonation", + "vpn_cert", + "winhello_cert", + "allatclaims", + "profile", + "email", + "openid", + "aza", + "logon_cert" + ], + "id_token_signing_alg_values_supported": ["RS256"], + "token_endpoint_auth_signing_alg_values_supported": ["RS256"], + "access_token_issuer": "http://your_domain/adfs/services/trust", + "claims_supported": [ + "aud", + "iss", + "iat", + "exp", + "auth_time", + "nonce", + "at_hash", + "c_hash", + "sub", + "upn", + "unique_name", + "pwd_url", + "pwd_exp", + "mfa_auth_time", + "sid" + ], + "microsoft_multi_refresh_token": true, + "userinfo_endpoint": "https://your_domain/adfs/userinfo", + "capabilities": [], + "end_session_endpoint": "https://your_domain/adfs/oauth2/logout", + "as_access_token_token_binding_supported": true, + "as_refresh_token_token_binding_supported": true, + "resource_access_token_token_binding_supported": true, + "op_id_token_token_binding_supported": true, + "rp_id_token_token_binding_supported": true, + "frontchannel_logout_supported": true, + "frontchannel_logout_session_supported": true, + "device_authorization_endpoint": "https://your_domain/adfs/oauth2/devicecode" +} diff --git a/tests/metadata/adfs/configuration/jwks_uri.json b/tests/metadata/adfs/configuration/jwks_uri.json new file mode 100644 index 00000000..ad8c9811 --- /dev/null +++ b/tests/metadata/adfs/configuration/jwks_uri.json @@ -0,0 +1,14 @@ +{ + "keys": [ + { + "kty": "RSA", + "use": "sig", + "alg": "RS256", + "kid": "2lEZNsDIjsBPH94_b7-1z1IvnybfzOIz0hsBamzxCWc", + "x5t": "2lEZNsDIjsBPH94_b7-1z1IvnybfzOIz0hsBamzxCWc", + "n": "iQ92-4_TfaV7N9SvmzOCU1wc3txNoz0s1N7EtM1jzm1yb1cdAW5JjptAuAl5N1Mg77VyWMbyjPmNye2QykKWFknPRWWlWiD63_9hERRYcTBITwRwDA_z9GXCbTT-IQtTBcDFDMlZ1MsVqHFilhaKVMyjac_Y0NwTdcncxnYuvXYA_kmGUTxO12wHryzlTBlf09K1rnyawWy6W0qbrrqmHguCmB_P2O9kQRXpKGCTDCaQ_cvGqgreXiDkyQ-z5-6M3Yl8SEeftqUe955LzVtQFCLUz2yZ22je-8fhNTQdKq2dhvAatog8e7mtDYUhN5DDkN9giU6t4hvsUzW9armZiw", + "e": "AQAB", + "x5c": ["MIIBITANBgkqhkiG9w0BAQEFAAOCAQ4AMIIBCQKCAQCJD3b7j9N9pXs31K+bM4JTXBze3E2jPSzU3sS0zWPObXJvVx0BbkmOm0C4CXk3UyDvtXJYxvKM+Y3J7ZDKQpYWSc9FZaVaIPrf/2ERFFhxMEhPBHAMD/P0ZcJtNP4hC1MFwMUMyVnUyxWocWKWFopUzKNpz9jQ3BN1ydzGdi69dgD+SYZRPE7XbAevLOVMGV/T0rWufJrBbLpbSpuuuqYeC4KYH8/Y72RBFekoYJMMJpD9y8aqCt5eIOTJD7Pn7ozdiXxIR5+2pR73nkvNW1AUItTPbJnbaN77x+E1NB0qrZ2G8Bq2iDx7ua0NhSE3kMOQ32CJTq3iG+xTNb1quZmLAgMBAAE="] + } + ] +} diff --git a/tests/metadata/adfs/configuration/jwks_uri_private.json b/tests/metadata/adfs/configuration/jwks_uri_private.json new file mode 100644 index 00000000..443a4ec7 --- /dev/null +++ b/tests/metadata/adfs/configuration/jwks_uri_private.json @@ -0,0 +1,18 @@ +{ + "keys": [ + { + "p": "zfHS3PHp8E1txJfdrj3XOKj36reObeQuotvkbL5YlKcM5lSvYoOVlhRcTSkf0VwGGENqo7IAdXL24Nt3oK-5eQKY5LiDSeP5sZ0SXwxFm9gRCUZkmxyd8ojOi6hi4ixEq-LiSvPZa_6etj1mg2bcFjmFWUi4fljM-VSCZATJf0E", + "kty": "RSA", + "q": "ql-QdsrCA1qF-wpuf0OyT5A01Q9U2kAo_V99Z7opxqmG6lH6Wrd1OEAXvP3L5KyH7wxx0NgKfCDUVkwxFXbXHB9X1pf3pwPxdSi5B3Q88gfq0Vm_ByJJ5G16QCIsZfzi2nA7gEnDx4jOQZuG6rP2-6TYn5SSUayqyWipQWJGccs", + "d": "huZpvX_6MRjHBlSX4rH3_AQVhmRfJeP4VCxOf4YITgz7LQsYyWe7jljQglMQs5tmN4jnWum1oXueSrlAYVLYVmefUjaRYPxE8GNzLlRQGFflU6CSh8zL2CiwDLyzw-JZfLgXOlaTzBV246t11TOqQ3yG-oZQaIGzxaNBYTTBHEeunj6UWhUbKsXAZKxpOT4Kf3QJbEJERceXRwDd0kflmAXPZlFIUY_C4H0R-WUEg52HJWL_-e7zKNLbapjr-X57bnESvNMA2we_eYqRlelie-JBLHks_SUiuAY1FhnWbQGFpMPC5Miwr4Mwdus4p3kthPxJBc1oeNo69CpLKI-8AQ", + "e": "AQAB", + "use": "sig", + "kid": "2lEZNsDIjsBPH94_b7-1z1IvnybfzOIz0hsBamzxCWc", + "qi": "pItuK4C9ojkLYYyzTdGpN_XPfpQZiNp_EGlCflsl_TwmVaMq_R8qYjoMpVoGPfSVYq009YcubIv8r8JdjJhbVK8rNl3IMCZQRe4b9YMxX86ohoaAhSgfj2TxnCN8vZJpbff6_X41i4oyBUmHGsjUuBWuhESLrCdIBxhEBem-_jg", + "dp": "WudjLCOcH3YN_bkLIN4rIddzlydutxMBguGM8nMSposWJpU61UE_xf82vthoMwFrr0oSyC7KBQ2564b0RvlJ5SBAXLUVPohirwOmGE5Sa3f0DSQFRHJdRbEdeofZHfxsU3LScEMytXiulcKEfXvpqeW59q8iwKJx15x18bArQQ", + "alg": "RS256", + "dq": "QNsoL1B4Era_EhWigqfOlSMcOY8gigSqleln37iqdonKZiDW4Pm9kbA0WSl0GJTlGkbufMYBF8eXjVJrzPP0Zyw3T-WBzP5fSG48IW5KVQhWh2NWqOyQnHhgdVGM_TYLVYQr4mYyNR8LBUajUW04tArIu9be7GCCkzFYXR-AGYk", + "n": "iQ92-4_TfaV7N9SvmzOCU1wc3txNoz0s1N7EtM1jzm1yb1cdAW5JjptAuAl5N1Mg77VyWMbyjPmNye2QykKWFknPRWWlWiD63_9hERRYcTBITwRwDA_z9GXCbTT-IQtTBcDFDMlZ1MsVqHFilhaKVMyjac_Y0NwTdcncxnYuvXYA_kmGUTxO12wHryzlTBlf09K1rnyawWy6W0qbrrqmHguCmB_P2O9kQRXpKGCTDCaQ_cvGqgreXiDkyQ-z5-6M3Yl8SEeftqUe955LzVtQFCLUz2yZ22je-8fhNTQdKq2dhvAatog8e7mtDYUhN5DDkN9giU6t4hvsUzW9armZiw" + } + ] +} diff --git a/tests/metadata/adfs/configuration/private.pem b/tests/metadata/adfs/configuration/private.pem new file mode 100644 index 00000000..29bd0728 --- /dev/null +++ b/tests/metadata/adfs/configuration/private.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAiQ92+4/TfaV7N9SvmzOCU1wc3txNoz0s1N7EtM1jzm1yb1cd +AW5JjptAuAl5N1Mg77VyWMbyjPmNye2QykKWFknPRWWlWiD63/9hERRYcTBITwRw +DA/z9GXCbTT+IQtTBcDFDMlZ1MsVqHFilhaKVMyjac/Y0NwTdcncxnYuvXYA/kmG +UTxO12wHryzlTBlf09K1rnyawWy6W0qbrrqmHguCmB/P2O9kQRXpKGCTDCaQ/cvG +qgreXiDkyQ+z5+6M3Yl8SEeftqUe955LzVtQFCLUz2yZ22je+8fhNTQdKq2dhvAa +tog8e7mtDYUhN5DDkN9giU6t4hvsUzW9armZiwIDAQABAoIBAQCG5mm9f/oxGMcG +VJfisff8BBWGZF8l4/hULE5/hghODPstCxjJZ7uOWNCCUxCzm2Y3iOda6bWhe55K +uUBhUthWZ59SNpFg/ETwY3MuVFAYV+VToJKHzMvYKLAMvLPD4ll8uBc6VpPMFXbj +q3XVM6pDfIb6hlBogbPFo0FhNMEcR66ePpRaFRsqxcBkrGk5Pgp/dAlsQkRFx5dH +AN3SR+WYBc9mUUhRj8LgfRH5ZQSDnYclYv/57vMo0ttqmOv5fntucRK80wDbB795 +ipGV6WJ74kEseSz9JSK4BjUWGdZtAYWkw8LkyLCvgzB26zineS2E/EkFzWh42jr0 +Kksoj7wBAoGBAM3x0tzx6fBNbcSX3a491zio9+q3jm3kLqLb5Gy+WJSnDOZUr2KD +lZYUXE0pH9FcBhhDaqOyAHVy9uDbd6CvuXkCmOS4g0nj+bGdEl8MRZvYEQlGZJsc +nfKIzouoYuIsRKvi4krz2Wv+nrY9ZoNm3BY5hVlIuH5YzPlUgmQEyX9BAoGBAKpf +kHbKwgNahfsKbn9Dsk+QNNUPVNpAKP1ffWe6KcaphupR+lq3dThAF7z9y+Ssh+8M +cdDYCnwg1FZMMRV21xwfV9aX96cD8XUouQd0PPIH6tFZvwciSeRtekAiLGX84tpw +O4BJw8eIzkGbhuqz9vuk2J+UklGsqsloqUFiRnHLAn9a52MsI5wfdg39uQsg3ish +13OXJ263EwGC4YzycxKmixYmlTrVQT/F/za+2GgzAWuvShLILsoFDbnrhvRG+Unl +IEBctRU+iGKvA6YYTlJrd/QNJAVEcl1FsR16h9kd/GxTctJwQzK1eK6VwoR9e+mp +5bn2ryLAonHXnHXxsCtBAoGAQNsoL1B4Era/EhWigqfOlSMcOY8gigSqleln37iq +donKZiDW4Pm9kbA0WSl0GJTlGkbufMYBF8eXjVJrzPP0Zyw3T+WBzP5fSG48IW5K +VQhWh2NWqOyQnHhgdVGM/TYLVYQr4mYyNR8LBUajUW04tArIu9be7GCCkzFYXR+A +GYkCgYEApItuK4C9ojkLYYyzTdGpN/XPfpQZiNp/EGlCflsl/TwmVaMq/R8qYjoM +pVoGPfSVYq009YcubIv8r8JdjJhbVK8rNl3IMCZQRe4b9YMxX86ohoaAhSgfj2Tx +nCN8vZJpbff6/X41i4oyBUmHGsjUuBWuhESLrCdIBxhEBem+/jg= +-----END RSA PRIVATE KEY----- diff --git a/tests/metadata/azure_ad/configuration/configuration.json b/tests/metadata/azure_ad/configuration/configuration.json new file mode 100644 index 00000000..20b0446e --- /dev/null +++ b/tests/metadata/azure_ad/configuration/configuration.json @@ -0,0 +1,52 @@ +{ + "token_endpoint": "https://login.microsoftonline.com/iv9puejd-qmJ1-AL2i-j3TP-wrb7qjjvxttz/oauth2/v2.0/token", + "token_endpoint_auth_methods_supported": [ + "client_secret_post", "private_key_jwt", "client_secret_basic" + ], + "jwks_uri": "/../../tests/metadata/azure_ad/configuration/jwks_uri.json", + "response_modes_supported": [ + "query", "fragment", "form_post" + ], + "subject_types_supported": ["pairwise"], + "id_token_signing_alg_values_supported": ["RS256"], + "response_types_supported": [ + "code", "id_token", "code id_token", "id_token token" + ], + "scopes_supported": [ + "openid", "profile", "email", "offline_access" + ], + "issuer": "https://login.microsoftonline.com/iv9puejd-qmJ1-AL2i-j3TP-wrb7qjjvxttz/v2.0", + "request_uri_parameter_supported": false, + "userinfo_endpoint": "https://graph.microsoft.com/oidc/userinfo", + "authorization_endpoint": "https://login.microsoftonline.com/iv9puejd-qmJ1-AL2i-j3TP-wrb7qjjvxttz/oauth2/v2.0/authorize", + "device_authorization_endpoint": "https://login.microsoftonline.com/iv9puejd-qmJ1-AL2i-j3TP-wrb7qjjvxttz/oauth2/v2.0/devicecode", + "http_logout_supported": true, + "frontchannel_logout_supported": true, + "end_session_endpoint": "https://login.microsoftonline.com/iv9puejd-qmJ1-AL2i-j3TP-wrb7qjjvxttz/oauth2/v2.0/logout", + "claims_supported": [ + "sub", + "iss", + "cloud_instance_name", + "cloud_instance_host_name", + "cloud_graph_host_name", + "msgraph_host", + "aud", + "exp", + "iat", + "auth_time", + "acr", + "nonce", + "preferred_username", + "name", + "tid", + "ver", + "at_hash", + "c_hash", + "email" + ], + "tenant_region_scope": "AS", + "cloud_instance_name": "microsoftonline.com", + "cloud_graph_host_name": "graph.windows.net", + "msgraph_host": "graph.microsoft.com", + "rbac_url": "https://pas.windows.net" +} diff --git a/tests/metadata/azure_ad/configuration/jwks_uri.json b/tests/metadata/azure_ad/configuration/jwks_uri.json new file mode 100644 index 00000000..ad8c9811 --- /dev/null +++ b/tests/metadata/azure_ad/configuration/jwks_uri.json @@ -0,0 +1,14 @@ +{ + "keys": [ + { + "kty": "RSA", + "use": "sig", + "alg": "RS256", + "kid": "2lEZNsDIjsBPH94_b7-1z1IvnybfzOIz0hsBamzxCWc", + "x5t": "2lEZNsDIjsBPH94_b7-1z1IvnybfzOIz0hsBamzxCWc", + "n": "iQ92-4_TfaV7N9SvmzOCU1wc3txNoz0s1N7EtM1jzm1yb1cdAW5JjptAuAl5N1Mg77VyWMbyjPmNye2QykKWFknPRWWlWiD63_9hERRYcTBITwRwDA_z9GXCbTT-IQtTBcDFDMlZ1MsVqHFilhaKVMyjac_Y0NwTdcncxnYuvXYA_kmGUTxO12wHryzlTBlf09K1rnyawWy6W0qbrrqmHguCmB_P2O9kQRXpKGCTDCaQ_cvGqgreXiDkyQ-z5-6M3Yl8SEeftqUe955LzVtQFCLUz2yZ22je-8fhNTQdKq2dhvAatog8e7mtDYUhN5DDkN9giU6t4hvsUzW9armZiw", + "e": "AQAB", + "x5c": ["MIIBITANBgkqhkiG9w0BAQEFAAOCAQ4AMIIBCQKCAQCJD3b7j9N9pXs31K+bM4JTXBze3E2jPSzU3sS0zWPObXJvVx0BbkmOm0C4CXk3UyDvtXJYxvKM+Y3J7ZDKQpYWSc9FZaVaIPrf/2ERFFhxMEhPBHAMD/P0ZcJtNP4hC1MFwMUMyVnUyxWocWKWFopUzKNpz9jQ3BN1ydzGdi69dgD+SYZRPE7XbAevLOVMGV/T0rWufJrBbLpbSpuuuqYeC4KYH8/Y72RBFekoYJMMJpD9y8aqCt5eIOTJD7Pn7ozdiXxIR5+2pR73nkvNW1AUItTPbJnbaN77x+E1NB0qrZ2G8Bq2iDx7ua0NhSE3kMOQ32CJTq3iG+xTNb1quZmLAgMBAAE="] + } + ] +} diff --git a/tests/metadata/azure_ad/configuration/jwks_uri_private.json b/tests/metadata/azure_ad/configuration/jwks_uri_private.json new file mode 100644 index 00000000..443a4ec7 --- /dev/null +++ b/tests/metadata/azure_ad/configuration/jwks_uri_private.json @@ -0,0 +1,18 @@ +{ + "keys": [ + { + "p": "zfHS3PHp8E1txJfdrj3XOKj36reObeQuotvkbL5YlKcM5lSvYoOVlhRcTSkf0VwGGENqo7IAdXL24Nt3oK-5eQKY5LiDSeP5sZ0SXwxFm9gRCUZkmxyd8ojOi6hi4ixEq-LiSvPZa_6etj1mg2bcFjmFWUi4fljM-VSCZATJf0E", + "kty": "RSA", + "q": "ql-QdsrCA1qF-wpuf0OyT5A01Q9U2kAo_V99Z7opxqmG6lH6Wrd1OEAXvP3L5KyH7wxx0NgKfCDUVkwxFXbXHB9X1pf3pwPxdSi5B3Q88gfq0Vm_ByJJ5G16QCIsZfzi2nA7gEnDx4jOQZuG6rP2-6TYn5SSUayqyWipQWJGccs", + "d": "huZpvX_6MRjHBlSX4rH3_AQVhmRfJeP4VCxOf4YITgz7LQsYyWe7jljQglMQs5tmN4jnWum1oXueSrlAYVLYVmefUjaRYPxE8GNzLlRQGFflU6CSh8zL2CiwDLyzw-JZfLgXOlaTzBV246t11TOqQ3yG-oZQaIGzxaNBYTTBHEeunj6UWhUbKsXAZKxpOT4Kf3QJbEJERceXRwDd0kflmAXPZlFIUY_C4H0R-WUEg52HJWL_-e7zKNLbapjr-X57bnESvNMA2we_eYqRlelie-JBLHks_SUiuAY1FhnWbQGFpMPC5Miwr4Mwdus4p3kthPxJBc1oeNo69CpLKI-8AQ", + "e": "AQAB", + "use": "sig", + "kid": "2lEZNsDIjsBPH94_b7-1z1IvnybfzOIz0hsBamzxCWc", + "qi": "pItuK4C9ojkLYYyzTdGpN_XPfpQZiNp_EGlCflsl_TwmVaMq_R8qYjoMpVoGPfSVYq009YcubIv8r8JdjJhbVK8rNl3IMCZQRe4b9YMxX86ohoaAhSgfj2TxnCN8vZJpbff6_X41i4oyBUmHGsjUuBWuhESLrCdIBxhEBem-_jg", + "dp": "WudjLCOcH3YN_bkLIN4rIddzlydutxMBguGM8nMSposWJpU61UE_xf82vthoMwFrr0oSyC7KBQ2564b0RvlJ5SBAXLUVPohirwOmGE5Sa3f0DSQFRHJdRbEdeofZHfxsU3LScEMytXiulcKEfXvpqeW59q8iwKJx15x18bArQQ", + "alg": "RS256", + "dq": "QNsoL1B4Era_EhWigqfOlSMcOY8gigSqleln37iqdonKZiDW4Pm9kbA0WSl0GJTlGkbufMYBF8eXjVJrzPP0Zyw3T-WBzP5fSG48IW5KVQhWh2NWqOyQnHhgdVGM_TYLVYQr4mYyNR8LBUajUW04tArIu9be7GCCkzFYXR-AGYk", + "n": "iQ92-4_TfaV7N9SvmzOCU1wc3txNoz0s1N7EtM1jzm1yb1cdAW5JjptAuAl5N1Mg77VyWMbyjPmNye2QykKWFknPRWWlWiD63_9hERRYcTBITwRwDA_z9GXCbTT-IQtTBcDFDMlZ1MsVqHFilhaKVMyjac_Y0NwTdcncxnYuvXYA_kmGUTxO12wHryzlTBlf09K1rnyawWy6W0qbrrqmHguCmB_P2O9kQRXpKGCTDCaQ_cvGqgreXiDkyQ-z5-6M3Yl8SEeftqUe955LzVtQFCLUz2yZ22je-8fhNTQdKq2dhvAatog8e7mtDYUhN5DDkN9giU6t4hvsUzW9armZiw" + } + ] +} diff --git a/tests/metadata/azure_ad/configuration/private.pem b/tests/metadata/azure_ad/configuration/private.pem new file mode 100644 index 00000000..29bd0728 --- /dev/null +++ b/tests/metadata/azure_ad/configuration/private.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAiQ92+4/TfaV7N9SvmzOCU1wc3txNoz0s1N7EtM1jzm1yb1cd +AW5JjptAuAl5N1Mg77VyWMbyjPmNye2QykKWFknPRWWlWiD63/9hERRYcTBITwRw +DA/z9GXCbTT+IQtTBcDFDMlZ1MsVqHFilhaKVMyjac/Y0NwTdcncxnYuvXYA/kmG +UTxO12wHryzlTBlf09K1rnyawWy6W0qbrrqmHguCmB/P2O9kQRXpKGCTDCaQ/cvG +qgreXiDkyQ+z5+6M3Yl8SEeftqUe955LzVtQFCLUz2yZ22je+8fhNTQdKq2dhvAa +tog8e7mtDYUhN5DDkN9giU6t4hvsUzW9armZiwIDAQABAoIBAQCG5mm9f/oxGMcG +VJfisff8BBWGZF8l4/hULE5/hghODPstCxjJZ7uOWNCCUxCzm2Y3iOda6bWhe55K +uUBhUthWZ59SNpFg/ETwY3MuVFAYV+VToJKHzMvYKLAMvLPD4ll8uBc6VpPMFXbj +q3XVM6pDfIb6hlBogbPFo0FhNMEcR66ePpRaFRsqxcBkrGk5Pgp/dAlsQkRFx5dH +AN3SR+WYBc9mUUhRj8LgfRH5ZQSDnYclYv/57vMo0ttqmOv5fntucRK80wDbB795 +ipGV6WJ74kEseSz9JSK4BjUWGdZtAYWkw8LkyLCvgzB26zineS2E/EkFzWh42jr0 +Kksoj7wBAoGBAM3x0tzx6fBNbcSX3a491zio9+q3jm3kLqLb5Gy+WJSnDOZUr2KD +lZYUXE0pH9FcBhhDaqOyAHVy9uDbd6CvuXkCmOS4g0nj+bGdEl8MRZvYEQlGZJsc +nfKIzouoYuIsRKvi4krz2Wv+nrY9ZoNm3BY5hVlIuH5YzPlUgmQEyX9BAoGBAKpf +kHbKwgNahfsKbn9Dsk+QNNUPVNpAKP1ffWe6KcaphupR+lq3dThAF7z9y+Ssh+8M +cdDYCnwg1FZMMRV21xwfV9aX96cD8XUouQd0PPIH6tFZvwciSeRtekAiLGX84tpw +O4BJw8eIzkGbhuqz9vuk2J+UklGsqsloqUFiRnHLAn9a52MsI5wfdg39uQsg3ish +13OXJ263EwGC4YzycxKmixYmlTrVQT/F/za+2GgzAWuvShLILsoFDbnrhvRG+Unl +IEBctRU+iGKvA6YYTlJrd/QNJAVEcl1FsR16h9kd/GxTctJwQzK1eK6VwoR9e+mp +5bn2ryLAonHXnHXxsCtBAoGAQNsoL1B4Era/EhWigqfOlSMcOY8gigSqleln37iq +donKZiDW4Pm9kbA0WSl0GJTlGkbufMYBF8eXjVJrzPP0Zyw3T+WBzP5fSG48IW5K +VQhWh2NWqOyQnHhgdVGM/TYLVYQr4mYyNR8LBUajUW04tArIu9be7GCCkzFYXR+A +GYkCgYEApItuK4C9ojkLYYyzTdGpN/XPfpQZiNp/EGlCflsl/TwmVaMq/R8qYjoM +pVoGPfSVYq009YcubIv8r8JdjJhbVK8rNl3IMCZQRe4b9YMxX86ohoaAhSgfj2Tx +nCN8vZJpbff6/X41i4oyBUmHGsjUuBWuhESLrCdIBxhEBem+/jg= +-----END RSA PRIVATE KEY----- diff --git a/tests/metadata/jwk/rsa-public-no-e.json b/tests/metadata/jwk/rsa-public-no-e.json new file mode 100644 index 00000000..29ff2264 --- /dev/null +++ b/tests/metadata/jwk/rsa-public-no-e.json @@ -0,0 +1,7 @@ +{ + "kty": "RSA", + "use": "sig", + "kid": "2lEZNsDIjsBPH94_b7-1z1IvnybfzOIz0hsBamzxCWc", + "alg": "RS256", + "n": "iQ92-4_TfaV7N9SvmzOCU1wc3txNoz0s1N7EtM1jzm1yb1cdAW5JjptAuAl5N1Mg77VyWMbyjPmNye2QykKWFknPRWWlWiD63_9hERRYcTBITwRwDA_z9GXCbTT-IQtTBcDFDMlZ1MsVqHFilhaKVMyjac_Y0NwTdcncxnYuvXYA_kmGUTxO12wHryzlTBlf09K1rnyawWy6W0qbrrqmHguCmB_P2O9kQRXpKGCTDCaQ_cvGqgreXiDkyQ-z5-6M3Yl8SEeftqUe955LzVtQFCLUz2yZ22je-8fhNTQdKq2dhvAatog8e7mtDYUhN5DDkN9giU6t4hvsUzW9armZiw" +} diff --git a/tests/metadata/jwk/rsa-public-no-n.json b/tests/metadata/jwk/rsa-public-no-n.json new file mode 100644 index 00000000..9e850581 --- /dev/null +++ b/tests/metadata/jwk/rsa-public-no-n.json @@ -0,0 +1,7 @@ +{ + "kty": "RSA", + "e": "AQAB", + "use": "sig", + "kid": "2lEZNsDIjsBPH94_b7-1z1IvnybfzOIz0hsBamzxCWc", + "alg": "RS256" +} diff --git a/tests/metadata/jwk/rsa-public-private.json b/tests/metadata/jwk/rsa-public-private.json new file mode 100644 index 00000000..4ae2b953 --- /dev/null +++ b/tests/metadata/jwk/rsa-public-private.json @@ -0,0 +1,14 @@ +{ + "p": "zfHS3PHp8E1txJfdrj3XOKj36reObeQuotvkbL5YlKcM5lSvYoOVlhRcTSkf0VwGGENqo7IAdXL24Nt3oK-5eQKY5LiDSeP5sZ0SXwxFm9gRCUZkmxyd8ojOi6hi4ixEq-LiSvPZa_6etj1mg2bcFjmFWUi4fljM-VSCZATJf0E", + "kty": "RSA", + "q": "ql-QdsrCA1qF-wpuf0OyT5A01Q9U2kAo_V99Z7opxqmG6lH6Wrd1OEAXvP3L5KyH7wxx0NgKfCDUVkwxFXbXHB9X1pf3pwPxdSi5B3Q88gfq0Vm_ByJJ5G16QCIsZfzi2nA7gEnDx4jOQZuG6rP2-6TYn5SSUayqyWipQWJGccs", + "d": "huZpvX_6MRjHBlSX4rH3_AQVhmRfJeP4VCxOf4YITgz7LQsYyWe7jljQglMQs5tmN4jnWum1oXueSrlAYVLYVmefUjaRYPxE8GNzLlRQGFflU6CSh8zL2CiwDLyzw-JZfLgXOlaTzBV246t11TOqQ3yG-oZQaIGzxaNBYTTBHEeunj6UWhUbKsXAZKxpOT4Kf3QJbEJERceXRwDd0kflmAXPZlFIUY_C4H0R-WUEg52HJWL_-e7zKNLbapjr-X57bnESvNMA2we_eYqRlelie-JBLHks_SUiuAY1FhnWbQGFpMPC5Miwr4Mwdus4p3kthPxJBc1oeNo69CpLKI-8AQ", + "e": "AQAB", + "use": "sig", + "kid": "2lEZNsDIjsBPH94_b7-1z1IvnybfzOIz0hsBamzxCWc", + "qi": "pItuK4C9ojkLYYyzTdGpN_XPfpQZiNp_EGlCflsl_TwmVaMq_R8qYjoMpVoGPfSVYq009YcubIv8r8JdjJhbVK8rNl3IMCZQRe4b9YMxX86ohoaAhSgfj2TxnCN8vZJpbff6_X41i4oyBUmHGsjUuBWuhESLrCdIBxhEBem-_jg", + "dp": "WudjLCOcH3YN_bkLIN4rIddzlydutxMBguGM8nMSposWJpU61UE_xf82vthoMwFrr0oSyC7KBQ2564b0RvlJ5SBAXLUVPohirwOmGE5Sa3f0DSQFRHJdRbEdeofZHfxsU3LScEMytXiulcKEfXvpqeW59q8iwKJx15x18bArQQ", + "alg": "RS256", + "dq": "QNsoL1B4Era_EhWigqfOlSMcOY8gigSqleln37iqdonKZiDW4Pm9kbA0WSl0GJTlGkbufMYBF8eXjVJrzPP0Zyw3T-WBzP5fSG48IW5KVQhWh2NWqOyQnHhgdVGM_TYLVYQr4mYyNR8LBUajUW04tArIu9be7GCCkzFYXR-AGYk", + "n": "iQ92-4_TfaV7N9SvmzOCU1wc3txNoz0s1N7EtM1jzm1yb1cdAW5JjptAuAl5N1Mg77VyWMbyjPmNye2QykKWFknPRWWlWiD63_9hERRYcTBITwRwDA_z9GXCbTT-IQtTBcDFDMlZ1MsVqHFilhaKVMyjac_Y0NwTdcncxnYuvXYA_kmGUTxO12wHryzlTBlf09K1rnyawWy6W0qbrrqmHguCmB_P2O9kQRXpKGCTDCaQ_cvGqgreXiDkyQ-z5-6M3Yl8SEeftqUe955LzVtQFCLUz2yZ22je-8fhNTQdKq2dhvAatog8e7mtDYUhN5DDkN9giU6t4hvsUzW9armZiw" +} diff --git a/tests/metadata/jwk/rsa-public.json b/tests/metadata/jwk/rsa-public.json new file mode 100644 index 00000000..4a33df93 --- /dev/null +++ b/tests/metadata/jwk/rsa-public.json @@ -0,0 +1,8 @@ +{ + "kty": "RSA", + "e": "AQAB", + "use": "sig", + "kid": "2lEZNsDIjsBPH94_b7-1z1IvnybfzOIz0hsBamzxCWc", + "alg": "RS256", + "n": "iQ92-4_TfaV7N9SvmzOCU1wc3txNoz0s1N7EtM1jzm1yb1cdAW5JjptAuAl5N1Mg77VyWMbyjPmNye2QykKWFknPRWWlWiD63_9hERRYcTBITwRwDA_z9GXCbTT-IQtTBcDFDMlZ1MsVqHFilhaKVMyjac_Y0NwTdcncxnYuvXYA_kmGUTxO12wHryzlTBlf09K1rnyawWy6W0qbrrqmHguCmB_P2O9kQRXpKGCTDCaQ_cvGqgreXiDkyQ-z5-6M3Yl8SEeftqUe955LzVtQFCLUz2yZ22je-8fhNTQdKq2dhvAatog8e7mtDYUhN5DDkN9giU6t4hvsUzW9armZiw" +} From e6a3386c18a717f291c4278a92df683875b1f241 Mon Sep 17 00:00:00 2001 From: Neodeblackie <80467285+Neodeblackie@users.noreply.github.com> Date: Mon, 15 Mar 2021 18:46:29 +0800 Subject: [PATCH 6/9] Add logic to use CURL retrieving Config and JWT (#5) * Add logic to use CURL retrieving Config and JWT * Amend coveralls.yml for handling PHP 7 Co-authored-by: neoblackie --- .github/workflows/coveralls.yml | 6 ++++++ composer.json | 6 +++--- src/Base/MicrosoftConfiguration.php | 7 +++++++ 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/.github/workflows/coveralls.yml b/.github/workflows/coveralls.yml index dde873c5..8d2580b9 100644 --- a/.github/workflows/coveralls.yml +++ b/.github/workflows/coveralls.yml @@ -13,6 +13,12 @@ jobs: steps: - uses: actions/checkout@v2 + - name: Setup PHP with Xdebug + uses: shivammathur/setup-php@v2 + with: + php-version: '7.4' + coverage: xdebug + - name: Validate composer.json and composer.lock run: composer validate diff --git a/composer.json b/composer.json index 649a300a..8559114c 100644 --- a/composer.json +++ b/composer.json @@ -45,8 +45,8 @@ } }, "scripts": { - "test": ["./vendor/bin/phpunit --colors=always"], - "coverage": ["./vendor/bin/phpunit --colors=always --coverage-text --coverage-html ./coverage --coverage-clover=build/logs/clover.xml"], - "coveralls": ["composer run coverage && ./vendor/bin/php-coveralls"] + "test": [ "@putenv XDEBUG_MODE=coverage", "./vendor/bin/phpunit --colors=always" ], + "coverage": [ "@putenv XDEBUG_MODE=coverage", "./vendor/bin/phpunit --colors=always --coverage-text --coverage-html ./coverage --coverage-clover=build/logs/clover.xml" ], + "coveralls": [ "@putenv XDEBUG_MODE=coverage", "composer run coverage && ./vendor/bin/php-coveralls"] } } diff --git a/src/Base/MicrosoftConfiguration.php b/src/Base/MicrosoftConfiguration.php index fdc58678..fa0a1030 100644 --- a/src/Base/MicrosoftConfiguration.php +++ b/src/Base/MicrosoftConfiguration.php @@ -161,6 +161,13 @@ private function getFromUrlOrFile($value) $targetUri = $value; if (filter_var($value, FILTER_VALIDATE_URL) === false) { $targetUri = realpath($value) === false ? __DIR__ . $value : $value; + $result = @file_get_contents($targetUri); + } else { + $ch = curl_init($value); + curl_setopt($ch, CURLOPT_HTTPGET, true); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + $result = curl_exec($ch); + curl_close($ch); } $result = @file_get_contents($targetUri); From 567b7630b8a82313c508fd0deb1e329ba65e2c43 Mon Sep 17 00:00:00 2001 From: Alan Si Date: Mon, 15 Mar 2021 18:58:06 +0800 Subject: [PATCH 7/9] v1.1.0 release update --- composer.json | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/composer.json b/composer.json index 8559114c..6dcd63c9 100644 --- a/composer.json +++ b/composer.json @@ -11,7 +11,7 @@ "ad", "microsoft" ], - "version": "1.0.0", + "version": "1.1.0", "authors": [ { "name": "Neuman Vong", @@ -45,8 +45,12 @@ } }, "scripts": { - "test": [ "@putenv XDEBUG_MODE=coverage", "./vendor/bin/phpunit --colors=always" ], - "coverage": [ "@putenv XDEBUG_MODE=coverage", "./vendor/bin/phpunit --colors=always --coverage-text --coverage-html ./coverage --coverage-clover=build/logs/clover.xml" ], - "coveralls": [ "@putenv XDEBUG_MODE=coverage", "composer run coverage && ./vendor/bin/php-coveralls"] + "test": [ + "@putenv XDEBUG_MODE=coverage", "./vendor/bin/phpunit --colors=always" + ], + "coverage": [ + "@putenv XDEBUG_MODE=coverage", "./vendor/bin/phpunit --colors=always --coverage-text --coverage-html ./coverage --coverage-clover=build/logs/clover.xml" + ], + "coveralls": ["@putenv XDEBUG_MODE=coverage", "composer run coverage && ./vendor/bin/php-coveralls"] } } From 3f62f07007c6c083c7412e7e2972eb963a19ed8f Mon Sep 17 00:00:00 2001 From: Alan Si Date: Mon, 15 Mar 2021 19:20:25 +0800 Subject: [PATCH 8/9] Sync with firebase/php-jwt:v5.2.1 --- README.md | 2 +- composer.json | 2 +- src/JWK.php | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 0ecec9e2..e323d1d7 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![Test](https://img.shields.io/github/workflow/status/alancting/php-microsoft-jwt/PHP%20Test?label=TEST&style=for-the-badge)](https://github.com/alancting/php-microsoft-jwt) [![Coverage Status](https://img.shields.io/coveralls/github/alancting/php-microsoft-jwt/master?style=for-the-badge)](https://coveralls.io/github/alancting/php-microsoft-jwt?branch=master) [![GitHub license](https://img.shields.io/github/license/alancting/php-microsoft-jwt?color=green&style=for-the-badge)](https://github.com/alancting/php-microsoft-jwt/blob/master/LICENCE) -[![firebase/php-jwt Version](https://img.shields.io/static/v1?label=firebase%2Fphp-jwt&message=5.2.0&color=blue&style=for-the-badge)](https://github.com/firebase/php-jwt/tree/v5.2.0) +[![firebase/php-jwt Version](https://img.shields.io/static/v1?label=firebase%2Fphp-jwt&message=5.2.1&color=blue&style=for-the-badge)](https://github.com/firebase/php-jwt/tree/v5.2.1) # php-microsoft-jwt diff --git a/composer.json b/composer.json index 6dcd63c9..dddeee90 100644 --- a/composer.json +++ b/composer.json @@ -11,7 +11,7 @@ "ad", "microsoft" ], - "version": "1.1.0", + "version": "1.2.0", "authors": [ { "name": "Neuman Vong", diff --git a/src/JWK.php b/src/JWK.php index d18546cf..f19b1d2d 100644 --- a/src/JWK.php +++ b/src/JWK.php @@ -5,7 +5,6 @@ use DomainException; use InvalidArgumentException; use UnexpectedValueException; -use InvalidArgumentException; /** * JSON Web Key implementation, based on this spec: From 5b6c69017b9b6393e510c1ef66becf2c7426472b Mon Sep 17 00:00:00 2001 From: Alan Si Date: Mon, 15 Mar 2021 19:25:02 +0800 Subject: [PATCH 9/9] Conflixt fix --- src/JWK.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/JWK.php b/src/JWK.php index d18546cf..f19b1d2d 100644 --- a/src/JWK.php +++ b/src/JWK.php @@ -5,7 +5,6 @@ use DomainException; use InvalidArgumentException; use UnexpectedValueException; -use InvalidArgumentException; /** * JSON Web Key implementation, based on this spec: