diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index da459b99..baf35ea4 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -16,23 +16,19 @@ jobs: lint: name: "Lint" runs-on: "ubuntu-latest" - strategy: fail-fast: false matrix: - php-version: [ '5.6', '7.0', '7.1', '7.2', '7.3', '7.4', '8.0', '8.1' ] - - continue-on-error: ${{ matrix.php-version == '8.1' }} - + php-version: [ '5.6', '7.0', '7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2' ] steps: - uses: "actions/checkout@v2" - uses: "shivammathur/setup-php@v2" with: - php-version: "7.4" + php-version: "${{ matrix.php-version }}" coverage: "none" ini-values: "memory_limit=-1, zend.assertions=1, error_reporting=-1, display_errors=On" tools: "composer:v2" - - uses: "ramsey/composer-install@v1" + - uses: "ramsey/composer-install@v2" - name: "Lint the PHP source code" run: "./vendor/bin/parallel-lint src test" @@ -43,47 +39,50 @@ jobs: - uses: "actions/checkout@v2" - uses: "shivammathur/setup-php@v2" with: - php-version: "7.4" + php-version: "latest" coverage: "none" ini-values: "memory_limit=-1" tools: "composer:v2" - - uses: "ramsey/composer-install@v1" + - uses: "ramsey/composer-install@v2" - name: "Check coding standards" run: "./vendor/bin/phpcs src --standard=psr2 -sp --colors" + coverage: + name: "Coverage" + runs-on: "ubuntu-latest" + steps: + - uses: "actions/checkout@v2" + - uses: "shivammathur/setup-php@v2" + with: + php-version: "latest" + coverage: "pcov" + ini-values: "memory_limit=-1, zend.assertions=1, error_reporting=-1, display_errors=On" + tools: "composer" + - name: "Prepare for tests" + run: "mkdir -p build/logs" + - uses: "ramsey/composer-install@v2" + - name: "Run unit tests" + run: "./vendor/bin/phpunit --colors=always --coverage-clover build/logs/clover.xml --coverage-text" + - name: "Publish coverage report to Codecov" + uses: "codecov/codecov-action@v2" + unit-tests: name: "Unit Tests" runs-on: "ubuntu-latest" - continue-on-error: ${{ matrix.experimental }} strategy: fail-fast: false matrix: - php-version: - - "5.6" - - "7.0" - - "7.1" - - "7.2" - - "7.3" - - "7.4" - - "8.0" - - "8.1" - experimental: - - false - + php-version: [ '5.6', '7.0', '7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2' ] steps: - uses: "actions/checkout@v2" - uses: "shivammathur/setup-php@v2" with: php-version: "${{ matrix.php-version }}" - coverage: "pcov" + coverage: "none" ini-values: "memory_limit=-1, zend.assertions=1, error_reporting=-1, display_errors=On" - tools: "composer:v2" + tools: "composer" - name: "Prepare for tests" run: "mkdir -p build/logs" - - uses: "ramsey/composer-install@v1" - with: - composer-options: "${{ matrix.composer-options }}" + - uses: "ramsey/composer-install@v2" - name: "Run unit tests" - run: "./vendor/bin/phpunit --colors=always --coverage-clover build/logs/clover.xml" - - name: "Publish coverage report to Codecov" - uses: "codecov/codecov-action@v1" + run: "./vendor/bin/phpunit --colors=always" diff --git a/CHANGELOG.md b/CHANGELOG.md index 8db2e264..6247164d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # OAuth 2.0 Client Changelog +## 2.6.1 + +_Released: 2021-12-22_ + +* Fix deprecation notices, providing full support for PHP 8.1 + [#919](https://github.com/thephpleague/oauth2-client/pull/919) + [#920](https://github.com/thephpleague/oauth2-client/pull/920) + ## 2.6.0 _Released: 2020-10-27_ diff --git a/README.md b/README.md index 96f4b05e..cbb449d4 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ This package provides a base for integrating with [OAuth 2.0](http://oauth.net/2 [![Source Code](https://img.shields.io/badge/source-thephpleague/oauth2--client-blue.svg?style=flat-square)](https://github.com/thephpleague/oauth2-client) [![Latest Version](https://img.shields.io/github/release/thephpleague/oauth2-client.svg?style=flat-square)](https://github.com/thephpleague/oauth2-client/releases) [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](https://github.com/thephpleague/oauth2-client/blob/master/LICENSE) -[![Build Status](https://img.shields.io/github/workflow/status/thephpleague/oauth2-client/CI?label=CI&logo=github&style=flat-square)](https://github.com/thephpleague/oauth2-client/actions?query=workflow%3ACI) +[![Build Status](https://img.shields.io/github/actions/workflow/status/thephpleague/oauth2-client/continuous-integration.yml?label=CI&logo=github&style=flat-square)](https://github.com/thephpleague/oauth2-client/actions?query=workflow%3ACI) [![Codecov Code Coverage](https://img.shields.io/codecov/c/gh/thephpleague/oauth2-client?label=codecov&logo=codecov&style=flat-square)](https://codecov.io/gh/thephpleague/oauth2-client) [![Total Downloads](https://img.shields.io/packagist/dt/league/oauth2-client.svg?style=flat-square)](https://packagist.org/packages/league/oauth2-client) @@ -24,6 +24,7 @@ This package is compliant with [PSR-1][], [PSR-2][], [PSR-4][], and [PSR-7][]. I We support the following versions of PHP: +* PHP 8.1 * PHP 8.0 * PHP 7.4 * PHP 7.3 diff --git a/docs/Gemfile.lock b/docs/Gemfile.lock index 304df935..5cc1c3c5 100644 --- a/docs/Gemfile.lock +++ b/docs/Gemfile.lock @@ -1,23 +1,22 @@ GEM remote: https://rubygems.org/ specs: - activesupport (6.0.4.1) + activesupport (6.0.6.1) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 0.7, < 2) minitest (~> 5.1) tzinfo (~> 1.1) zeitwerk (~> 2.2, >= 2.2.2) - addressable (2.8.0) - public_suffix (>= 2.0.2, < 5.0) + addressable (2.8.1) + public_suffix (>= 2.0.2, < 6.0) coffee-script (2.4.1) coffee-script-source execjs coffee-script-source (1.11.1) colorator (1.1.0) - commonmarker (0.17.13) - ruby-enum (~> 0.5) - concurrent-ruby (1.1.9) - dnsruby (1.61.7) + commonmarker (0.23.9) + concurrent-ruby (1.2.2) + dnsruby (1.61.9) simpleidn (~> 0.1) em-websocket (0.5.3) eventmachine (>= 0.12.9) @@ -26,38 +25,24 @@ GEM ffi (>= 1.15.0) eventmachine (1.2.7) execjs (2.8.1) - faraday (1.8.0) - faraday-em_http (~> 1.0) - faraday-em_synchrony (~> 1.0) - faraday-excon (~> 1.1) - faraday-httpclient (~> 1.0.1) - faraday-net_http (~> 1.0) - faraday-net_http_persistent (~> 1.1) - faraday-patron (~> 1.0) - faraday-rack (~> 1.0) - multipart-post (>= 1.2, < 3) + faraday (2.6.0) + faraday-net_http (>= 2.0, < 3.1) ruby2_keywords (>= 0.0.4) - faraday-em_http (1.0.0) - faraday-em_synchrony (1.0.0) - faraday-excon (1.1.0) - faraday-httpclient (1.0.1) - faraday-net_http (1.0.1) - faraday-net_http_persistent (1.2.0) - faraday-patron (1.0.0) - faraday-rack (1.0.0) - ffi (1.15.4) + faraday-net_http (3.0.1) + ffi (1.15.5) forwardable-extended (2.6.0) gemoji (3.0.1) - github-pages (222) + github-pages (227) github-pages-health-check (= 1.17.9) - jekyll (= 3.9.0) + jekyll (= 3.9.2) jekyll-avatar (= 0.7.0) jekyll-coffeescript (= 1.1.1) - jekyll-commonmark-ghpages (= 0.1.6) + jekyll-commonmark-ghpages (= 0.2.0) jekyll-default-layout (= 0.1.4) jekyll-feed (= 0.15.1) jekyll-gist (= 1.5.0) jekyll-github-metadata (= 2.13.0) + jekyll-include-cache (= 0.2.1) jekyll-mentions (= 1.6.0) jekyll-optional-front-matter (= 0.3.2) jekyll-paginate (= 1.1.0) @@ -66,7 +51,7 @@ GEM jekyll-relative-links (= 0.6.1) jekyll-remote-theme (= 0.4.3) jekyll-sass-converter (= 1.5.2) - jekyll-seo-tag (= 2.7.1) + jekyll-seo-tag (= 2.8.0) jekyll-sitemap (= 1.4.0) jekyll-swiss (= 1.0.0) jekyll-theme-architect (= 0.2.0) @@ -84,12 +69,12 @@ GEM jekyll-theme-time-machine (= 0.2.0) jekyll-titles-from-headings (= 0.5.3) jemoji (= 0.12.0) - kramdown (= 2.3.1) + kramdown (= 2.3.2) kramdown-parser-gfm (= 1.1.0) liquid (= 4.0.3) mercenary (~> 0.3) minima (= 2.5.1) - nokogiri (>= 1.12.5, < 2.0) + nokogiri (>= 1.13.6, < 2.0) rouge (= 3.26.0) terminal-table (~> 1.4) github-pages-health-check (1.17.9) @@ -98,13 +83,13 @@ GEM octokit (~> 4.0) public_suffix (>= 3.0, < 5.0) typhoeus (~> 1.3) - html-pipeline (2.14.0) + html-pipeline (2.14.3) activesupport (>= 2) nokogiri (>= 1.4) http_parser.rb (0.8.0) i18n (0.9.5) concurrent-ruby (~> 1.0) - jekyll (3.9.0) + jekyll (3.9.2) addressable (~> 2.4) colorator (~> 1.0) em-websocket (~> 0.5) @@ -122,12 +107,12 @@ GEM jekyll-coffeescript (1.1.1) coffee-script (~> 2.2) coffee-script-source (~> 1.11.1) - jekyll-commonmark (1.3.1) - commonmarker (~> 0.14) - jekyll (>= 3.7, < 5.0) - jekyll-commonmark-ghpages (0.1.6) - commonmarker (~> 0.17.6) - jekyll-commonmark (~> 1.2) + jekyll-commonmark (1.4.0) + commonmarker (~> 0.22) + jekyll-commonmark-ghpages (0.2.0) + commonmarker (~> 0.23.4) + jekyll (~> 3.9.0) + jekyll-commonmark (~> 1.4.0) rouge (>= 2.0, < 4.0) jekyll-default-layout (0.1.4) jekyll (~> 3.0) @@ -138,6 +123,8 @@ GEM jekyll-github-metadata (2.13.0) jekyll (>= 3.4, < 5.0) octokit (~> 4.0, != 4.4.0) + jekyll-include-cache (0.2.1) + jekyll (>= 3.7, < 5.0) jekyll-mentions (1.6.0) html-pipeline (~> 2.3) jekyll (>= 3.7, < 5.0) @@ -157,7 +144,7 @@ GEM rubyzip (>= 1.3.0, < 3.0) jekyll-sass-converter (1.5.2) sass (~> 3.4) - jekyll-seo-tag (2.7.1) + jekyll-seo-tag (2.8.0) jekyll (>= 3.8, < 5.0) jekyll-sitemap (1.4.0) jekyll (>= 3.7, < 5.0) @@ -210,41 +197,38 @@ GEM gemoji (~> 3.0) html-pipeline (~> 2.2) jekyll (>= 3.0, < 5.0) - kramdown (2.3.1) + kramdown (2.3.2) rexml kramdown-parser-gfm (1.1.0) kramdown (~> 2.0) libv8 (3.16.14.19) liquid (4.0.3) - listen (3.7.0) + listen (3.7.1) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) mercenary (0.3.6) - mini_portile2 (2.6.1) + mini_portile2 (2.8.1) minima (2.5.1) jekyll (>= 3.5, < 5.0) jekyll-feed (~> 0.9) jekyll-seo-tag (~> 2.1) - minitest (5.14.4) - multipart-post (2.1.1) - nokogiri (1.12.5) - mini_portile2 (~> 2.6.1) + minitest (5.18.0) + nokogiri (1.14.3) + mini_portile2 (~> 2.8.0) racc (~> 1.4) - octokit (4.21.0) - faraday (>= 0.9) - sawyer (~> 0.8.0, >= 0.5.3) + octokit (4.25.1) + faraday (>= 1, < 3) + sawyer (~> 0.9) pathutil (0.16.2) forwardable-extended (~> 2.6) - public_suffix (4.0.6) - racc (1.6.0) - rb-fsevent (0.11.0) + public_suffix (4.0.7) + racc (1.6.2) + rb-fsevent (0.11.2) rb-inotify (0.10.1) ffi (~> 1.0) ref (2.0.0) rexml (3.2.5) rouge (3.26.0) - ruby-enum (0.9.0) - i18n ruby2_keywords (0.0.5) rubyzip (2.3.2) safe_yaml (1.0.5) @@ -253,9 +237,9 @@ GEM sass-listen (4.0.0) rb-fsevent (~> 0.9, >= 0.9.4) rb-inotify (~> 0.9, >= 0.9.7) - sawyer (0.8.2) + sawyer (0.9.2) addressable (>= 2.3.5) - faraday (> 0.8, < 2.0) + faraday (>= 0.17.3, < 3) simpleidn (0.2.1) unf (~> 0.1.4) terminal-table (1.8.0) @@ -266,13 +250,13 @@ GEM thread_safe (0.3.6) typhoeus (1.4.0) ethon (>= 0.9.0) - tzinfo (1.2.9) + tzinfo (1.2.11) thread_safe (~> 0.1) unf (0.1.4) unf_ext - unf_ext (0.0.8) + unf_ext (0.0.8.2) unicode-display_width (1.8.0) - zeitwerk (2.5.1) + zeitwerk (2.6.7) PLATFORMS ruby diff --git a/docs/providers/thirdparty.md b/docs/providers/thirdparty.md index aeabd759..fb0192e7 100755 --- a/docs/providers/thirdparty.md +++ b/docs/providers/thirdparty.md @@ -34,6 +34,7 @@ Gateway | Composer Package | Maintainer [ColorMeShop](https://github.com/pepabo/oauth2-colormeshop) | pepabo/oauth2-colormeshop | [GMO Pepabo, Inc.](https://github.com/pepabo) [Canvas LMS](https://github.com/smtech/oauth2-canvaslms) | smtech/oauth2-canvaslms | [Seth Battis](https://github.com/battis) [concrete5](https://github.com/concrete5/oauth2-concrete5) | concrete5/oauth2-concrete5 | [Andrew Embler](https://github.com/aembler) +[Chaster App](https://github.com/Austomos/oauth2-chaster-app) | austomos/oauth2-chaster-app | [Ben Hyr](https://github.com/Austomos) [ChatWork](https://github.com/chatwork/oauth2-chatwork-php) | chatwork/oauth2-chatwork | [Yuta Adachi](https://github.com/ada-u) [Clever](https://github.com/schoolrunner/oauth2-clever) | schoolrunner/oauth2-clever | [Schoolrunner](https://github.com/schoolrunner) [CloudConvert](https://github.com/osavchenko/oauth2-cloudconvert) | osavchenko/oauth2-cloudconvert | [Oleksandr Savchenko](https://github.com/osavchenko) @@ -45,6 +46,7 @@ Gateway | Composer Package | Maintainer [Discord](https://github.com/wohali/oauth2-discord-new) | wohali/oauth2-discord-new | [Joan Touzet](https://github.com/wohali) [Docusign](https://github.com/AlaaSarhan/oauth2-docusign) | sarhan/oauth2-docusign | [Alaa Sarhan](https://github.com/AlaaSarhan) [Dokeop](https://github.com/dokeop/oauth2-dokeop) | dokeop/oauth2-dokeop | [Dokeop](https://github.com/dokeop) +[DonationAlerts](https://github.com/mish-ka-mishka/oauth2-donationalerts) | mkaverin/oauth2-donationalerts | [Michael Kaverin](https://github.com/mish-ka-mishka) [Dribbble](https://github.com/crewlabs/oauth2-dribbble) | crewlabs/oauth2-dribbble | [Crew Labs](https://crew.co/labs) [Dropbox](https://github.com/stevenmaguire/oauth2-dropbox) | stevenmaguire/oauth2-dropbox | [Steven Maguire](https://github.com/stevenmaguire) [Drupal](https://github.com/chrishemmings/oauth2-drupal) | chrishemmings/oauth2-drupal | [Chris Hemmings](https://github.com/chrishemmings) @@ -54,18 +56,23 @@ Gateway | Composer Package | Maintainer [Envato](https://github.com/dilab/envato-oauth2-provider) | dilab/envato-oauth2-provider | [Xu Ding](https://github.com/dilab) [Epic Games](https://github.com/MrPropre/oauth2-epicgames) | mrpropre/oauth2-epicgames | [Adrien Alais](https://github.com/MrPropre) [ESIA](https://packagist.org/packages/ekapusta/oauth2-esia) | ekapusta/oauth2-esia | [Alexander Ustimenko](https://github.com/ekapusta) +[Etsy](https://packagist.org/packages/startz/oauth2-etsy) | startz/oauth2-etsy | [Chuck Burgess](https://github.com/cdburgess) [EVE Online](https://github.com/killmails/oauth2-eve) | killmails/oauth2-eve | [Oizys](https://github.com/syzio) [Eventbrite](https://github.com/stevenmaguire/oauth2-eventbrite) | stevenmaguire/oauth2-eventbrite | [Steven Maguire](https://github.com/stevenmaguire) +[FieldEdge](https://github.com/compwright/oauth2-fieldedge) | compwright/oauth2-fieldedge | [Jonathon Hill](https://compwright.com) +[Firefly III](https://github.com/StanSoftBG/oauth2-firefly-iii) | stansoft/oauth2-firefly-iii | [Stanimir Stoyanov](https://github.com/stratoss) [Fitbit](https://github.com/djchen/oauth2-fitbit) | djchen/oauth2-fitbit | [Dan Chen](https://github.com/djchen) [FormAssembly](https://github.com/FatherShawn/oauth2-formassembly) | fathershawn/oauth2-formassembly | [Shawn Duncan](https://github.com/FatherShawn) [Foursquare](https://github.com/stevenmaguire/oauth2-foursquare) | stevenmaguire/oauth2-foursquare | [Steven Maguire](https://github.com/stevenmaguire) [FreeAgent](https://github.com/CloudManaged/oauth2-freeagent) | cloudmanaged/oauth2-freeagent | *Unmaintained* [FreshBooks](https://github.com/zerospam/oauth2-freshbooks) | zerospam/oauth2-freshbooks | [Antoine Aflalo](https://github.com/Belphemur) +[Genesys](https://github.com/vormkracht10/oauth2-genesys) | vormkracht10/oauth2-genesys | [Vormkracht10](https://github.com/vormkracht10) [Geocaching](https://github.com/Surfoo/oauth2-geocaching) | surfoo/oauth2-geocaching | [Surfoo](https://github.com/Surfoo) [GitLab](https://github.com/omines/oauth2-gitlab) | omines/oauth2-gitlab | [Niels Keurentjes](https://github.com/curry684) [Gumroad](https://github.com/Alofoxx/oauth2-gumroad) | alofoxx/oauth2-gumroad | [Alofoxx](https://github.com/Alofoxx) [GotoWebinar](https://github.com/dalpras/oauth2-gotowebinar) | dalpras/oauth2-gotowebinar | [Stefano Dal Prà](https://github.com/dalpras) -[Harvest](https://github.com/nilesuan/oauth2-harvest) | nilesuan/oauth2-harvest | [Nile Suan](https://github.com/nilesuan) +[Harvest API v2](https://github.com/globalvisionmedia/oauth2-harvest) | globalvisionmedia/oauth2-harvest | [Peter Hawkins](https://www.globalvision.com.au) +[Harvest API v1](https://github.com/nilesuan/oauth2-harvest) | nilesuan/oauth2-harvest | [Nile Suan](https://github.com/nilesuan) [HeadHunter](https://packagist.org/packages/alexmasterov/oauth2-headhunter) | alexmasterov/oauth2-headhunter | [Alex Masterov](https://github.com/AlexMasterov) [Heroku](https://github.com/stevenmaguire/oauth2-heroku) | stevenmaguire/oauth2-heroku | [Steven Maguire](https://github.com/stevenmaguire) [Housecall Pro](https://github.com/compwright/oauth2-housecallpro) | compwright/oauth2-housecallpro | [Jonathon Hill](https://github.com/compwright) @@ -74,6 +81,7 @@ Gateway | Composer Package | Maintainer [Imgur](https://github.com/adam-paterson/oauth2-imgur) | adam-paterson/oauth2-imgur | [Adam Paterson](https://github.com/adam-paterson) [Jira](https://packagist.org/packages/mrjoops/oauth2-jira) | mrjoops/oauth2-jira | [Alexandre Lahure](https://github.com/mrjoops) [Keycloak](https://github.com/stevenmaguire/oauth2-keycloak) | stevenmaguire/oauth2-keycloak | [Steven Maguire](https://github.com/stevenmaguire) +[Lichess](https://packagist.org/packages/joseayram/oauth2-lichess) | joseayram/oauth2-lichess | [José Ayram](https://github.com/joseayram) [Linode](https://packagist.org/packages/webinarium/oauth2-linode) | webinarium/oauth2-linode | [Artem Rodygin](https://github.com/webinarium) [Mailchimp](https://github.com/chadhutchins/oauth2-mailchimp) | chadhutchins/oauth2-mailchimp | [Chad Hutchins](https://github.com/chadhutchins) [Mail.ru](https://packagist.org/packages/aego/oauth2-mailru) | aego/oauth2-mailru | [Alexey](https://github.com/rakeev) @@ -84,7 +92,9 @@ Gateway | Composer Package | Maintainer [Meetup](https://packagist.org/packages/wittestier/oauth2-meetup) | wittestier/oauth2-meetup | [WitteStier](https://gitlab.com/WitteStier) [MercadoLibre](https://github.com/docta/oauth2-mercadolibre) | docta/oauth2-mercadolibre | [Lucas Banegas](https://github.com/lucascono) [Microsoft](https://github.com/stevenmaguire/oauth2-microsoft) | stevenmaguire/oauth2-microsoft | [Steven Maguire](https://github.com/stevenmaguire) +[MYOB](https://github.com/globalvisionmedia/oauth2-myob) | globalvisionmedia/oauth2-myob | [Peter Hawkins](https://www.globalvision.com.au) [Mollie](https://github.com/mollie/oauth2-mollie-php) | mollie/oauth2-mollie-php | [Mollie](https://github.com/mollie) +[Monizze](https://github.com/jzecca/oauth2-monizze) | jzecca/oauth2-monizze | [Jérôme Zecca](https://github.com/jzecca) [Mixer](https://gitlab.com/morgann/oauth2-mixer) | morgann/oauth2-mixer | [Morgann](https://gitlab.com/morgann/oauth2-mixer) [Naver](https://packagist.org/packages/deminoth/oauth2-naver) | deminoth/oauth2-naver | [SangYeob Bono Yu](https://github.com/deminoth) [Netatmo](https://github.com/rugaard/oauth2-netatmo) | rugaard/oauth2-netatmo | [Morten Rugaard](https://github.com/rugaard) @@ -106,9 +116,13 @@ Gateway | Composer Package | Maintainer [Resource Guru](https://github.com/adam-paterson/oauth2-resource-guru) | adam-paterson/oauth2-resource-guru | [Adam Paterson](https://github.com/adam-paterson) [Riot (RSO)](https://github.com/kdefives/oauth2-riot) | kdefives/oauth2-riot | [Kevin Defives](https://github.com/kdefives) [Salesforce](https://github.com/stevenmaguire/oauth2-salesforce) | stevenmaguire/oauth2-salesforce | [Steven Maguire](https://github.com/stevenmaguire) +[SceneId](https://github.com/potibm/oauth2-sceneid) | potibm/oauth2-sceneid | [Stefan Keßeler](https://github.com/potibm) +[Service Fusion](https://github.com/compwright/oauth2-servicefusion) | compwright/oauth2-servicefusion | [Jonathon Hill](https://compwright.com) +[ServiceTitan](https://github.com/compwright/oauth2-servicetitan) | compwright/oauth2-servicetitan | [Jonathon Hill](https://compwright.com) [Shopify](https://github.com/multidimension-al/oauth2-shopify) | multidimensional/oauth2-shopify | [multidimension.al](https://multidimension.al/) [Slack](https://github.com/adam-paterson/oauth2-slack) | adam-paterson/oauth2-slack | [Adam Paterson](https://github.com/adam-paterson) [Snapchat](https://github.com/pbringetto/oauth2-snapchat) | pbringetto/oauth2-snapchat | [Paul Bringetto](https://github.com/pbringetto) +[SoundCloud](https://packagist.org/packages/martin1982/oauth2-soundcloud) | martin1982/oauth2-soundcloud | [Martin de Keijzer](https://github.com/martin1982) [Spotify](https://packagist.org/packages/audeio/spotify-web-api) | audeio/spotify-web-api | [Jonjo McKay](https://github.com/jonjomckay) [Spotify](https://packagist.org/packages/kerox/oauth2-spotify) | kerox/oauth2-spotify | [Romain Monteil](https://github.com/ker0x) [SteemConnect V2](https://github.com/hernandev/oauth2-sc2) | hernandev/oauth2-sc2 | [Diego Hernandes](https://github.com/hernandev) @@ -117,13 +131,16 @@ Gateway | Composer Package | Maintainer [Square](https://packagist.org/packages/wheniwork/oauth2-square) | wheniwork/oauth2-square | [Woody Gilk](https://github.com/shadowhand) [StackExchange](https://packagist.org/packages/alexmasterov/oauth2-stackexchange) | alexmasterov/oauth2-stackexchange | [Alex Masterov](https://github.com/AlexMasterov) [SuperJob](https://packagist.org/packages/alexmasterov/oauth2-superjob) | alexmasterov/oauth2-superjob | [Alex Masterov](https://github.com/AlexMasterov) +[TikTok](https://github.com/bastiaandewaele/oauth2-tiktok) | bastiaandewaele/oauth2-tiktok | [Bastiaan Dewaele](https://github.com/bastiaandewaele) [Thingiverse](https://packagist.org/packages/freshworkx/oauth2-thingiverse) | freshworkx/oauth2-thingiverse | [Jens Neumann](https://github.com/freshworkx) [ThirtySevenSignals](https://github.com/nilesuan/oauth2-thirtysevensignals) | nilesuan/oauth2-thirtysevensignals | [Nile Suan](https://github.com/nilesuan) [Trakt.tv](https://github.com/Bogstag/oauth2-trakt) | bogstag/oauth2-trakt | [Krister Bogstag](https://github.com/Bogstag/) [Trovo](https://github.com/artandor/oauth2-trovo) | artandor/oauth2-trovo | [Nicolas Mylle](https://github.com/artandor/) +[Trustpilot](https://github.com/dmt-software/oauth2-trustpilot) | dmt-software/oauth2-trustpilot | [DMT software](https://github.com/dmt-software/) [Twinfield](https://github.com/php-twinfield/twinfield) | php-twinfield/twinfield | [Mollie B.V.](https://github.com/mollie) [Twitch.tv](https://github.com/tpavlek/oauth2-twitch) | depotwarehouse/oauth2-twitch | [Troy Pavlek](https://github.com/tpavlek) [Twitch.tv (New API Helix)](https://github.com/vertisan/oauth2-twitch-helix) | vertisan/oauth2-twitch-helix | [Paweł Farys](https://github.com/vertisan) +[Twitter](https://github.com/smolblog/oauth2-twitter) | smolblog/oauth2-twitter | [Evan Hildreth](https://github.com/oddevan) [Uber](https://github.com/stevenmaguire/oauth2-uber) | stevenmaguire/oauth2-uber | [Steven Maguire](https://github.com/stevenmaguire) [Unsplash](https://github.com/hughbertd/oauth2-unsplash) | hughbertd/oauth2-unsplash | [Hugh Downer](https://github.com/hughbertd) [Untappd](https://github.com/shadowhand/oauth2-untappd) | shadowhand/oauth2-untappd | [Woody Gilk](https://github.com/shadowhand) @@ -132,6 +149,7 @@ Gateway | Composer Package | Maintainer [Vkontakte](https://github.com/j4k/oauth2-vkontakte) | j4k/oauth2-vkontakte | [Jack W](https://github.com/j4k) [Withings](https://github.com/waytohealth/oauth2-withings) | waytohealth/oauth2-withings | [Way to Health](https://github.com/waytohealth) [WordPress](https://github.com/krombox/oauth2-wordpress) | krombox/oauth2-wordpress | [Roman Kapustian](https://github.com/krombox) +[Webflow](https://github.com/koalatiapp/oauth2-webflow) | koalati/oauth2-webflow | [Koalati](https://github.com/koalatiapp) [Wechat](https://github.com/oakhope/oauth2-wechat) | oakhope/oauth2-wechat | [Benji Wang](https://github.com/oakhope) [WeCounsel](https://github.com/stevenmaguire/oauth2-wecounsel) | stevenmaguire/oauth2-wecounsel | [Steven Maguire](https://github.com/stevenmaguire) [Wrike](https://github.com/michaelKaefer/oauth2-wrike) | michaelkaefer/oauth2-wrike | [Michael Käfer](https://github.com/michaelKaefer) diff --git a/docs/usage.md b/docs/usage.md index 5950fd95..88a8c21f 100755 --- a/docs/usage.md +++ b/docs/usage.md @@ -16,6 +16,7 @@ The following example uses the out-of-the-box `GenericProvider` provided by this The *authorization code* grant type is the most common grant type used when authenticating users with a third-party service. This grant type utilizes a *client* (this library), a *service provider* (the server), and a *resource owner* (the account with credentials to a protected—or owned—resource) to request access to resources owned by the user. This is often referred to as _3-legged OAuth_, since there are three parties involved. + ```php $provider = new \League\OAuth2\Client\Provider\GenericProvider([ 'clientId' => 'XXXXXX', // The client ID assigned to you by the provider @@ -37,12 +38,16 @@ if (!isset($_GET['code'])) { // Get the state generated for you and store it to the session. $_SESSION['oauth2state'] = $provider->getState(); + // Optional, only required when PKCE is enabled. + // Get the PKCE code generated for you and store it to the session. + $_SESSION['oauth2pkceCode'] = $provider->getPkceCode(); + // Redirect the user to the authorization URL. header('Location: ' . $authorizationUrl); exit; // Check given state against previously stored one to mitigate CSRF attack -} elseif (empty($_GET['state']) || (isset($_SESSION['oauth2state']) && $_GET['state'] !== $_SESSION['oauth2state'])) { +} elseif (empty($_GET['state']) || empty($_SESSION['oauth2state']) || $_GET['state'] !== $_SESSION['oauth2state']) { if (isset($_SESSION['oauth2state'])) { unset($_SESSION['oauth2state']); @@ -53,6 +58,10 @@ if (!isset($_GET['code'])) { } else { try { + + // Optional, only required when PKCE is enabled. + // Restore the PKCE code stored in the session. + $provider->setPkceCode($_SESSION['oauth2pkceCode']); // Try to get an access token using the authorization code grant. $accessToken = $provider->getAccessToken('authorization_code', [ @@ -90,6 +99,31 @@ if (!isset($_GET['code'])) { } ``` +### Authorization Code Grant with PKCE + +To enable PKCE (Proof Key for Code Exchange) you can set the `pkceMethod` option for the provider. +Supported methods are: +- `S256` Recommended method. The code challenge will be hashed with sha256. +- `plain` **NOT** recommended. The code challenge will be sent as plain text. Only use this if no other option is possible. + +You can configure the PKCE method as follows: +```php +$provider = new \League\OAuth2\Client\Provider\GenericProvider([ + // ... + // other options + // ... + 'pkceMethod' => \League\OAuth2\Client\Provider\GenericProvider::PKCE_METHOD_S256 +]); +``` +The PKCE code needs to be used between requests and therefore be saved and restored, usually via the session. +In the [example](#authorization-code-grant-example) above this is done as follows: +```php +// Store the PKCE code after the `getAuthorizationUrl()` call. +$_SESSION['oauth2pkceCode'] = $provider->getPkceCode(); +// ... +// Restore the PKCE code before the `getAccessToken()` call. +$provider->setPkceCode($_SESSION['oauth2pkceCode']); +``` Refreshing a Token ------------------ diff --git a/src/Provider/AbstractProvider.php b/src/Provider/AbstractProvider.php index d1679998..293a54d6 100644 --- a/src/Provider/AbstractProvider.php +++ b/src/Provider/AbstractProvider.php @@ -17,6 +17,7 @@ use GuzzleHttp\Client as HttpClient; use GuzzleHttp\ClientInterface as HttpClientInterface; use GuzzleHttp\Exception\BadResponseException; +use InvalidArgumentException; use League\OAuth2\Client\Grant\AbstractGrant; use League\OAuth2\Client\Grant\GrantFactory; use League\OAuth2\Client\OptionProvider\OptionProviderInterface; @@ -44,7 +45,7 @@ abstract class AbstractProvider use QueryBuilderTrait; /** - * @var string Key used in a token response to identify the resource owner. + * @var string|null Key used in a token response to identify the resource owner. */ const ACCESS_TOKEN_RESOURCE_OWNER_ID = null; @@ -58,6 +59,19 @@ abstract class AbstractProvider */ const METHOD_POST = 'POST'; + /** + * @var string PKCE method used to fetch authorization token. + * The PKCE code challenge will be hashed with sha256 (recommended). + */ + const PKCE_METHOD_S256 = 'S256'; + + /** + * @var string PKCE method used to fetch authorization token. + * The PKCE code challenge will be sent as plain text, this is NOT recommended. + * Only use `plain` if no other option is possible. + */ + const PKCE_METHOD_PLAIN = 'plain'; + /** * @var string */ @@ -78,6 +92,11 @@ abstract class AbstractProvider */ protected $state; + /** + * @var string|null + */ + protected $pkceCode = null; + /** * @var GrantFactory */ @@ -264,6 +283,32 @@ public function getState() return $this->state; } + /** + * Set the value of the pkceCode parameter. + * + * When using PKCE this should be set before requesting an access token. + * + * @param string $pkceCode + * @return self + */ + public function setPkceCode($pkceCode) + { + $this->pkceCode = $pkceCode; + return $this; + } + + /** + * Returns the current value of the pkceCode parameter. + * + * This can be accessed by the redirect handler during authorization. + * + * @return string|null + */ + public function getPkceCode() + { + return $this->pkceCode; + } + /** * Returns the base URL for authorizing a client. * @@ -305,6 +350,27 @@ protected function getRandomState($length = 32) return bin2hex(random_bytes($length / 2)); } + /** + * Returns a new random string to use as PKCE code_verifier and + * hashed as code_challenge parameters in an authorization flow. + * Must be between 43 and 128 characters long. + * + * @param int $length Length of the random string to be generated. + * @return string + */ + protected function getRandomPkceCode($length = 64) + { + return substr( + strtr( + base64_encode(random_bytes($length)), + '+/', + '-_' + ), + 0, + $length + ); + } + /** * Returns the default scopes used by this provider. * @@ -326,6 +392,14 @@ protected function getScopeSeparator() return ','; } + /** + * @return string|null + */ + protected function getPkceMethod() + { + return null; + } + /** * Returns authorization parameters based on provided options. * @@ -355,6 +429,26 @@ protected function getAuthorizationParameters(array $options) // Store the state as it may need to be accessed later on. $this->state = $options['state']; + $pkceMethod = $this->getPkceMethod(); + if (!empty($pkceMethod)) { + $this->pkceCode = $this->getRandomPkceCode(); + if ($pkceMethod === static::PKCE_METHOD_S256) { + $options['code_challenge'] = trim( + strtr( + base64_encode(hash('sha256', $this->pkceCode, true)), + '+/', + '-_' + ), + '=' + ); + } elseif ($pkceMethod === static::PKCE_METHOD_PLAIN) { + $options['code_challenge'] = $this->pkceCode; + } else { + throw new InvalidArgumentException('Unknown PKCE method "' . $pkceMethod . '".'); + } + $options['code_challenge_method'] = $pkceMethod; + } + // Business code layer might set a different redirect_uri parameter // depending on the context, leave it as-is if (!isset($options['redirect_uri'])) { @@ -517,8 +611,8 @@ protected function getAccessTokenRequest(array $params) /** * Requests an access token using a specified grant and option set. * - * @param mixed $grant - * @param array $options + * @param mixed $grant + * @param array $options * @throws IdentityProviderException * @return AccessTokenInterface */ @@ -532,6 +626,10 @@ public function getAccessToken($grant, array $options = []) 'redirect_uri' => $this->redirectUri, ]; + if (!empty($this->pkceCode)) { + $params['code_verifier'] = $this->pkceCode; + } + $params = $grant->prepareRequestParameters($params, $options); $request = $this->getAccessTokenRequest($params); $response = $this->getParsedResponse($request); @@ -564,7 +662,7 @@ public function getRequest($method, $url, array $options = []) * * @param string $method * @param string $url - * @param AccessTokenInterface|string $token + * @param AccessTokenInterface|string|null $token * @param array $options Any of "headers", "body", and "protocolVersion". * @return RequestInterface */ diff --git a/src/Provider/Exception/IdentityProviderException.php b/src/Provider/Exception/IdentityProviderException.php index 52b7e035..55cb438f 100644 --- a/src/Provider/Exception/IdentityProviderException.php +++ b/src/Provider/Exception/IdentityProviderException.php @@ -27,7 +27,7 @@ class IdentityProviderException extends \Exception /** * @param string $message * @param int $code - * @param array|string $response The response body + * @param mixed $response The response body */ public function __construct($message, $code, $response) { @@ -39,7 +39,7 @@ public function __construct($message, $code, $response) /** * Returns the exception's response body. * - * @return array|string + * @return mixed */ public function getResponseBody() { diff --git a/src/Provider/GenericProvider.php b/src/Provider/GenericProvider.php index 74393ffd..0fc95f25 100644 --- a/src/Provider/GenericProvider.php +++ b/src/Provider/GenericProvider.php @@ -78,6 +78,11 @@ class GenericProvider extends AbstractProvider */ private $responseResourceOwnerId = 'id'; + /** + * @var string|null + */ + private $pkceMethod = null; + /** * @param array $options * @param array $collaborators @@ -114,6 +119,7 @@ protected function getConfigurableOptions() 'responseCode', 'responseResourceOwnerId', 'scopes', + 'pkceMethod', ]); } @@ -205,6 +211,14 @@ protected function getScopeSeparator() return $this->scopeSeparator ?: parent::getScopeSeparator(); } + /** + * @inheritdoc + */ + protected function getPkceMethod() + { + return $this->pkceMethod ?: parent::getPkceMethod(); + } + /** * @inheritdoc */ diff --git a/test/src/Provider/AbstractProviderTest.php b/test/src/Provider/AbstractProviderTest.php index bf24ba38..b9ebf6f1 100644 --- a/test/src/Provider/AbstractProviderTest.php +++ b/test/src/Provider/AbstractProviderTest.php @@ -332,6 +332,122 @@ public function testAuthorizationStateIsRandom() } } + public function testSetGetPkceCode() + { + $pkceCode = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'; + + $provider = $this->getMockProvider(); + $this->assertEquals($provider, $provider->setPkceCode($pkceCode)); + $this->assertEquals($pkceCode, $provider->getPkceCode()); + } + + /** + * @dataProvider pkceMethodProvider + */ + public function testPkceMethod($pkceMethod, $pkceCode, $expectedChallenge) + { + $provider = $this->getMockProvider(); + $provider->setPkceMethod($pkceMethod); + $provider->setFixedPkceCode($pkceCode); + + $url = $provider->getAuthorizationUrl(); + $this->assertSame($pkceCode, $provider->getPkceCode()); + + parse_str(parse_url($url, PHP_URL_QUERY), $qs); + $this->assertArrayHasKey('code_challenge', $qs); + $this->assertArrayHasKey('code_challenge_method', $qs); + $this->assertSame($pkceMethod, $qs['code_challenge_method']); + $this->assertSame($expectedChallenge, $qs['code_challenge']); + + // Simulate re-initialization of provider after authorization request + $provider = $this->getMockProvider(); + + $raw_response = ['access_token' => 'okay', 'expires' => time() + 3600, 'resource_owner_id' => 3]; + $stream = Mockery::mock(StreamInterface::class); + $stream + ->shouldReceive('__toString') + ->once() + ->andReturn(json_encode($raw_response)); + + $response = Mockery::mock(ResponseInterface::class); + $response + ->shouldReceive('getBody') + ->once() + ->andReturn($stream); + $response + ->shouldReceive('getHeader') + ->once() + ->with('content-type') + ->andReturn('application/json'); + + $client = Mockery::spy(ClientInterface::class, [ + 'send' => $response, + ]); + $provider->setHttpClient($client); + + // restore $pkceCode (normally done by client from session) + $provider->setPkceCode($pkceCode); + + $provider->getAccessToken('authorization_code', ['code' => 'mock_authorization_code']); + + $client + ->shouldHaveReceived('send') + ->once() + ->withArgs(function ($request) use ($pkceCode) { + parse_str((string)$request->getBody(), $body); + return $body['code_verifier'] === $pkceCode; + }); + } + + public function pkceMethodProvider() + { + return [ + [ + AbstractProvider::PKCE_METHOD_S256, + '1234567890123456789012345678901234567890', + 'pOvdVBRUuEzGcMnx9VCLr2f_0_5ZuIMmeAh4H5kqCx0', + ], + [ + AbstractProvider::PKCE_METHOD_PLAIN, + '1234567890123456789012345678901234567890', + '1234567890123456789012345678901234567890', + ], + ]; + } + + public function testInvalidPkceMethod() + { + $provider = $this->getMockProvider(); + $provider->setPkceMethod('non-existing'); + + $this->expectExceptionMessage('Unknown PKCE method "non-existing".'); + $provider->getAuthorizationUrl(); + } + + public function testPkceCodeIsRandom() + { + $last = null; + $provider = $this->getMockProvider(); + $provider->setPkceMethod('S256'); + + for ($i = 0; $i < 100; $i++) { + // Repeat the test multiple times to verify code_challenge changes + $url = $provider->getAuthorizationUrl(); + + parse_str(parse_url($url, PHP_URL_QUERY), $qs); + $this->assertTrue(1 === preg_match('/^[a-zA-Z0-9-_]{43}$/', $qs['code_challenge'])); + $this->assertNotSame($qs['code_challenge'], $last); + $last = $qs['code_challenge']; + } + } + + public function testPkceMethodIsDisabledByDefault() + { + $provider = $this->getAbstractProviderMock(); + $pkceMethod = $provider->getPkceMethod(); + $this->assertNull($pkceMethod); + } + public function testErrorResponsesCanBeCustomizedAtTheProvider() { $provider = new MockProvider([ diff --git a/test/src/Provider/Fake.php b/test/src/Provider/Fake.php index b0bedcbf..7a02b51a 100644 --- a/test/src/Provider/Fake.php +++ b/test/src/Provider/Fake.php @@ -14,6 +14,10 @@ class Fake extends AbstractProvider private $accessTokenMethod = 'POST'; + private $pkceMethod = null; + + private $fixedPkceCode = null; + public function getClientId() { return $this->clientId; @@ -59,6 +63,26 @@ public function getAccessTokenMethod() return $this->accessTokenMethod; } + public function setPkceMethod($method) + { + $this->pkceMethod = $method; + } + + public function getPkceMethod() + { + return $this->pkceMethod; + } + + public function setFixedPkceCode($code) + { + return $this->fixedPkceCode = $code; + } + + protected function getRandomPkceCode($length = 64) + { + return $this->fixedPkceCode ?: parent::getRandomPkceCode($length); + } + protected function createResourceOwner(array $response, AccessToken $token) { return new Fake\User($response); diff --git a/test/src/Provider/GenericProviderTest.php b/test/src/Provider/GenericProviderTest.php index ae3214f3..65fe93ca 100644 --- a/test/src/Provider/GenericProviderTest.php +++ b/test/src/Provider/GenericProviderTest.php @@ -55,6 +55,7 @@ public function testConfigurableOptions() 'responseCode' => 'mock_code', 'responseResourceOwnerId' => 'mock_response_uid', 'scopes' => ['mock', 'scopes'], + 'pkceMethod' => 'S256', ]; $provider = new GenericProvider($options + [ @@ -88,6 +89,10 @@ public function testConfigurableOptions() $getScopeSeparator = $reflection->getMethod('getScopeSeparator'); $getScopeSeparator->setAccessible(true); $this->assertEquals($options['scopeSeparator'], $getScopeSeparator->invoke($provider)); + + $getPkceMethod = $reflection->getMethod('getPkceMethod'); + $getPkceMethod->setAccessible(true); + $this->assertEquals($options['pkceMethod'], $getPkceMethod->invoke($provider)); } public function testResourceOwnerDetails() diff --git a/test/src/Tool/ProviderRedirectTraitTest.php b/test/src/Tool/ProviderRedirectTraitTest.php index e761cd9b..dc06f45a 100644 --- a/test/src/Tool/ProviderRedirectTraitTest.php +++ b/test/src/Tool/ProviderRedirectTraitTest.php @@ -15,6 +15,11 @@ class ProviderRedirectTraitTest extends TestCase { use ProviderRedirectTrait; + /** + * @var ClientInterface + */ + private $httpClient; + public function getHttpClient() { return $this->httpClient;