From 2634afed889eea33eefd24629d21cadc09e80818 Mon Sep 17 00:00:00 2001 From: Andrew Dolgov Date: Sun, 24 Jul 2022 14:48:21 +0300 Subject: initial --- .../.github/PULL_REQUEST_TEMPLATE.md | 2 + .../openid-connect-php/.github/workflows/build.yml | 38 + vendor/jumbojett/openid-connect-php/.gitignore | 4 + vendor/jumbojett/openid-connect-php/CHANGELOG.md | 161 ++ vendor/jumbojett/openid-connect-php/LICENSE | 201 ++ vendor/jumbojett/openid-connect-php/README.md | 168 ++ .../openid-connect-php/client_example.php | 56 + vendor/jumbojett/openid-connect-php/composer.json | 24 + .../jumbojett/openid-connect-php/phpunit.xml.dist | 28 + .../openid-connect-php/src/OpenIDConnectClient.php | 1945 ++++++++++++++++++++ .../tests/OpenIDConnectClientTest.php | 65 + .../tests/TokenVerificationTest.php | 34 + .../openid-connect-php/tests/data/jwks-ps256.json | 12 + 13 files changed, 2738 insertions(+) create mode 100644 vendor/jumbojett/openid-connect-php/.github/PULL_REQUEST_TEMPLATE.md create mode 100644 vendor/jumbojett/openid-connect-php/.github/workflows/build.yml create mode 100644 vendor/jumbojett/openid-connect-php/.gitignore create mode 100644 vendor/jumbojett/openid-connect-php/CHANGELOG.md create mode 100644 vendor/jumbojett/openid-connect-php/LICENSE create mode 100644 vendor/jumbojett/openid-connect-php/README.md create mode 100644 vendor/jumbojett/openid-connect-php/client_example.php create mode 100644 vendor/jumbojett/openid-connect-php/composer.json create mode 100644 vendor/jumbojett/openid-connect-php/phpunit.xml.dist create mode 100644 vendor/jumbojett/openid-connect-php/src/OpenIDConnectClient.php create mode 100644 vendor/jumbojett/openid-connect-php/tests/OpenIDConnectClientTest.php create mode 100644 vendor/jumbojett/openid-connect-php/tests/TokenVerificationTest.php create mode 100644 vendor/jumbojett/openid-connect-php/tests/data/jwks-ps256.json (limited to 'vendor/jumbojett') diff --git a/vendor/jumbojett/openid-connect-php/.github/PULL_REQUEST_TEMPLATE.md b/vendor/jumbojett/openid-connect-php/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..d62f378 --- /dev/null +++ b/vendor/jumbojett/openid-connect-php/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,2 @@ +**List of common tasks a pull request require complete** +- [ ] Changelog entry is added or the pull request don't alter library's functionality diff --git a/vendor/jumbojett/openid-connect-php/.github/workflows/build.yml b/vendor/jumbojett/openid-connect-php/.github/workflows/build.yml new file mode 100644 index 0000000..2033968 --- /dev/null +++ b/vendor/jumbojett/openid-connect-php/.github/workflows/build.yml @@ -0,0 +1,38 @@ +--- +name: build + +on: [push, pull_request] + +env: + DEFAULT_COMPOSER_FLAGS: "--prefer-dist --no-interaction --no-progress --optimize-autoloader --ansi" + +jobs: + phpunit: + name: PHP ${{ matrix.php }} on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest] + php: ['5.5', '5.6', '7.0', '7.1', '7.2', '7.3', '7.4', '8.0', '8.1'] + + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + - name: Get composer cache directory + id: composer-cache + run: echo "::set-output name=dir::$(composer config cache-files-dir)" + - name: Cache composer dependencies + uses: actions/cache@v1 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: ${{ runner.os }}-composer- + - name: Install dependencies + run: composer update $DEFAULT_COMPOSER_FLAGS + - name: Run unit tests + run: vendor/bin/phpunit --verbose --colors=always tests diff --git a/vendor/jumbojett/openid-connect-php/.gitignore b/vendor/jumbojett/openid-connect-php/.gitignore new file mode 100644 index 0000000..e55efdf --- /dev/null +++ b/vendor/jumbojett/openid-connect-php/.gitignore @@ -0,0 +1,4 @@ +/.idea +/vendor +/composer.lock +.phpunit.result.cache diff --git a/vendor/jumbojett/openid-connect-php/CHANGELOG.md b/vendor/jumbojett/openid-connect-php/CHANGELOG.md new file mode 100644 index 0000000..e0f0910 --- /dev/null +++ b/vendor/jumbojett/openid-connect-php/CHANGELOG.md @@ -0,0 +1,161 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/) +and this project adheres to [Semantic Versioning](http://semver.org/). + +## [unreleased] + +## [0.9.7] + +### Added + +* Support for Self-Contained JWTs. #308 +* Support for RFC8693 Token Exchange Request. #275 + +### Fixed + +* PHP 5.4 compatibility. #304 +* Use session_status(). #306 + +## [0.9.6] + +### Added + +* Support for [phpseclib/phpseclib](https://phpseclib.com/) version **3**. #260 +* Support client_secret on token endpoint with PKCE. #293 +* Added new parameter to `requestTokens()` to pass custom HTTP headers #297 + +### Changed + +* Allow serializing `OpenIDConnectClient` using `serialize()` #295 + +## [0.9.5] + +### Changed + +* signOut() Method parameter $accessToken -> $idToken to prevent confusion about access and id tokens usage. #127 +* Fixed issue where missing nonce within the claims was causing an exception. #280 + +## [0.9.4] + +### Added + +* Enabled `client_secret_basic` authentication on `refreshToken()` #215 +* Basic auth support for requestResourceOwnerToken #271 + +## [0.9.3] + +### Added + +* getRedirectURL() will not log a warning for PHP 7.1+ #179 +* it is now possible to disable upgrading from HTTP to HTTPS for development purposes by calling `setHttpUpgradeInsecureRequests(false)` #241 +* bugfix in getSessionKey when _SESSION key does not exist #251 +* Added scope parameter to refresh token request #225 +* bugfix in verifyJWTclaims when $accessToken is empty and $claims->at_hash is not #276 +* bugfix with the `empty` function in PHP 5.4 #267 + +## [0.9.2] + +### Added +* Support for [PKCE](https://tools.ietf.org/html/rfc7636). Currently the supported methods are 'plain' and 'S256'. + +## [0.9.1] + +### Added +* Add support for MS Azure Active Directory B2C user flows + +### Changed +* Fix at_hash verification #200 +* Getters for public parameters #204 +* Removed client ID query parameter when making a token request using Basic Auth +* Use of `random_bytes()` for token generation instead of `uniqid()`; polyfill for PHP < 7.0 provided. + +### Removed +* Removed explicit content-length header - caused issues with proxy servers + + +## [0.9.0] + +### Added +* php 7.4 deprecates array_key_exists on objects, use property_exists in getVerifiedClaims and requestUserInfo +* Adding a header to indicate JSON as the return type for userinfo endpoint #151 +* ~Updated OpenIDConnectClient to conditionally verify nonce #146~ +* Add possibility to change enc_type parameter for http_build_query #155 +* Adding OAuth 2.0 Token Introspection #156 +* Add optional parameters clientId/clientSecret for introspection #157 & #158 +* Adding OAuth 2.0 Token Revocation #160 +* Adding issuer validator #145 +* Adding signing algorithm PS256 #180 +* Check http status of request user info #186 +* URL encode clientId and clientSecret when using basic authentication, according to https://tools.ietf.org/html/rfc6749#section-2.3.1 #192 +* Adjust PHPDoc to state that null is also allowed #193 + +### Changed +* Bugfix/code cleanup #152 + * Cleanup PHPDoc #46e5b59 + * Replace unnecessary double quotes with single quotes #2a76b57 + * Use original function names instead of aliases #1f37892 + * Remove unnecessary default values #5ab801e + * Explicit declare field $redirectURL #9187c0b + * Remove unused code #1e65384 + * Fix indent #e9cdf56 + * Cleanup conditional code flow for better readability #107f3fb + * Added strict type comparisons #167 +* Bugfix: required `openid` scope was omitted when additional scopes were registered using `addScope` method. This resulted in failing OpenID process. + +## [0.8.0] + +### Added +* Fix `verifyJWTsignature()`: verify JWT to prevent php errors and warnings on invalid token + +### Changed +* Decouple session manipulation, it's allow use of other session libraries #134 +* Broaden version requirements of the phpseclib/phpseclib package. #144 + +## [0.7.0] + +### Added +* Add "license" field to composer.json #138 +* Ensure key_alg is set when getting key #139 +* Add option to send additional registration parameters like post_logout_redirect_uris. #140 + +### Changed +* disabled autoload for Crypt_RSA + makre refreshToken() method tolerant for errors #137 + +### Removed +* + +## [0.6.0] + +### Added +* Added five minutes leeway due to clock skew between openidconnect server and client. +* Fix save access_token from request in implicit flow authentication #129 +* verifyJWTsignature() method private -> public #126 +* Support for providers where provider/login URL is not the same as the issuer URL. #125 +* Support for providers that has a different login URL from the issuer URL, for instance Azure Active Directory. Here, the provider URL is on the format: https://login.windows.net/(tenant-id), while the issuer claim actually is on the format: https://sts.windows.net/(tenant-id). + +### Changed +* refreshToken method update #124 + +### Removed +* + +## [0.5.0] +## Added +* Implement Azure AD B2C Implicit Workflow + +## [0.4.1] +## Changed +* Documentation updates for include path. + +## [0.4] +### Added +* Timeout is configurable via setTimeout method. This addresses issue #94. +* Add the ability to authenticate using the Resource Owner flow (with or without the Client ID and ClientSecret). This addresses issue #98 +* Add support for HS256, HS512 and HS384 signatures +* Removed unused calls to $this->getProviderConfigValue("token_endpoint_… + +### Changed + +### Removed diff --git a/vendor/jumbojett/openid-connect-php/LICENSE b/vendor/jumbojett/openid-connect-php/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/vendor/jumbojett/openid-connect-php/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/jumbojett/openid-connect-php/README.md b/vendor/jumbojett/openid-connect-php/README.md new file mode 100644 index 0000000..6689f46 --- /dev/null +++ b/vendor/jumbojett/openid-connect-php/README.md @@ -0,0 +1,168 @@ +PHP OpenID Connect Basic Client +======================== +A simple library that allows an application to authenticate a user through the basic OpenID Connect flow. +This library hopes to encourage OpenID Connect use by making it simple enough for a developer with little knowledge of +the OpenID Connect protocol to setup authentication. + +A special thanks goes to Justin Richer and Amanda Anganes for their help and support of the protocol. + +# Requirements # + 1. PHP 5.4 or greater + 2. CURL extension + 3. JSON extension + +## Install ## + 1. Install library using composer +``` +composer require jumbojett/openid-connect-php +``` + 2. Include composer autoloader +```php +require __DIR__ . '/vendor/autoload.php'; +``` + +## Example 1: Basic Client ## + +```php +use Jumbojett\OpenIDConnectClient; + +$oidc = new OpenIDConnectClient('https://id.provider.com', + 'ClientIDHere', + 'ClientSecretHere'); +$oidc->setCertPath('/path/to/my.cert'); +$oidc->authenticate(); +$name = $oidc->requestUserInfo('given_name'); + +``` + +[See openid spec for available user attributes][1] + +## Example 2: Dynamic Registration ## + +```php +use Jumbojett\OpenIDConnectClient; + +$oidc = new OpenIDConnectClient("https://id.provider.com"); + +$oidc->register(); +$client_id = $oidc->getClientID(); +$client_secret = $oidc->getClientSecret(); + +// Be sure to add logic to store the client id and client secret +``` + +## Example 3: Network and Security ## +```php +// Configure a proxy +$oidc->setHttpProxy("http://my.proxy.com:80/"); + +// Configure a cert +$oidc->setCertPath("/path/to/my.cert"); +``` + +## Example 4: Request Client Credentials Token ## + +```php +use Jumbojett\OpenIDConnectClient; + +$oidc = new OpenIDConnectClient('https://id.provider.com', + 'ClientIDHere', + 'ClientSecretHere'); +$oidc->providerConfigParam(array('token_endpoint'=>'https://id.provider.com/connect/token')); +$oidc->addScope('my_scope'); + +// this assumes success (to validate check if the access_token property is there and a valid JWT) : +$clientCredentialsToken = $oidc->requestClientCredentialsToken()->access_token; + +``` + +## Example 5: Request Resource Owners Token (with client auth) ## + +```php +use Jumbojett\OpenIDConnectClient; + +$oidc = new OpenIDConnectClient('https://id.provider.com', + 'ClientIDHere', + 'ClientSecretHere'); +$oidc->providerConfigParam(array('token_endpoint'=>'https://id.provider.com/connect/token')); +$oidc->addScope('my_scope'); + +//Add username and password +$oidc->addAuthParam(array('username'=>'')); +$oidc->addAuthParam(array('password'=>'')); + +//Perform the auth and return the token (to validate check if the access_token property is there and a valid JWT) : +$token = $oidc->requestResourceOwnerToken(TRUE)->access_token; + +``` + +## Example 6: Basic client for implicit flow e.g. with Azure AD B2C (see http://openid.net/specs/openid-connect-core-1_0.html#ImplicitFlowAuth) ## + +```php +use Jumbojett\OpenIDConnectClient; + +$oidc = new OpenIDConnectClient('https://id.provider.com', + 'ClientIDHere', + 'ClientSecretHere'); +$oidc->setResponseTypes(array('id_token')); +$oidc->addScope(array('openid')); +$oidc->setAllowImplicitFlow(true); +$oidc->addAuthParam(array('response_mode' => 'form_post')); +$oidc->setCertPath('/path/to/my.cert'); +$oidc->authenticate(); +$sub = $oidc->getVerifiedClaims('sub'); + +``` + +## Example 7: Introspection of an access token (see https://tools.ietf.org/html/rfc7662) ## + +```php +use Jumbojett\OpenIDConnectClient; + +$oidc = new OpenIDConnectClient('https://id.provider.com', + 'ClientIDHere', + 'ClientSecretHere'); +$data = $oidc->introspectToken('an.access-token.as.given'); +if (!$data->active) { + // the token is no longer usable +} + +``` + +## Example 8: PKCE Client ## + +```php +use Jumbojett\OpenIDConnectClient; + +$oidc = new OpenIDConnectClient('https://id.provider.com', + 'ClientIDHere', + null); +$oidc->setCodeChallengeMethod('S256'); +$oidc->authenticate(); +$name = $oidc->requestUserInfo('given_name'); + +``` + + +## Development Environments ## +In some cases you may need to disable SSL security on your development systems. +Note: This is not recommended on production systems. + +```php +$oidc->setVerifyHost(false); +$oidc->setVerifyPeer(false); +``` + +Also, your local system might not support HTTPS, so you might disable upgrading to it: + +```php +$oidc->setHttpUpgradeInsecureRequests(false); +``` + +### Todo ### +- Dynamic registration does not support registration auth tokens and endpoints + + [1]: http://openid.net/specs/openid-connect-basic-1_0-15.html#id_res + +## Contributing ### + - All pull requests, once merged, should be added to the CHANGELOG.md file. diff --git a/vendor/jumbojett/openid-connect-php/client_example.php b/vendor/jumbojett/openid-connect-php/client_example.php new file mode 100644 index 0000000..1ab2551 --- /dev/null +++ b/vendor/jumbojett/openid-connect-php/client_example.php @@ -0,0 +1,56 @@ + + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + */ + +require __DIR__ . '/vendor/autoload.php'; + +use Jumbojett\OpenIDConnectClient; + +$oidc = new OpenIDConnectClient( + 'http://myproviderURL.com/', + 'ClientIDHere', + 'ClientSecretHere' +); + +$oidc->authenticate(); +$name = $oidc->requestUserInfo('given_name'); + +?> + + + + Example OpenID Connect Client Use + + + + +
+ Hello +
+ + + + diff --git a/vendor/jumbojett/openid-connect-php/composer.json b/vendor/jumbojett/openid-connect-php/composer.json new file mode 100644 index 0000000..6d218cc --- /dev/null +++ b/vendor/jumbojett/openid-connect-php/composer.json @@ -0,0 +1,24 @@ +{ + "name": "jumbojett/openid-connect-php", + "description": "Bare-bones OpenID Connect client", + "license": "Apache-2.0", + "require": { + "php": ">=5.4", + "phpseclib/phpseclib" : "~2.0 || ^3.0", + "ext-json": "*", + "ext-curl": "*", + "paragonie/random_compat": ">=2" + }, + "require-dev": { + "roave/security-advisories": "dev-master", + "yoast/phpunit-polyfills": "^1.0" + }, + "archive" : { + "exclude" : [ + ".*" + ] + }, + "autoload" : { + "classmap": [ "src/"] + } +} diff --git a/vendor/jumbojett/openid-connect-php/phpunit.xml.dist b/vendor/jumbojett/openid-connect-php/phpunit.xml.dist new file mode 100644 index 0000000..df8ef25 --- /dev/null +++ b/vendor/jumbojett/openid-connect-php/phpunit.xml.dist @@ -0,0 +1,28 @@ + + + + + ./tests + + + + + ./src + + ./vendor + ./tests + + + + diff --git a/vendor/jumbojett/openid-connect-php/src/OpenIDConnectClient.php b/vendor/jumbojett/openid-connect-php/src/OpenIDConnectClient.php new file mode 100644 index 0000000..6fd6d11 --- /dev/null +++ b/vendor/jumbojett/openid-connect-php/src/OpenIDConnectClient.php @@ -0,0 +1,1945 @@ + + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + */ + +namespace Jumbojett; + +/** + * + * JWT signature verification support by Jonathan Reed + * Licensed under the same license as the rest of this file. + * + * phpseclib is required to validate the signatures of some tokens. + * It can be downloaded from: http://phpseclib.sourceforge.net/ + */ + +if (!class_exists('\phpseclib3\Crypt\RSA') && !class_exists('\phpseclib\Crypt\RSA') && !class_exists('Crypt_RSA')) { + user_error('Unable to find phpseclib Crypt/RSA.php. Ensure phpseclib is installed and in include_path before you include this file'); +} + +/** + * A wrapper around base64_decode which decodes Base64URL-encoded data, + * which is not the same alphabet as base64. + * @param string $base64url + * @return bool|string + */ +function base64url_decode($base64url) { + return base64_decode(b64url2b64($base64url)); +} + +/** + * Per RFC4648, "base64 encoding with URL-safe and filename-safe + * alphabet". This just replaces characters 62 and 63. None of the + * reference implementations seem to restore the padding if necessary, + * but we'll do it anyway. + * @param string $base64url + * @return string + */ +function b64url2b64($base64url) { + // "Shouldn't" be necessary, but why not + $padding = strlen($base64url) % 4; + if ($padding > 0) { + $base64url .= str_repeat('=', 4 - $padding); + } + return strtr($base64url, '-_', '+/'); +} + + +/** + * OpenIDConnect Exception Class + */ +class OpenIDConnectClientException extends \Exception +{ + +} + +/** + * Require the CURL and JSON PHP extensions to be installed + */ +if (!function_exists('curl_init')) { + throw new OpenIDConnectClientException('OpenIDConnect needs the CURL PHP extension.'); +} +if (!function_exists('json_decode')) { + throw new OpenIDConnectClientException('OpenIDConnect needs the JSON PHP extension.'); +} + +/** + * + * Please note this class stores nonces by default in $_SESSION['openid_connect_nonce'] + * + */ +class OpenIDConnectClient +{ + + /** + * @var string arbitrary id value + */ + private $clientID; + + /** + * @var string arbitrary name value + */ + private $clientName; + + /** + * @var string arbitrary secret value + */ + private $clientSecret; + + /** + * @var array holds the provider configuration + */ + private $providerConfig = []; + + /** + * @var string http proxy if necessary + */ + private $httpProxy; + + /** + * @var string full system path to the SSL certificate + */ + private $certPath; + + /** + * @var bool Verify SSL peer on transactions + */ + private $verifyPeer = true; + + /** + * @var bool Verify peer hostname on transactions + */ + private $verifyHost = true; + + /** + * @var string if we acquire an access token it will be stored here + */ + protected $accessToken; + + /** + * @var string if we acquire a refresh token it will be stored here + */ + private $refreshToken; + + /** + * @var string if we acquire an id token it will be stored here + */ + protected $idToken; + + /** + * @var string stores the token response + */ + private $tokenResponse; + + /** + * @var array holds scopes + */ + private $scopes = []; + + /** + * @var int|null Response code from the server + */ + private $responseCode; + + /** + * @var array holds response types + */ + private $responseTypes = []; + + /** + * @var array holds a cache of info returned from the user info endpoint + */ + private $userInfo = []; + + /** + * @var array holds authentication parameters + */ + private $authParams = []; + + /** + * @var array holds additional registration parameters for example post_logout_redirect_uris + */ + private $registrationParams = []; + + /** + * @var mixed holds well-known openid server properties + */ + private $wellKnown = false; + + /** + * @var mixed holds well-known opendid configuration parameters, like policy for MS Azure AD B2C User Flow + * @see https://docs.microsoft.com/en-us/azure/active-directory-b2c/user-flow-overview + */ + private $wellKnownConfigParameters = []; + + /** + * @var int timeout (seconds) + */ + protected $timeOut = 60; + + /** + * @var int leeway (seconds) + */ + private $leeway = 300; + + /** + * @var array holds response types + */ + private $additionalJwks = []; + + /** + * @var array holds verified jwt claims + */ + protected $verifiedClaims = []; + + /** + * @var callable|null validator function for issuer claim + */ + private $issuerValidator; + + /** + * @var bool Allow OAuth 2 implicit flow; see http://openid.net/specs/openid-connect-core-1_0.html#ImplicitFlowAuth + */ + private $allowImplicitFlow = false; + + /** + * @var string + */ + private $redirectURL; + + /** + * @var int defines which URL-encoding http_build_query() uses + */ + protected $encType = PHP_QUERY_RFC1738; + + /** + * @var bool Enable or disable upgrading to HTTPS by paying attention to HTTP header HTTP_UPGRADE_INSECURE_REQUESTS + */ + protected $httpUpgradeInsecureRequests = true; + + /** + * @var string holds code challenge method for PKCE mode + * @see https://tools.ietf.org/html/rfc7636 + */ + private $codeChallengeMethod = false; + + /** + * @var array holds PKCE supported algorithms + */ + private $pkceAlgs = ['S256' => 'sha256', 'plain' => false]; + + /** + * @param $provider_url string optional + * + * @param $client_id string optional + * @param $client_secret string optional + * @param null $issuer + */ + public function __construct($provider_url = null, $client_id = null, $client_secret = null, $issuer = null) { + $this->setProviderURL($provider_url); + if ($issuer === null) { + $this->setIssuer($provider_url); + } else { + $this->setIssuer($issuer); + } + + $this->clientID = $client_id; + $this->clientSecret = $client_secret; + } + + /** + * @param $provider_url + */ + public function setProviderURL($provider_url) { + $this->providerConfig['providerUrl'] = $provider_url; + } + + /** + * @param $issuer + */ + public function setIssuer($issuer) { + $this->providerConfig['issuer'] = $issuer; + } + + /** + * @param $response_types + */ + public function setResponseTypes($response_types) { + $this->responseTypes = array_merge($this->responseTypes, (array)$response_types); + } + + /** + * @return bool + * @throws OpenIDConnectClientException + */ + public function authenticate() { + + // Do a preemptive check to see if the provider has thrown an error from a previous redirect + if (isset($_REQUEST['error'])) { + $desc = isset($_REQUEST['error_description']) ? ' Description: ' . $_REQUEST['error_description'] : ''; + throw new OpenIDConnectClientException('Error: ' . $_REQUEST['error'] .$desc); + } + + // If we have an authorization code then proceed to request a token + if (isset($_REQUEST['code'])) { + + $code = $_REQUEST['code']; + $token_json = $this->requestTokens($code); + + // Throw an error if the server returns one + if (isset($token_json->error)) { + if (isset($token_json->error_description)) { + throw new OpenIDConnectClientException($token_json->error_description); + } + throw new OpenIDConnectClientException('Got response: ' . $token_json->error); + } + + // Do an OpenID Connect session check + if ($_REQUEST['state'] !== $this->getState()) { + throw new OpenIDConnectClientException('Unable to determine state'); + } + + // Cleanup state + $this->unsetState(); + + if (!property_exists($token_json, 'id_token')) { + throw new OpenIDConnectClientException('User did not authorize openid scope.'); + } + + $claims = $this->decodeJWT($token_json->id_token, 1); + + // Verify the signature + if ($this->canVerifySignatures()) { + if (!$this->getProviderConfigValue('jwks_uri')) { + throw new OpenIDConnectClientException ('Unable to verify signature due to no jwks_uri being defined'); + } + if (!$this->verifyJWTsignature($token_json->id_token)) { + throw new OpenIDConnectClientException ('Unable to verify signature'); + } + } else { + user_error('Warning: JWT signature verification unavailable.'); + } + + // Save the id token + $this->idToken = $token_json->id_token; + + // Save the access token + $this->accessToken = $token_json->access_token; + + // If this is a valid claim + if ($this->verifyJWTclaims($claims, $token_json->access_token)) { + + // Clean up the session a little + $this->unsetNonce(); + + // Save the full response + $this->tokenResponse = $token_json; + + // Save the verified claims + $this->verifiedClaims = $claims; + + // Save the refresh token, if we got one + if (isset($token_json->refresh_token)) { + $this->refreshToken = $token_json->refresh_token; + } + + // Success! + return true; + + } + + throw new OpenIDConnectClientException ('Unable to verify JWT claims'); + } + + if ($this->allowImplicitFlow && isset($_REQUEST['id_token'])) { + // if we have no code but an id_token use that + $id_token = $_REQUEST['id_token']; + + $accessToken = null; + if (isset($_REQUEST['access_token'])) { + $accessToken = $_REQUEST['access_token']; + } + + // Do an OpenID Connect session check + if ($_REQUEST['state'] !== $this->getState()) { + throw new OpenIDConnectClientException('Unable to determine state'); + } + + // Cleanup state + $this->unsetState(); + + $claims = $this->decodeJWT($id_token, 1); + + // Verify the signature + if ($this->canVerifySignatures()) { + if (!$this->getProviderConfigValue('jwks_uri')) { + throw new OpenIDConnectClientException ('Unable to verify signature due to no jwks_uri being defined'); + } + if (!$this->verifyJWTsignature($id_token)) { + throw new OpenIDConnectClientException ('Unable to verify signature'); + } + } else { + user_error('Warning: JWT signature verification unavailable.'); + } + + // Save the id token + $this->idToken = $id_token; + + // If this is a valid claim + if ($this->verifyJWTclaims($claims, $accessToken)) { + + // Clean up the session a little + $this->unsetNonce(); + + // Save the verified claims + $this->verifiedClaims = $claims; + + // Save the access token + if ($accessToken) { + $this->accessToken = $accessToken; + } + + // Success! + return true; + + } + + throw new OpenIDConnectClientException ('Unable to verify JWT claims'); + } + + $this->requestAuthorization(); + return false; + } + + /** + * It calls the end-session endpoint of the OpenID Connect provider to notify the OpenID + * Connect provider that the end-user has logged out of the relying party site + * (the client application). + * + * @param string $idToken ID token (obtained at login) + * @param string|null $redirect URL to which the RP is requesting that the End-User's User Agent + * be redirected after a logout has been performed. The value MUST have been previously + * registered with the OP. Value can be null. + * + * @throws OpenIDConnectClientException + */ + public function signOut($idToken, $redirect) { + $signout_endpoint = $this->getProviderConfigValue('end_session_endpoint'); + + $signout_params = null; + if($redirect === null){ + $signout_params = ['id_token_hint' => $idToken]; + } + else { + $signout_params = [ + 'id_token_hint' => $idToken, + 'post_logout_redirect_uri' => $redirect]; + } + + $signout_endpoint .= (strpos($signout_endpoint, '?') === false ? '?' : '&') . http_build_query( $signout_params, '', '&', $this->encType); + $this->redirect($signout_endpoint); + } + + /** + * @param array $scope - example: openid, given_name, etc... + */ + public function addScope($scope) { + $this->scopes = array_merge($this->scopes, (array)$scope); + } + + /** + * @param array $param - example: prompt=login + */ + public function addAuthParam($param) { + $this->authParams = array_merge($this->authParams, (array)$param); + } + + /** + * @param array $param - example: post_logout_redirect_uris=[http://example.com/successful-logout] + */ + public function addRegistrationParam($param) { + $this->registrationParams = array_merge($this->registrationParams, (array)$param); + } + + /** + * @param $jwk object - example: (object) ['kid' => ..., 'nbf' => ..., 'use' => 'sig', 'kty' => "RSA", 'e' => "", 'n' => ""] + */ + protected function addAdditionalJwk($jwk) { + $this->additionalJwks[] = $jwk; + } + + /** + * Get's anything that we need configuration wise including endpoints, and other values + * + * @param string $param + * @param string $default optional + * @throws OpenIDConnectClientException + * @return string + * + */ + protected function getProviderConfigValue($param, $default = null) { + + // If the configuration value is not available, attempt to fetch it from a well known config endpoint + // This is also known as auto "discovery" + if (!isset($this->providerConfig[$param])) { + $this->providerConfig[$param] = $this->getWellKnownConfigValue($param, $default); + } + + return $this->providerConfig[$param]; + } + + /** + * Get's anything that we need configuration wise including endpoints, and other values + * + * @param string $param + * @param string $default optional + * @throws OpenIDConnectClientException + * @return string + * + */ + private function getWellKnownConfigValue($param, $default = null) { + + // If the configuration value is not available, attempt to fetch it from a well known config endpoint + // This is also known as auto "discovery" + if(!$this->wellKnown) { + $well_known_config_url = rtrim($this->getProviderURL(), '/') . '/.well-known/openid-configuration'; + if (count($this->wellKnownConfigParameters) > 0){ + $well_known_config_url .= '?' . http_build_query($this->wellKnownConfigParameters) ; + } + $this->wellKnown = json_decode($this->fetchURL($well_known_config_url)); + } + + $value = false; + if(isset($this->wellKnown->{$param})){ + $value = $this->wellKnown->{$param}; + } + + if ($value) { + return $value; + } + + if (isset($default)) { + // Uses default value if provided + return $default; + } + + throw new OpenIDConnectClientException("The provider {$param} could not be fetched. Make sure your provider has a well known configuration available."); + } + + /** + * Set optionnal parameters for .well-known/openid-configuration + * + * @param string $param + * + */ + public function setWellKnownConfigParameters(array $params = []){ + $this->wellKnownConfigParameters=$params; + } + + + /** + * @param string $url Sets redirect URL for auth flow + */ + public function setRedirectURL ($url) { + if (parse_url($url,PHP_URL_HOST) !== false) { + $this->redirectURL = $url; + } + } + + /** + * Gets the URL of the current page we are on, encodes, and returns it + * + * @return string + */ + public function getRedirectURL() { + + // If the redirect URL has been set then return it. + if (property_exists($this, 'redirectURL') && $this->redirectURL) { + return $this->redirectURL; + } + + // Other-wise return the URL of the current page + + /** + * Thank you + * http://stackoverflow.com/questions/189113/how-do-i-get-current-page-full-url-in-php-on-a-windows-iis-server + */ + + /* + * Compatibility with multiple host headers. + * The problem with SSL over port 80 is resolved and non-SSL over port 443. + * Support of 'ProxyReverse' configurations. + */ + + if ($this->httpUpgradeInsecureRequests && isset($_SERVER['HTTP_UPGRADE_INSECURE_REQUESTS']) && ($_SERVER['HTTP_UPGRADE_INSECURE_REQUESTS'] === '1')) { + $protocol = 'https'; + } elseif (isset($_SERVER['HTTP_X_FORWARDED_PROTO'])) { + $protocol = $_SERVER['HTTP_X_FORWARDED_PROTO']; + } elseif (isset($_SERVER['REQUEST_SCHEME'])) { + $protocol = $_SERVER['REQUEST_SCHEME']; + } elseif (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') { + $protocol = 'https'; + } else { + $protocol = 'http'; + } + + if (isset($_SERVER['HTTP_X_FORWARDED_PORT'])) { + $port = intval($_SERVER['HTTP_X_FORWARDED_PORT']); + } elseif (isset($_SERVER['SERVER_PORT'])) { + $port = intval($_SERVER['SERVER_PORT']); + } elseif ($protocol === 'https') { + $port = 443; + } else { + $port = 80; + } + + if (isset($_SERVER['HTTP_HOST'])) { + $host = explode(':', $_SERVER['HTTP_HOST'])[0]; + } elseif (isset($_SERVER['SERVER_NAME'])) { + $host = $_SERVER['SERVER_NAME']; + } elseif (isset($_SERVER['SERVER_ADDR'])) { + $host = $_SERVER['SERVER_ADDR']; + } else { + return 'http:///'; + } + + $port = (443 === $port) || (80 === $port) ? '' : ':' . $port; + + $explodedRequestUri = isset($_SERVER['REQUEST_URI']) ? explode('?', $_SERVER['REQUEST_URI']) : []; + return sprintf('%s://%s%s/%s', $protocol, $host, $port, trim(reset($explodedRequestUri), '/')); + } + + /** + * Used for arbitrary value generation for nonces and state + * + * @return string + * @throws OpenIDConnectClientException + */ + protected function generateRandString() { + // Error and Exception need to be catched in this order, see https://github.com/paragonie/random_compat/blob/master/README.md + // random_compat polyfill library should be removed if support for PHP versions < 7 is dropped + try { + return \bin2hex(\random_bytes(16)); + } catch (Error $e) { + throw new OpenIDConnectClientException('Random token generation failed.'); + } catch (Exception $e) { + throw new OpenIDConnectClientException('Random token generation failed.'); + }; + } + + /** + * Start Here + * @return void + * @throws OpenIDConnectClientException + */ + private function requestAuthorization() { + + $auth_endpoint = $this->getProviderConfigValue('authorization_endpoint'); + $response_type = 'code'; + + // Generate and store a nonce in the session + // The nonce is an arbitrary value + $nonce = $this->setNonce($this->generateRandString()); + + // State essentially acts as a session key for OIDC + $state = $this->setState($this->generateRandString()); + + $auth_params = array_merge($this->authParams, [ + 'response_type' => $response_type, + 'redirect_uri' => $this->getRedirectURL(), + 'client_id' => $this->clientID, + 'nonce' => $nonce, + 'state' => $state, + 'scope' => 'openid' + ]); + + // If the client has been registered with additional scopes + if (count($this->scopes) > 0) { + $auth_params = array_merge($auth_params, ['scope' => implode(' ', array_merge($this->scopes, ['openid']))]); + } + + // If the client has been registered with additional response types + if (count($this->responseTypes) > 0) { + $auth_params = array_merge($auth_params, ['response_type' => implode(' ', $this->responseTypes)]); + } + + // If the client supports Proof Key for Code Exchange (PKCE) + $ccm = $this->getCodeChallengeMethod(); + if (!empty($ccm) && in_array($this->getCodeChallengeMethod(), $this->getProviderConfigValue('code_challenge_methods_supported'))) { + $codeVerifier = bin2hex(random_bytes(64)); + $this->setCodeVerifier($codeVerifier); + if (!empty($this->pkceAlgs[$this->getCodeChallengeMethod()])) { + $codeChallenge = rtrim(strtr(base64_encode(hash($this->pkceAlgs[$this->getCodeChallengeMethod()], $codeVerifier, true)), '+/', '-_'), '='); + } else { + $codeChallenge = $codeVerifier; + } + $auth_params = array_merge($auth_params, [ + 'code_challenge' => $codeChallenge, + 'code_challenge_method' => $this->getCodeChallengeMethod() + ]); + } + + $auth_endpoint .= (strpos($auth_endpoint, '?') === false ? '?' : '&') . http_build_query($auth_params, '', '&', $this->encType); + + $this->commitSession(); + $this->redirect($auth_endpoint); + } + + /** + * Requests a client credentials token + * + * @throws OpenIDConnectClientException + */ + public function requestClientCredentialsToken() { + $token_endpoint = $this->getProviderConfigValue('token_endpoint'); + + $headers = []; + + $grant_type = 'client_credentials'; + + $post_data = [ + 'grant_type' => $grant_type, + 'client_id' => $this->clientID, + 'client_secret' => $this->clientSecret, + 'scope' => implode(' ', $this->scopes) + ]; + + // Convert token params to string format + $post_params = http_build_query($post_data, '', '&', $this->encType); + + return json_decode($this->fetchURL($token_endpoint, $post_params, $headers)); + } + + /** + * Requests a resource owner token + * (Defined in https://tools.ietf.org/html/rfc6749#section-4.3) + * + * @param boolean $bClientAuth Indicates that the Client ID and Secret be used for client authentication + * @return mixed + * @throws OpenIDConnectClientException + */ + public function requestResourceOwnerToken($bClientAuth = FALSE) { + $token_endpoint = $this->getProviderConfigValue('token_endpoint'); + + $headers = []; + + $grant_type = 'password'; + + $post_data = [ + 'grant_type' => $grant_type, + 'username' => $this->authParams['username'], + 'password' => $this->authParams['password'], + 'scope' => implode(' ', $this->scopes) + ]; + + //For client authentication include the client values + if($bClientAuth) { + $token_endpoint_auth_methods_supported = $this->getProviderConfigValue('token_endpoint_auth_methods_supported', ['client_secret_basic']); + if (in_array('client_secret_basic', $token_endpoint_auth_methods_supported, true)) { + $headers = ['Authorization: Basic ' . base64_encode(urlencode($this->clientID) . ':' . urlencode($this->clientSecret))]; + } else { + $post_data['client_id'] = $this->clientID; + $post_data['client_secret'] = $this->clientSecret; + } + } + + // Convert token params to string format + $post_params = http_build_query($post_data, '', '&', $this->encType); + + return json_decode($this->fetchURL($token_endpoint, $post_params, $headers)); + } + + + /** + * Requests ID and Access tokens + * + * @param string $code + * @param string[] $headers Extra HTTP headers to pass to the token endpoint + * @return mixed + * @throws OpenIDConnectClientException + */ + protected function requestTokens($code, $headers = array()) { + $token_endpoint = $this->getProviderConfigValue('token_endpoint'); + $token_endpoint_auth_methods_supported = $this->getProviderConfigValue('token_endpoint_auth_methods_supported', ['client_secret_basic']); + + $grant_type = 'authorization_code'; + + $token_params = [ + 'grant_type' => $grant_type, + 'code' => $code, + 'redirect_uri' => $this->getRedirectURL(), + 'client_id' => $this->clientID, + 'client_secret' => $this->clientSecret + ]; + + $authorizationHeader = null; + # Consider Basic authentication if provider config is set this way + if (in_array('client_secret_basic', $token_endpoint_auth_methods_supported, true)) { + $authorizationHeader = 'Authorization: Basic ' . base64_encode(urlencode($this->clientID) . ':' . urlencode($this->clientSecret)); + unset($token_params['client_secret']); + unset($token_params['client_id']); + } + + $ccm = $this->getCodeChallengeMethod(); + $cv = $this->getCodeVerifier(); + if (!empty($ccm) && !empty($cv)) { + $cs = $this->getClientSecret(); + if (empty($cs)) { + $authorizationHeader = null; + unset($token_params['client_secret']); + } + $token_params = array_merge($token_params, [ + 'client_id' => $this->clientID, + 'code_verifier' => $this->getCodeVerifier() + ]); + } + + // Convert token params to string format + $token_params = http_build_query($token_params, '', '&', $this->encType); + + if (null !== $authorizationHeader) { + $headers[] = $authorizationHeader; + } + + $this->tokenResponse = json_decode($this->fetchURL($token_endpoint, $token_params, $headers)); + + return $this->tokenResponse; + } + + /** + * Request RFC8693 Token Exchange + * https://datatracker.ietf.org/doc/html/rfc8693 + * + * @param string $subjectToken + * @param string $subjectTokenType + * @param string $audience + * @return mixed + * @throws OpenIDConnectClientException + */ + public function requestTokenExchange($subjectToken, $subjectTokenType, $audience = '') { + $token_endpoint = $this->getProviderConfigValue('token_endpoint'); + $token_endpoint_auth_methods_supported = $this->getProviderConfigValue('token_endpoint_auth_methods_supported', ['client_secret_basic']); + $headers = []; + $grant_type = 'urn:ietf:params:oauth:grant-type:token-exchange'; + + $post_data = array( + 'grant_type' => $grant_type, + 'subject_token_type' => $subjectTokenType, + 'subject_token' => $subjectToken, + 'client_id' => $this->clientID, + 'client_secret' => $this->clientSecret, + 'scope' => implode(' ', $this->scopes) + ); + + if (!empty($audience)) { + $post_data['audience'] = $audience; + } + + # Consider Basic authentication if provider config is set this way + if (in_array('client_secret_basic', $token_endpoint_auth_methods_supported, true)) { + $headers = ['Authorization: Basic ' . base64_encode(urlencode($this->clientID) . ':' . urlencode($this->clientSecret))]; + unset($post_data['client_secret']); + unset($post_data['client_id']); + } + + // Convert token params to string format + $post_params = http_build_query($post_data, null, '&', $this->enc_type); + + return json_decode($this->fetchURL($token_endpoint, $post_params, $headers)); + } + + + /** + * Requests Access token with refresh token + * + * @param string $refresh_token + * @return mixed + * @throws OpenIDConnectClientException + */ + public function refreshToken($refresh_token) { + $token_endpoint = $this->getProviderConfigValue('token_endpoint'); + $token_endpoint_auth_methods_supported = $this->getProviderConfigValue('token_endpoint_auth_methods_supported', ['client_secret_basic']); + + $headers = []; + + $grant_type = 'refresh_token'; + + $token_params = [ + 'grant_type' => $grant_type, + 'refresh_token' => $refresh_token, + 'client_id' => $this->clientID, + 'client_secret' => $this->clientSecret, + 'scope' => implode(' ', $this->scopes), + ]; + + # Consider Basic authentication if provider config is set this way + if (in_array('client_secret_basic', $token_endpoint_auth_methods_supported, true)) { + $headers = ['Authorization: Basic ' . base64_encode(urlencode($this->clientID) . ':' . urlencode($this->clientSecret))]; + unset($token_params['client_secret']); + unset($token_params['client_id']); + } + + // Convert token params to string format + $token_params = http_build_query($token_params, '', '&', $this->encType); + + $json = json_decode($this->fetchURL($token_endpoint, $token_params, $headers)); + + if (isset($json->access_token)) { + $this->accessToken = $json->access_token; + } + + if (isset($json->refresh_token)) { + $this->refreshToken = $json->refresh_token; + } + + return $json; + } + + /** + * @param array $keys + * @param array $header + * @throws OpenIDConnectClientException + * @return object + */ + private function getKeyForHeader($keys, $header) { + foreach ($keys as $key) { + if ($key->kty === 'RSA') { + if (!isset($header->kid) || $key->kid === $header->kid) { + return $key; + } + } else { + if (isset($key->alg) && $key->alg === $header->alg && $key->kid === $header->kid) { + return $key; + } + } + } + if ($this->additionalJwks) { + foreach ($this->additionalJwks as $key) { + if ($key->kty === 'RSA') { + if (!isset($header->kid) || $key->kid === $header->kid) { + return $key; + } + } else { + if (isset($key->alg) && $key->alg === $header->alg && $key->kid === $header->kid) { + return $key; + } + } + } + } + if (isset($header->kid)) { + throw new OpenIDConnectClientException('Unable to find a key for (algorithm, kid):' . $header->alg . ', ' . $header->kid . ')'); + } + + throw new OpenIDConnectClientException('Unable to find a key for RSA'); + } + + + /** + * @param string $hashtype + * @param object $key + * @param $payload + * @param $signature + * @param $signatureType + * @return bool + * @throws OpenIDConnectClientException + */ + private function verifyRSAJWTsignature($hashtype, $key, $payload, $signature, $signatureType) { + if (!class_exists('\phpseclib3\Crypt\RSA') && !class_exists('\phpseclib\Crypt\RSA') && !class_exists('Crypt_RSA')) { + throw new OpenIDConnectClientException('Crypt_RSA support unavailable.'); + } + if (!(property_exists($key, 'n') && property_exists($key, 'e'))) { + throw new OpenIDConnectClientException('Malformed key object'); + } + + /* We already have base64url-encoded data, so re-encode it as + regular base64 and use the XML key format for simplicity. + */ + $public_key_xml = "\r\n". + ' ' . b64url2b64($key->n) . "\r\n" . + ' ' . b64url2b64($key->e) . "\r\n" . + ''; + if (class_exists('\phpseclib3\Crypt\RSA', false)) { + $key = \phpseclib3\Crypt\PublicKeyLoader::load($public_key_xml) + ->withHash($hashtype); + if ($signatureType === 'PSS') { + $key = $key->withMGFHash($hashtype) + ->withPadding(\phpseclib3\Crypt\RSA::SIGNATURE_PSS); + } else { + $key = $key->withPadding(\phpseclib3\Crypt\RSA::SIGNATURE_PKCS1); + } + return $key->verify($payload, $signature); + } elseif (class_exists('Crypt_RSA', false)) { + $rsa = new Crypt_RSA(); + $rsa->setHash($hashtype); + if ($signatureType === 'PSS') { + $rsa->setMGFHash($hashtype); + } + $rsa->loadKey($public_key_xml, Crypt_RSA::PUBLIC_FORMAT_XML); + $rsa->setSignatureMode($signatureType === 'PSS' ? Crypt_RSA::SIGNATURE_PSS : Crypt_RSA::SIGNATURE_PKCS1); + return $rsa->verify($payload, $signature); + } else { + $rsa = new \phpseclib\Crypt\RSA(); + $rsa->setHash($hashtype); + if ($signatureType === 'PSS') { + $rsa->setMGFHash($hashtype); + } + $rsa->loadKey($public_key_xml, \phpseclib\Crypt\RSA::PUBLIC_FORMAT_XML); + $rsa->setSignatureMode($signatureType === 'PSS' ? \phpseclib\Crypt\RSA::SIGNATURE_PSS : \phpseclib\Crypt\RSA::SIGNATURE_PKCS1); + return $rsa->verify($payload, $signature); + } + } + + /** + * @param string $hashtype + * @param object $key + * @param $payload + * @param $signature + * @return bool + * @throws OpenIDConnectClientException + */ + private function verifyHMACJWTsignature($hashtype, $key, $payload, $signature) + { + if (!function_exists('hash_hmac')) { + throw new OpenIDConnectClientException('hash_hmac support unavailable.'); + } + + $expected=hash_hmac($hashtype, $payload, $key, true); + + if (function_exists('hash_equals')) { + return hash_equals($signature, $expected); + } + + return self::hashEquals($signature, $expected); + } + + /** + * @param string $jwt encoded JWT + * @throws OpenIDConnectClientException + * @return bool + */ + public function verifyJWTsignature($jwt) { + if (!\is_string($jwt)) { + throw new OpenIDConnectClientException('Error token is not a string'); + } + $parts = explode('.', $jwt); + if (!isset($parts[0])) { + throw new OpenIDConnectClientException('Error missing part 0 in token'); + } + $signature = base64url_decode(array_pop($parts)); + if (false === $signature || '' === $signature) { + throw new OpenIDConnectClientException('Error decoding signature from token'); + } + $header = json_decode(base64url_decode($parts[0])); + if (null === $header || !\is_object($header)) { + throw new OpenIDConnectClientException('Error decoding JSON from token header'); + } + if (!isset($header->alg)) { + throw new OpenIDConnectClientException('Error missing signature type in token header'); + } + + $payload = implode('.', $parts); + switch ($header->alg) { + case 'RS256': + case 'PS256': + case 'RS384': + case 'RS512': + $hashtype = 'sha' . substr($header->alg, 2); + $signatureType = $header->alg === 'PS256' ? 'PSS' : ''; + + if (isset($header->jwk)) { + $jwk = $header->jwk; + } else { + $jwks = json_decode($this->fetchURL($this->getProviderConfigValue('jwks_uri'))); + if ($jwks === NULL) { + throw new OpenIDConnectClientException('Error decoding JSON from jwks_uri'); + } + $jwk = $this->getKeyForHeader($jwks->keys, $header); + } + + $verified = $this->verifyRSAJWTsignature($hashtype, + $jwk, + $payload, $signature, $signatureType); + break; + case 'HS256': + case 'HS512': + case 'HS384': + $hashtype = 'SHA' . substr($header->alg, 2); + $verified = $this->verifyHMACJWTsignature($hashtype, $this->getClientSecret(), $payload, $signature); + break; + default: + throw new OpenIDConnectClientException('No support for signature type: ' . $header->alg); + } + return $verified; + } + + /** + * @param string $iss + * @return bool + * @throws OpenIDConnectClientException + */ + protected function validateIssuer($iss) { + if ($this->issuerValidator !== null) { + return $this->issuerValidator->__invoke($iss); + } + + return ($iss === $this->getIssuer() || $iss === $this->getWellKnownIssuer() || $iss === $this->getWellKnownIssuer(true)); + } + + /** + * @param object $claims + * @param string|null $accessToken + * @return bool + */ + protected function verifyJWTclaims($claims, $accessToken = null) { + if(isset($claims->at_hash) && isset($accessToken)) { + if(isset($this->getIdTokenHeader()->alg) && $this->getIdTokenHeader()->alg !== 'none') { + $bit = substr($this->getIdTokenHeader()->alg, 2, 3); + } else { + // TODO: Error case. throw exception??? + $bit = '256'; + } + $len = ((int)$bit)/16; + $expected_at_hash = $this->urlEncode(substr(hash('sha'.$bit, $accessToken, true), 0, $len)); + } + return (($this->validateIssuer($claims->iss)) + && (($claims->aud === $this->clientID) || in_array($this->clientID, $claims->aud, true)) + && (!isset($claims->nonce) || $claims->nonce === $this->getNonce()) + && ( !isset($claims->exp) || ((gettype($claims->exp) === 'integer') && ($claims->exp >= time() - $this->leeway))) + && ( !isset($claims->nbf) || ((gettype($claims->nbf) === 'integer') && ($claims->nbf <= time() + $this->leeway))) + && ( !isset($claims->at_hash) || !isset($accessToken) || $claims->at_hash === $expected_at_hash ) + ); + } + + /** + * @param string $str + * @return string + */ + protected function urlEncode($str) { + $enc = base64_encode($str); + $enc = rtrim($enc, '='); + $enc = strtr($enc, '+/', '-_'); + return $enc; + } + + /** + * @param string $jwt encoded JWT + * @param int $section the section we would like to decode + * @return object + */ + protected function decodeJWT($jwt, $section = 0) { + + $parts = explode('.', $jwt); + return json_decode(base64url_decode($parts[$section])); + } + + /** + * + * @param string|null $attribute optional + * + * Attribute Type Description + * user_id string REQUIRED Identifier for the End-User at the Issuer. + * name string End-User's full name in displayable form including all name parts, ordered according to End-User's locale and preferences. + * given_name string Given name or first name of the End-User. + * family_name string Surname or last name of the End-User. + * middle_name string Middle name of the End-User. + * nickname string Casual name of the End-User that may or may not be the same as the given_name. For instance, a nickname value of Mike might be returned alongside a given_name value of Michael. + * profile string URL of End-User's profile page. + * picture string URL of the End-User's profile picture. + * website string URL of End-User's web page or blog. + * email string The End-User's preferred e-mail address. + * verified boolean True if the End-User's e-mail address has been verified; otherwise false. + * gender string The End-User's gender: Values defined by this specification are female and male. Other values MAY be used when neither of the defined values are applicable. + * birthday string The End-User's birthday, represented as a date string in MM/DD/YYYY format. The year MAY be 0000, indicating that it is omitted. + * zoneinfo string String from zoneinfo [zoneinfo] time zone database. For example, Europe/Paris or America/Los_Angeles. + * locale string The End-User's locale, represented as a BCP47 [RFC5646] language tag. This is typically an ISO 639-1 Alpha-2 [ISO639‑1] language code in lowercase and an ISO 3166-1 Alpha-2 [ISO3166‑1] country code in uppercase, separated by a dash. For example, en-US or fr-CA. As a compatibility note, some implementations have used an underscore as the separator rather than a dash, for example, en_US; Implementations MAY choose to accept this locale syntax as well. + * phone_number string The End-User's preferred telephone number. E.164 [E.164] is RECOMMENDED as the format of this Claim. For example, +1 (425) 555-1212 or +56 (2) 687 2400. + * address JSON object The End-User's preferred address. The value of the address member is a JSON [RFC4627] structure containing some or all of the members defined in Section 2.4.2.1. + * updated_time string Time the End-User's information was last updated, represented as a RFC 3339 [RFC3339] datetime. For example, 2011-01-03T23:58:42+0000. + * + * @return mixed + * + * @throws OpenIDConnectClientException + */ + public function requestUserInfo($attribute = null) { + + $user_info_endpoint = $this->getProviderConfigValue('userinfo_endpoint'); + $schema = 'openid'; + + $user_info_endpoint .= '?schema=' . $schema; + + //The accessToken has to be sent in the Authorization header. + // Accept json to indicate response type + $headers = ["Authorization: Bearer {$this->accessToken}", + 'Accept: application/json']; + + $user_json = json_decode($this->fetchURL($user_info_endpoint,null,$headers)); + if ($this->getResponseCode() <> 200) { + throw new OpenIDConnectClientException('The communication to retrieve user data has failed with status code '.$this->getResponseCode()); + } + $this->userInfo = $user_json; + + if($attribute === null) { + return $this->userInfo; + } + + if (property_exists($this->userInfo, $attribute)) { + return $this->userInfo->$attribute; + } + + return null; + } + + /** + * + * @param string|null $attribute optional + * + * Attribute Type Description + * exp int Expires at + * nbf int Not before + * ver string Version + * iss string Issuer + * sub string Subject + * aud string Audience + * nonce string nonce + * iat int Issued At + * auth_time int Authenatication time + * oid string Object id + * + * @return mixed + * + */ + public function getVerifiedClaims($attribute = null) { + + if($attribute === null) { + return $this->verifiedClaims; + } + + if (property_exists($this->verifiedClaims, $attribute)) { + return $this->verifiedClaims->$attribute; + } + + return null; + } + + /** + * @param string $url + * @param string | null $post_body string If this is set the post type will be POST + * @param array $headers Extra headers to be send with the request. Format as 'NameHeader: ValueHeader' + * @throws OpenIDConnectClientException + * @return mixed + */ + protected function fetchURL($url, $post_body = null, $headers = []) { + + // OK cool - then let's create a new cURL resource handle + $ch = curl_init(); + + // Determine whether this is a GET or POST + if ($post_body !== null) { + // curl_setopt($ch, CURLOPT_POST, 1); + // Alows to keep the POST method even after redirect + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST'); + curl_setopt($ch, CURLOPT_POSTFIELDS, $post_body); + + // Default content type is form encoded + $content_type = 'application/x-www-form-urlencoded'; + + // Determine if this is a JSON payload and add the appropriate content type + if (is_object(json_decode($post_body))) { + $content_type = 'application/json'; + } + + // Add POST-specific headers + $headers[] = "Content-Type: {$content_type}"; + + } + + // If we set some headers include them + if(count($headers) > 0) { + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + } + + // Set URL to download + curl_setopt($ch, CURLOPT_URL, $url); + + if (isset($this->httpProxy)) { + curl_setopt($ch, CURLOPT_PROXY, $this->httpProxy); + } + + // Include header in result? (0 = yes, 1 = no) + curl_setopt($ch, CURLOPT_HEADER, 0); + + // Allows to follow redirect + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); + + /** + * Set cert + * Otherwise ignore SSL peer verification + */ + if (isset($this->certPath)) { + curl_setopt($ch, CURLOPT_CAINFO, $this->certPath); + } + + if($this->verifyHost) { + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); + } else { + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0); + } + + if($this->verifyPeer) { + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); + } else { + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + } + + // Should cURL return or print out the data? (true = return, false = print) + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + + // Timeout in seconds + curl_setopt($ch, CURLOPT_TIMEOUT, $this->timeOut); + + // Download the given URL, and return output + $output = curl_exec($ch); + + // HTTP Response code from server may be required from subclass + $info = curl_getinfo($ch); + $this->responseCode = $info['http_code']; + + if ($output === false) { + throw new OpenIDConnectClientException('Curl error: (' . curl_errno($ch) . ') ' . curl_error($ch)); + } + + // Close the cURL resource, and free system resources + curl_close($ch); + + return $output; + } + + /** + * @param bool $appendSlash + * @return string + * @throws OpenIDConnectClientException + */ + public function getWellKnownIssuer($appendSlash = false) { + + return $this->getWellKnownConfigValue('issuer') . ($appendSlash ? '/' : ''); + } + + /** + * @return string + * @throws OpenIDConnectClientException + */ + public function getIssuer() { + + if (!isset($this->providerConfig['issuer'])) { + throw new OpenIDConnectClientException('The issuer has not been set'); + } + + return $this->providerConfig['issuer']; + } + + /** + * @return mixed + * @throws OpenIDConnectClientException + */ + public function getProviderURL() { + if (!isset($this->providerConfig['providerUrl'])) { + throw new OpenIDConnectClientException('The provider URL has not been set'); + } + + return $this->providerConfig['providerUrl']; + } + + /** + * @param string $url + */ + public function redirect($url) { + header('Location: ' . $url); + exit; + } + + /** + * @param string $httpProxy + */ + public function setHttpProxy($httpProxy) { + $this->httpProxy = $httpProxy; + } + + /** + * @param string $certPath + */ + public function setCertPath($certPath) { + $this->certPath = $certPath; + } + + /** + * @return string|null + */ + public function getCertPath() { + return $this->certPath; + } + + /** + * @param bool $verifyPeer + */ + public function setVerifyPeer($verifyPeer) { + $this->verifyPeer = $verifyPeer; + } + + /** + * @param bool $verifyHost + */ + public function setVerifyHost($verifyHost) { + $this->verifyHost = $verifyHost; + } + + + /** + * Controls whether http header HTTP_UPGRADE_INSECURE_REQUESTS should be considered + * defaults to true + * @param bool $httpUpgradeInsecureRequests + */ + public function setHttpUpgradeInsecureRequests($httpUpgradeInsecureRequests) { + $this->httpUpgradeInsecureRequests = $httpUpgradeInsecureRequests; + } + + /** + * @return bool + */ + public function getVerifyHost() { + return $this->verifyHost; + } + + /** + * @return bool + */ + public function getVerifyPeer() { + return $this->verifyPeer; + } + + /** + * @return bool + */ + public function getHttpUpgradeInsecureRequests() + { + return $this->httpUpgradeInsecureRequests; + } + + /** + * Use this for custom issuer validation + * The given function should accept the issuer string from the JWT claim as the only argument + * and return true if the issuer is valid, otherwise return false + * + * @param callable $issuerValidator + */ + public function setIssuerValidator($issuerValidator) { + $this->issuerValidator = $issuerValidator; + } + + /** + * @param bool $allowImplicitFlow + */ + public function setAllowImplicitFlow($allowImplicitFlow) { + $this->allowImplicitFlow = $allowImplicitFlow; + } + + /** + * @return bool + */ + public function getAllowImplicitFlow() { + return $this->allowImplicitFlow; + } + + /** + * + * Use this to alter a provider's endpoints and other attributes + * + * @param array $array + * simple key => value + */ + public function providerConfigParam($array) { + $this->providerConfig = array_merge($this->providerConfig, $array); + } + + /** + * @param string $clientSecret + */ + public function setClientSecret($clientSecret) { + $this->clientSecret = $clientSecret; + } + + /** + * @param string $clientID + */ + public function setClientID($clientID) { + $this->clientID = $clientID; + } + + + /** + * Dynamic registration + * + * @throws OpenIDConnectClientException + */ + public function register() { + + $registration_endpoint = $this->getProviderConfigValue('registration_endpoint'); + + $send_object = (object ) array_merge($this->registrationParams, [ + 'redirect_uris' => [$this->getRedirectURL()], + 'client_name' => $this->getClientName() + ]); + + $response = $this->fetchURL($registration_endpoint, json_encode($send_object)); + + $json_response = json_decode($response); + + // Throw some errors if we encounter them + if ($json_response === false) { + throw new OpenIDConnectClientException('Error registering: JSON response received from the server was invalid.'); + } + + if (isset($json_response->{'error_description'})) { + throw new OpenIDConnectClientException($json_response->{'error_description'}); + } + + $this->setClientID($json_response->{'client_id'}); + + // The OpenID Connect Dynamic registration protocol makes the client secret optional + // and provides a registration access token and URI endpoint if it is not present + if (isset($json_response->{'client_secret'})) { + $this->setClientSecret($json_response->{'client_secret'}); + } else { + throw new OpenIDConnectClientException('Error registering: + Please contact the OpenID Connect provider and obtain a Client ID and Secret directly from them'); + } + + } + + /** + * Introspect a given token - either access token or refresh token. + * @see https://tools.ietf.org/html/rfc7662 + * + * @param string $token + * @param string $token_type_hint + * @param string|null $clientId + * @param string|null $clientSecret + * @return mixed + * @throws OpenIDConnectClientException + */ + public function introspectToken($token, $token_type_hint = '', $clientId = null, $clientSecret = null) { + $introspection_endpoint = $this->getProviderConfigValue('introspection_endpoint'); + + $post_data = ['token' => $token]; + + if ($token_type_hint) { + $post_data['token_type_hint'] = $token_type_hint; + } + $clientId = $clientId !== null ? $clientId : $this->clientID; + $clientSecret = $clientSecret !== null ? $clientSecret : $this->clientSecret; + + // Convert token params to string format + $post_params = http_build_query($post_data, '', '&'); + $headers = ['Authorization: Basic ' . base64_encode(urlencode($clientId) . ':' . urlencode($clientSecret)), + 'Accept: application/json']; + + return json_decode($this->fetchURL($introspection_endpoint, $post_params, $headers)); + } + + /** + * Revoke a given token - either access token or refresh token. + * @see https://tools.ietf.org/html/rfc7009 + * + * @param string $token + * @param string $token_type_hint + * @param string|null $clientId + * @param string|null $clientSecret + * @return mixed + * @throws OpenIDConnectClientException + */ + public function revokeToken($token, $token_type_hint = '', $clientId = null, $clientSecret = null) { + $revocation_endpoint = $this->getProviderConfigValue('revocation_endpoint'); + + $post_data = ['token' => $token]; + + if ($token_type_hint) { + $post_data['token_type_hint'] = $token_type_hint; + } + $clientId = $clientId !== null ? $clientId : $this->clientID; + $clientSecret = $clientSecret !== null ? $clientSecret : $this->clientSecret; + + // Convert token params to string format + $post_params = http_build_query($post_data, '', '&'); + $headers = ['Authorization: Basic ' . base64_encode(urlencode($clientId) . ':' . urlencode($clientSecret)), + 'Accept: application/json']; + + return json_decode($this->fetchURL($revocation_endpoint, $post_params, $headers)); + } + + /** + * @return string + */ + public function getClientName() { + return $this->clientName; + } + + /** + * @param string $clientName + */ + public function setClientName($clientName) { + $this->clientName = $clientName; + } + + /** + * @return string + */ + public function getClientID() { + return $this->clientID; + } + + /** + * @return string + */ + public function getClientSecret() { + return $this->clientSecret; + } + + /** + * @return bool + */ + public function canVerifySignatures() { + return class_exists('\phpseclib3\Crypt\RSA') || class_exists('\phpseclib\Crypt\RSA') || class_exists('Crypt_RSA'); + } + + /** + * Set the access token. + * + * May be required for subclasses of this Client. + * + * @param string $accessToken + * @return void + */ + public function setAccessToken($accessToken) { + $this->accessToken = $accessToken; + } + + /** + * @return string + */ + public function getAccessToken() { + return $this->accessToken; + } + + /** + * @return string + */ + public function getRefreshToken() { + return $this->refreshToken; + } + + /** + * @return string + */ + public function getIdToken() { + return $this->idToken; + } + + /** + * @return object + */ + public function getAccessTokenHeader() { + return $this->decodeJWT($this->accessToken); + } + + /** + * @return object + */ + public function getAccessTokenPayload() { + return $this->decodeJWT($this->accessToken, 1); + } + + /** + * @return object + */ + public function getIdTokenHeader() { + return $this->decodeJWT($this->idToken); + } + + /** + * @return object + */ + public function getIdTokenPayload() { + return $this->decodeJWT($this->idToken, 1); + } + + /** + * @return string + */ + public function getTokenResponse() { + return $this->tokenResponse; + } + + /** + * Stores nonce + * + * @param string $nonce + * @return string + */ + protected function setNonce($nonce) { + $this->setSessionKey('openid_connect_nonce', $nonce); + return $nonce; + } + + /** + * Get stored nonce + * + * @return string + */ + protected function getNonce() { + return $this->getSessionKey('openid_connect_nonce'); + } + + /** + * Cleanup nonce + * + * @return void + */ + protected function unsetNonce() { + $this->unsetSessionKey('openid_connect_nonce'); + } + + /** + * Stores $state + * + * @param string $state + * @return string + */ + protected function setState($state) { + $this->setSessionKey('openid_connect_state', $state); + return $state; + } + + /** + * Get stored state + * + * @return string + */ + protected function getState() { + return $this->getSessionKey('openid_connect_state'); + } + + /** + * Cleanup state + * + * @return void + */ + protected function unsetState() { + $this->unsetSessionKey('openid_connect_state'); + } + + /** + * Stores $codeVerifier + * + * @param string $codeVerifier + * @return string + */ + protected function setCodeVerifier($codeVerifier) { + $this->setSessionKey('openid_connect_code_verifier', $codeVerifier); + return $codeVerifier; + } + + /** + * Get stored codeVerifier + * + * @return string + */ + protected function getCodeVerifier() { + return $this->getSessionKey('openid_connect_code_verifier'); + } + + /** + * Cleanup state + * + * @return void + */ + protected function unsetCodeVerifier() { + $this->unsetSessionKey('openid_connect_code_verifier'); + } + + /** + * Get the response code from last action/curl request. + * + * @return int + */ + public function getResponseCode() { + return $this->responseCode; + } + + /** + * Set timeout (seconds) + * + * @param int $timeout + */ + public function setTimeout($timeout) { + $this->timeOut = $timeout; + } + + /** + * @return int + */ + public function getTimeout() { + return $this->timeOut; + } + + /** + * Safely calculate length of binary string + * @param string $str + * @return int + */ + private static function safeLength($str) { + if (function_exists('mb_strlen')) { + return mb_strlen($str, '8bit'); + } + return strlen($str); + } + + /** + * Where hash_equals is not available, this provides a timing-attack safe string comparison + * @param string $str1 + * @param string $str2 + * @return bool + */ + private static function hashEquals($str1, $str2) { + $len1=static::safeLength($str1); + $len2=static::safeLength($str2); + + //compare strings without any early abort... + $len = min($len1, $len2); + $status = 0; + for ($i = 0; $i < $len; $i++) { + $status |= (ord($str1[$i]) ^ ord($str2[$i])); + } + //if strings were different lengths, we fail + $status |= ($len1 ^ $len2); + return ($status === 0); + } + + /** + * Use session to manage a nonce + */ + protected function startSession() { + if (session_status() === PHP_SESSION_NONE) { + @session_start(); + } + } + + protected function commitSession() { + $this->startSession(); + + session_write_close(); + } + + protected function getSessionKey($key) { + $this->startSession(); + + if (array_key_exists($key, $_SESSION)) { + return $_SESSION[$key]; + } + return false; + } + + protected function setSessionKey($key, $value) { + $this->startSession(); + + $_SESSION[$key] = $value; + } + + protected function unsetSessionKey($key) { + $this->startSession(); + + unset($_SESSION[$key]); + } + + public function setUrlEncoding($curEncoding) { + switch ($curEncoding) + { + case PHP_QUERY_RFC1738: + $this->encType = PHP_QUERY_RFC1738; + break; + + case PHP_QUERY_RFC3986: + $this->encType = PHP_QUERY_RFC3986; + break; + + default: + break; + } + + } + + /** + * @return array + */ + public function getScopes() { + return $this->scopes; + } + + /** + * @return array + */ + public function getResponseTypes() { + return $this->responseTypes; + } + + /** + * @return array + */ + public function getAuthParams() { + return $this->authParams; + } + + /** + * @return callable + */ + public function getIssuerValidator() { + return $this->issuerValidator; + } + + /** + * @return int + */ + public function getLeeway() { + return $this->leeway; + } + + /** + * @return string + */ + public function getCodeChallengeMethod() { + return $this->codeChallengeMethod; + } + + /** + * @param string $codeChallengeMethod + */ + public function setCodeChallengeMethod($codeChallengeMethod) { + $this->codeChallengeMethod = $codeChallengeMethod; + } +} diff --git a/vendor/jumbojett/openid-connect-php/tests/OpenIDConnectClientTest.php b/vendor/jumbojett/openid-connect-php/tests/OpenIDConnectClientTest.php new file mode 100644 index 0000000..7abf311 --- /dev/null +++ b/vendor/jumbojett/openid-connect-php/tests/OpenIDConnectClientTest.php @@ -0,0 +1,65 @@ +getRedirectURL()); + + $_SERVER['SERVER_NAME'] = 'domain.test'; + $_SERVER['REQUEST_URI'] = '/path/index.php?foo=bar&baz#fragment'; + self::assertSame('http://domain.test/path/index.php', $client->getRedirectURL()); + } + + public function testAuthenticateDoesNotThrowExceptionIfClaimsIsMissingNonce() + { + $fakeClaims = new \StdClass(); + $fakeClaims->iss = 'fake-issuer'; + $fakeClaims->aud = 'fake-client-id'; + $fakeClaims->nonce = null; + + $_REQUEST['id_token'] = 'abc.123.xyz'; + $_REQUEST['state'] = false; + $_SESSION['openid_connect_state'] = false; + + /** @var OpenIDConnectClient | MockObject $client */ + $client = $this->getMockBuilder(OpenIDConnectClient::class)->setMethods(['decodeJWT', 'getProviderConfigValue', 'verifyJWTsignature'])->getMock(); + $client->method('decodeJWT')->willReturn($fakeClaims); + $client->method('getProviderConfigValue')->with('jwks_uri')->willReturn(true); + $client->method('verifyJWTsignature')->willReturn(true); + + $client->setClientID('fake-client-id'); + $client->setIssuer('fake-issuer'); + $client->setIssuerValidator(function() { + return true; + }); + $client->setAllowImplicitFlow(true); + $client->setProviderURL('https://jwt.io/'); + + try { + $authenticated = $client->authenticate(); + $this->assertTrue($authenticated); + } catch ( OpenIDConnectClientException $e ) { + if ( $e->getMessage() === 'Unable to verify JWT claims' ) { + self::fail( 'OpenIDConnectClientException was thrown when it should not have been.' ); + } + } + } + + public function testSerialize() + { + $client = new OpenIDConnectClient('https://example.com', 'foo', 'bar', 'baz'); + $serialized = serialize($client); + $this->assertInstanceOf('Jumbojett\OpenIDConnectClient', unserialize($serialized)); + } +} diff --git a/vendor/jumbojett/openid-connect-php/tests/TokenVerificationTest.php b/vendor/jumbojett/openid-connect-php/tests/TokenVerificationTest.php new file mode 100644 index 0000000..5844992 --- /dev/null +++ b/vendor/jumbojett/openid-connect-php/tests/TokenVerificationTest.php @@ -0,0 +1,34 @@ +getMockBuilder(OpenIDConnectClient::class)->setMethods(['fetchUrl'])->getMock(); + $client->method('fetchUrl')->willReturn(file_get_contents(__DIR__ . "/data/jwks-$alg.json")); + $client->setProviderURL('https://jwt.io/'); + $client->providerConfigParam(['jwks_uri' => 'https://jwt.io/.well-known/jwks.json']); + $verified = $client->verifyJWTsignature($jwt); + self::assertTrue($verified); + $client->setAccessToken($jwt); + } + + public function providesTokens() + { + return [ + 'PS256' => ['ps256', 'eyJhbGciOiJQUzI1NiIsImtpZCI6Imtvbm5lY3RkLXRva2Vucy1zaWduaW5nLWtleSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJrcG9wLWh0dHBzOi8va29wYW5vLmRlbW8vbWVldC8iLCJleHAiOjE1NjgzNzE0NjEsImp0aSI6IkpkR0tDbEdOTXl2VXJpcmlRRUlWUXZCVmttT2FfQkRjIiwiaWF0IjoxNTY4MzcxMjIxLCJpc3MiOiJodHRwczovL2tvcGFuby5kZW1vIiwic3ViIjoiUHpUVWp3NHBlXzctWE5rWlBILXJxVHE0MTQ1Z3lDdlRvQmk4V1E5bFBrcW5rbEc1aktvRU5LM21Qb0I1WGY1ZTM5dFRMR2RKWXBMNEJubXFnelpaX0FAa29ubmVjdCIsImtjLmlzQWNjZXNzVG9rZW4iOnRydWUsImtjLmF1dGhvcml6ZWRTY29wZXMiOlsicHJvZmlsZSIsImVtYWlsIiwia29wYW5vL2t3bSIsImtvcGFuby9nYyIsImtvcGFuby9rdnMiLCJvcGVuaWQiXSwia2MuYXV0aG9yaXplZENsYWltcyI6eyJpZF90b2tlbiI6eyJuYW1lIjpudWxsfX0sImtjLmlkZW50aXR5Ijp7ImtjLmkuZG4iOiJKb25hcyBCcmVra2UiLCJrYy5pLmlkIjoiQUFBQUFLd2hxVkJBMCs1SXN4bjdwMU13UkNVQkFBQUFCZ0FBQUJzQUFBQk5VVDA5QUFBQUFBPT0iLCJrYy5pLnVuIjoidXNlcjEiLCJrYy5pLnVzIjoiTVEifSwia2MucHJvdmlkZXIiOiJpZGVudGlmaWVyLWtjIn0.hGRuXvul2kOiALHexwYp5MBEJVwz1YV3ehyM3AOuwCoK2w5sJxdciqqY_TfXCKyO6nAEbYLK3J0CBOjfup_IG0aCZcwzjto8khYlc4ezXkGnFsbJBNQdDGkpHtWnioWx-OJ3cXvY9F8aOvjaq0gw11ZDAcqQl0g7LTbJ9-J_yx0pmy3NGai2JB30Fh1OgSDzYfxWnE0RRgZG-x68e65RXfSBaEGW85OUh4wihxO2zdTGAHJ3Iq_-QAG4yRbXZtLx3ZspG7LNmqG-YE3huy3Rd8u3xrJNhmUOfEnz3x07q7VW0cj9NedX98BAbj3iNvksQsE0oG0J_f_Tu8Ai8VbWB72sJuXZWxANDKdz0BBYLzXhsjXkNByRq9x3zqDVsX-cVHei_XudxEOVRBjhkvW2MmIjcAHNKCKsdar865-gFG9McP4PCcBlY28tC0Cvnzyi83LBfpGRXdl6MJunnUsKQ1C79iCoVI1doK1erFN959Q-TGJfJA3Tr5LNpuGawB5rpe1nDGWvmYhg3uYfNl8uTTyvNgvvejcflEb2DURuXdqABuSiP7RkDWYtzx6mq49G0tRxelBbvyjQ2id2QjmRRdQ6dHEZ2NCJ51b8OFoDJBtxN1CD62TTxa3FUqCdZAPAUR3hHn_69vYq82MR514s-Gb67A6j2PbMPFATQP2UdK8'] + ]; + } +} diff --git a/vendor/jumbojett/openid-connect-php/tests/data/jwks-ps256.json b/vendor/jumbojett/openid-connect-php/tests/data/jwks-ps256.json new file mode 100644 index 0000000..a73a1e0 --- /dev/null +++ b/vendor/jumbojett/openid-connect-php/tests/data/jwks-ps256.json @@ -0,0 +1,12 @@ +{ + "keys": [ + { + "kty": "RSA", + "use": "sig", + "kid": "konnectd-tokens-signing-key", + "n": "10hb3pFUVcqJcS-d1pLCkFTyTqVD1GavlAai582CoRwFcyIQxCPJz0LJVgkUNwxSRkY0g0PcgFN_MmuuzpFXMkkiMIC9O_KwnuL34FrbijZvcGpnDn7kb9KAM883OVTr_w3wFeQIyh0ksSwVQ9CxVQ-ZeCXP73CCGk99uDb8SeF8_vncXJmaak99pK6HKJteSLkA-Ywxo9HOINZK2vW06UYcSkeoQnSI27Cd5-T6GVgqKH0Su4c5Ydou_w0tL_UkbZA4fIbMZC6dtWmBQf6tyYsCM9fbWNIVOj_7WlWcAOSTFNF2We2dxJrOzt6vDND3k1nCgg_EEM6cgBO3swUCktTFuQxo1sryYX5WXz9wnJb38b9mTXhOeF0bd9y_VQq8erSlcyRu8UGzX65tIf534hLL16KQaHbjROGSQvzqFrISmSBjBTjkPedTZSYOhiVJ95-em_Y6uLi-T7V4bs4dcg3oa0H_glXltoC9JxzS6gfMGGLgh-NpGEOdC_QosyzVVfzT70TurOGnsB1_VcAm_fK-T1Zv_ztpr5OZNfXWXC3Pfq_3sxP5HDKMk8luZ7LOWk7HVSYBdCFmOM1A3KmHNS2fEs-QHIr-XjYQ7QrXsRFP3dmoEPfiYlu03m8Xs3UMB70eGeGQx7OhZSuogxV_oCfApV5EJfuz97tVmOg8iMs", + "e": "AQAB" + } + ], + "kty": "" +} -- cgit v1.2.3